diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index f231cc4d5..294e62539 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -100,7 +100,7 @@ public final class Config { public static final boolean REMOVE_BROKEN_DEVICES = false; public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; - public static final boolean TWELVE_BYTE_IV = false; + public static final boolean TWELVE_BYTE_IV = true; public static final boolean USE_BOOKMARKS2 = false; @@ -125,7 +125,7 @@ public final class Config { public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5; public static final int MAM_MAX_MESSAGES = 750; - public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; + public static final ChatState DEFAULT_CHAT_STATE = ChatState.ACTIVE; public static final int TYPING_TIMEOUT = 8; public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 44336d4fc..018a86b5d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -57,1512 +57,1511 @@ import rocks.xmpp.addr.Jid; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify"; - public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; - public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; - public static final String PEP_OMEMO_WHITELISTED = PEP_PREFIX + ".whitelisted"; - - public static final String LOGPREFIX = "AxolotlService"; - - private static final int NUM_KEYS_TO_PUBLISH = 100; - private static final int publishTriesThreshold = 3; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - private final SQLiteAxolotlStore axolotlStore; - private final SessionMap sessions; - private final Map> deviceIds; - private final Map messageCache; - private final FetchStatusMap fetchStatusMap; - private final Map fetchDeviceListStatus = new HashMap<>(); - private final HashMap> fetchDeviceIdsMap = new HashMap<>(); - private final SerialSingleThreadExecutor executor; - private int numPublishTriesOnEmptyPep = 0; - private boolean pepBroken = false; - private final Set healingAttempts = new HashSet<>(); - private int lastDeviceListNotificationHash = 0; - private final HashSet cleanedOwnDeviceIds = new HashSet<>(); - private Set postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment - private Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup - - private AtomicBoolean changeAccessMode = new AtomicBoolean(false); - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (Config.supportOmemo() - && account.getXmppConnection() != null - && account.getXmppConnection().getFeatures().pep()) { - publishBundlesIfNeeded(true, false); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping OMEMO initialization"); - } - } - - private boolean hasErrorFetchingDeviceList(Jid jid) { - Boolean status = fetchDeviceListStatus.get(jid); - return status != null && !status; - } - - public boolean hasErrorFetchingDeviceList(List jids) { - for(Jid jid : jids) { - if (hasErrorFetchingDeviceList(jid)) { - return true; - } - } - return false; - } - - public boolean fetchMapHasErrors(List jids) { - for (Jid jid : jids) { - if (deviceIds.get(jid) != null) { - for (Integer foreignId : this.deviceIds.get(jid)) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); - if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) { - return true; - } - } - } - } - return false; - } - - public void preVerifyFingerprint(Contact contact, String fingerprint) { - axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint); - } - - public void preVerifyFingerprint(Account account, String fingerprint) { - axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint); - } - - public boolean hasVerifiedKeys(String name) { - for (XmppAxolotlSession session : this.sessions.getAll(name).values()) { - if (session.getTrust().isVerified()) { - return true; - } - } - return false; - } - - private static class AxolotlAddressMap { - protected Map> map; - protected final Object MAP_LOCK = new Object(); - - public AxolotlAddressMap() { - this.map = new HashMap<>(); - } - - public void put(SignalProtocolAddress address, T value) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - devices = new HashMap<>(); - map.put(address.getName(), devices); - } - devices.put(address.getDeviceId(), value); - } - } - - public T get(SignalProtocolAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map getAll(String name) { - synchronized (MAP_LOCK) { - Map devices = map.get(name); - if (devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(SignalProtocolAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - public void clear() { - map.clear(); - } - - } - - private static class SessionMap extends AxolotlAddressMap { - private final XmppConnectionService xmppConnectionService; - private final Account account; - - public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { - super(); - this.xmppConnectionService = service; - this.account = account; - this.fillMap(store); - } - - public Set findCounterpartsForSourceId(Integer sid) { - Set candidates = new HashSet<>(); - synchronized (MAP_LOCK) { - for(Map.Entry> entry : map.entrySet()) { - String key = entry.getKey(); - if (entry.getValue().containsKey(sid)) { - candidates.add(Jid.of(key)); - } - } - } - return candidates; - } - - private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { - for (Integer deviceId : deviceIds) { - SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); - IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); - if (Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); - if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.of(bareJid); - Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - } - } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); - } - } - - private void fillMap(SQLiteAxolotlStore store) { - List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); - putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); - for (String address : store.getKnownAddresses()) { - deviceIds = store.getSubDeviceSessions(address); - putDevicesForJid(address, deviceIds, store); - } - } - - @Override - public void put(SignalProtocolAddress address, XmppAxolotlSession value) { - super.put(address, value); - value.setNotFresh(); - } - - public void put(XmppAxolotlSession session) { - this.put(session.getRemoteAddress(), session); - } - } - - public enum FetchStatus { - PENDING, - SUCCESS, - SUCCESS_VERIFIED, - TIMEOUT, - SUCCESS_TRUSTED, - ERROR - } - - private static class FetchStatusMap extends AxolotlAddressMap { - - public void clearErrorFor(Jid jid) { - synchronized (MAP_LOCK) { - Map devices = this.map.get(jid.asBareJid().toString()); - if (devices == null) { - return; - } - for (Map.Entry entry : devices.entrySet()) { - if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); - entry.setValue(FetchStatus.TIMEOUT); - } - } - } - } - } - - public static String getLogprefix(Account account) { - return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): "; - } - - public AxolotlService(Account account, XmppConnectionService connectionService) { - if (account == null || connectionService == null) { - throw new IllegalArgumentException("account and service cannot be null"); - } - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } - this.mXmppConnectionService = connectionService; - this.account = account; - this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.deviceIds = new HashMap<>(); - this.messageCache = new HashMap<>(); - this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); - this.fetchStatusMap = new FetchStatusMap(); - this.executor = new SerialSingleThreadExecutor("Axolotl"); - } - - public String getOwnFingerprint() { - return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); - } - - public Set getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { - return axolotlStore.getContactKeysWithTrust(jid.asBareJid().toString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, List jids) { - Set keys = new HashSet<>(); - for (Jid jid : jids) { - keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), status)); - } - return keys; - } - - public Set findCounterpartsBySourceId(int sid) { - return sessions.findCounterpartsForSourceId(sid); - } - - public long getNumTrustedKeys(Jid jid) { - return axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()); - } - - public boolean anyTargetHasNoTrustedKeys(List jids) { - for (Jid jid : jids) { - if (axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()) == 0) { - return true; - } - } - return false; - } - - private SignalProtocolAddress getAddressForJid(Jid jid) { - return new SignalProtocolAddress(jid.toString(), 0); - } - - public Collection findOwnSessions() { - SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); - Collections.sort(s); - return s; - } - - - public Collection findSessionsForContact(Contact contact) { - SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); - Collections.sort(s); - return s; - } - - private Set findSessionsForConversation(Conversation conversation) { - if (conversation.getContact().isSelf()) { - //will be added in findOwnSessions() - return Collections.emptySet(); - } - HashSet sessions = new HashSet<>(); - for (Jid jid : conversation.getAcceptedCryptoTargets()) { - sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); - } - return sessions; - } - - private boolean hasAny(Jid jid) { - return sessions.hasAny(getAddressForJid(jid)); - } - - public boolean isPepBroken() { - return this.pepBroken; - } - - public void resetBrokenness() { - this.pepBroken = false; - this.numPublishTriesOnEmptyPep = 0; - this.lastDeviceListNotificationHash = 0; - this.healingAttempts.clear(); - } - - public void clearErrorsInFetchStatusMap(Jid jid) { - fetchStatusMap.clearErrorFor(jid); - fetchDeviceListStatus.remove(jid); - } - - public void regenerateKeys(boolean wipeOther) { - axolotlStore.regenerate(); - sessions.clear(); - fetchStatusMap.clear(); - fetchDeviceIdsMap.clear(); - fetchDeviceListStatus.clear(); - publishBundlesIfNeeded(true, wipeOther); - } - - public void destroy() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use"); - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - } - - public AxolotlService makeNew() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": make new axolotl service"); - return new AxolotlService(this.account, this.mXmppConnectionService); - } - - public int getOwnDeviceId() { - return axolotlStore.getLocalRegistrationId(); - } - - public SignalProtocolAddress getOwnAxolotlAddress() { - return new SignalProtocolAddress(account.getJid().asBareJid().toString(), getOwnDeviceId()); - } - - public Set getOwnDeviceIds() { - return this.deviceIds.get(account.getJid().asBareJid()); - } - - public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { - final int hash = deviceIds.hashCode(); - final boolean me = jid.asBareJid().equals(account.getJid().asBareJid()); - if (me) { - if (hash != 0 && hash == this.lastDeviceListNotificationHash) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list"); - return; - } - this.lastDeviceListNotificationHash = hash; - } - boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId()); - if (me) { - deviceIds.remove(getOwnDeviceId()); - } - Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); - expiredDevices.removeAll(deviceIds); - for (Integer deviceId : expiredDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (session.getTrust().isActive()) { - session.setTrust(session.getTrust().toInactive()); - } - } - } - Set newDevices = new HashSet<>(deviceIds); - for (Integer deviceId : newDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (!session.getTrust().isActive()) { - Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint()); - session.setTrust(session.getTrust().toActive()); - } - } - } - if (me) { - if (Config.OMEMO_AUTO_EXPIRY != 0) { - needsPublishing |= deviceIds.removeAll(getExpiredDevices()); - } - needsPublishing |= this.changeAccessMode.get(); - for (Integer deviceId : deviceIds) { - SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - if (sessions.get(ownDeviceAddress) == null) { - FetchStatus status = fetchStatusMap.get(ownDeviceAddress); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING); - this.buildSessionFromPEP(ownDeviceAddress); - } - } - } - if (needsPublishing) { - publishOwnDeviceId(deviceIds); - } - } - final Set oldSet = this.deviceIds.get(jid); - final boolean changed = oldSet == null || oldSet.hashCode() != hash; - this.deviceIds.put(jid, deviceIds); - if (changed) { - mXmppConnectionService.updateConversationUi(); //update the lock icon - mXmppConnectionService.keyStatusUpdated(null); - if (me) { - mXmppConnectionService.updateAccountUi(); - } - } else { - Log.d(Config.LOGTAG,"skipped device list update because it hasn't changed"); - } - } - - public void wipeOtherPepDevices() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); - return; - } - Set deviceIds = new HashSet<>(); - deviceIds.add(getOwnDeviceId()); - publishDeviceIdsAndRefineAccessModel(deviceIds); - } - - public void distrustFingerprint(final String fingerprint) { - final String fp = fingerprint.replaceAll("\\s", ""); - final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp); - axolotlStore.setFingerprintStatus(fp, fingerprintStatus.toUntrusted()); - } - - private void publishOwnDeviceIdIfNeeded() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); - } else { - //TODO consider calling registerDevices only after item-not-found to account for broken PEPs - Element item = mXmppConnectionService.getIqParser().getItem(packet); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); - registerDevices(account.getJid().asBareJid(), deviceIds); - } - } - }); - } - - private Set getExpiredDevices() { - Set devices = new HashSet<>(); - for (XmppAxolotlSession session : findOwnSessions()) { - if (session.getTrust().isActive()) { - long diff = System.currentTimeMillis() - session.getTrust().getLastActivation(); - if (diff > Config.OMEMO_AUTO_EXPIRY) { - long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint()); - long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0)); - if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) { - devices.add(session.getRemoteAddress().getDeviceId()); - session.setTrust(session.getTrust().toInactive()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago"); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago"); - } - } //TODO print last activation diff - } - } - return devices; - } - - private void publishOwnDeviceId(Set deviceIds) { - Set deviceIdsCopy = new HashSet<>(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids"); - if (deviceIdsCopy.isEmpty()) { - if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); - pepBroken = true; - return; - } else { - numPublishTriesOnEmptyPep++; - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); - } - } else { - numPublishTriesOnEmptyPep = 0; - } - deviceIdsCopy.add(getOwnDeviceId()); - publishDeviceIdsAndRefineAccessModel(deviceIdsCopy); - } - - private void publishDeviceIdsAndRefineAccessModel(Set ids) { - publishDeviceIdsAndRefineAccessModel(ids, true); - } - - private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); - mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - - @Override - public void onPushFailed() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - }); - } else { - if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preConditionNotMet) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": device list pre condition still not met on second attempt"); - } else if (error != null) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); - } - - } - } - } - }); - } - - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - try { - IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); - X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG()); - verifier.update(axolotlPublicKey.serialize()); - byte[] signature = verifier.sign(); - IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - }); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - - if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE)); - } else { - if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - } - if (this.changeAccessMode.get()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; //ignore timeout. do nothing - } - - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.findChild("error"); - if (error == null || !error.hasChild("item-not-found")) { - pepBroken = true; - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); - return; - } - } - - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); - } - try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - changed = true; - } - - // Validate signedPreKeyRecord + ID - SignedPreKeyRecord signedPreKeyRecord; - int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); - try { - signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if (flush - || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - - // Validate PreKeys - Set preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); - } - } catch (InvalidKeyIdException ignored) { - } - } - } - int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); - if (newKeys > 0) { - List newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId() + 1, newKeys); - preKeyRecords.addAll(newRecords); - for (PreKeyRecord record : newRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); - } - - - if (changed || changeAccessMode.get()) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - }); - } - - private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true); - } - - private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe, - final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, getOwnDeviceId(), publishOptions); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing..."); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preconditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - - @Override - public void onPushFailed() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - }); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preconditionNotMet) { - Log.d(Config.LOGTAG,getLogprefix(account) + "bundle precondition still not met after second attempt"); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.toString()); - } - pepBroken = true; - } - } - }); - } - - public void deleteOmemoIdentity() { - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node); - mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null); - final Set ownDeviceIds = getOwnDeviceIds(); - publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); - } - - public List getCryptoTargets(Conversation conversation) { - final List jids; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - jids = new ArrayList<>(); - jids.add(conversation.getJid().asBareJid()); - } else { - jids = conversation.getMucOptions().getMembers(false); - } - return jids; - } - - public FingerprintStatus getFingerprintTrust(String fingerprint) { - return axolotlStore.getFingerprintStatus(fingerprint); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return axolotlStore.getFingerprintCertificate(fingerprint); - } - - public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { - axolotlStore.setFingerprintStatus(fingerprint, status); - } - - private void verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); - final SignalProtocolAddress address = session.getRemoteAddress(); - final IdentityKey identityKey = session.getIdentityKey(); - try { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Pair verification = mXmppConnectionService.getIqParser().verification(packet); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { - try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - finishBuildingSessionsFromPEP(address); - return; - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); - } - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - }); - } catch (IllegalArgumentException e) { - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - } - } - - private final Set PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>(); - - private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); - Map own = fetchStatusMap.getAll(ownAddress.getName()); - Map remote = fetchStatusMap.getAll(address.getName()); - if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { - FetchStatus report = null; - if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { - report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { - report = FetchStatus.SUCCESS_VERIFIED; - } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { - report = FetchStatus.SUCCESS_TRUSTED; - } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { - report = FetchStatus.ERROR; - } - mXmppConnectionService.keyStatusUpdated(report); - } - if (Config.REMOVE_BROKEN_DEVICES) { - Set ownDeviceIds = new HashSet<>(getOwnDeviceIds()); - boolean publish = false; - for (Map.Entry entry : own.entrySet()) { - int id = entry.getKey(); - if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) { - publish = true; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error fetching own device with id " + id + ". removing from announcement"); - } - } - if (publish) { - publishOwnDeviceId(ownDeviceIds); - } - } - } - - public boolean hasEmptyDeviceList(Jid jid) { - return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty()); - } - - public interface OnDeviceIdsFetched { - void fetched(Jid jid, Set deviceIds); - } - - public interface OnMultipleDeviceIdFetched { - void fetched(); - } - - public void fetchDeviceIds(final Jid jid) { - fetchDeviceIds(jid, null); - } - - private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { - IqPacket packet; - synchronized (this.fetchDeviceIdsMap) { - List callbacks = this.fetchDeviceIdsMap.get(jid); - if (callbacks != null) { - if (callback != null) { - callbacks.add(callback); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); - packet = null; - } else { - callbacks = new ArrayList<>(); - if (callback != null) { - callbacks.add(callback); - } - this.fetchDeviceIdsMap.put(jid, callbacks); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); - packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); - } - } - if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - fetchDeviceListStatus.put(jid, true); - Element item = mXmppConnectionService.getIqParser().getItem(response); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - registerDevices(jid, deviceIds); - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, deviceIds); - } - } - } else { - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - fetchDeviceListStatus.remove(jid); - } else { - fetchDeviceListStatus.put(jid, false); - } - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, null); - } - } - } - }); - } - } - - private void fetchDeviceIds(List jids, final OnMultipleDeviceIdFetched callback) { - final ArrayList unfinishedJids = new ArrayList<>(jids); - synchronized (unfinishedJids) { - for (Jid jid : unfinishedJids) { - fetchDeviceIds(jid, (j, deviceIds) -> { - synchronized (unfinishedJids) { - unfinishedJids.remove(j); - if (unfinishedJids.size() == 0 && callback != null) { - callback.fetched(); - } - } - }); - } - } - } - - interface OnSessionBuildFromPep { - void onSessionBuildSuccessful(); - void onSessionBuildFailed(); - } - - private void buildSessionFromPEP(final SignalProtocolAddress address) { - buildSessionFromPEP(address, null); - } - - private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); - if (address.equals(getOwnAxolotlAddress())) { - throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); - } - - final Jid jid = Jid.of(address.getName()); - final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - return; - } - Random random = new Random(); - final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (preKey == null) { - //should never happen - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - return; - } - - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - try { - SessionBuilder builder = new SessionBuilder(axolotlStore, address); - builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); - sessions.put(address, session); - if (Config.X509_VERIFICATION) { - verifySessionWithPEP(session); //TODO; maybe inject callback in here too - } else { - FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); - FetchStatus fetchStatus; - if (status != null && status.isVerified()) { - fetchStatus = FetchStatus.SUCCESS_VERIFIED; - } else if (status != null && status.isTrusted()) { - fetchStatus = FetchStatus.SUCCESS_TRUSTED; - } else { - fetchStatus = FetchStatus.SUCCESS; - } - fetchStatusMap.put(address, fetchStatus); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildSuccessful(); - } - } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - } - } else { - fetchStatusMap.put(address, FetchStatus.ERROR); - Element error = packet.findChild("error"); - boolean itemNotFound = error != null && error.hasChild("item-not-found"); - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && itemNotFound && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - } - }); - } - - private void removeFromDeviceAnnouncement(Integer id) { - HashSet temp = new HashSet<>(getOwnDeviceIds()); - if (temp.remove(id)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+" remove own device id "+id+" from announcement. devices left:"+temp); - publishOwnDeviceId(temp); - } - } - - public Set findDevicesWithoutSession(final Conversation conversation) { - Set addresses = new HashSet<>(); - for (Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); - final Set ids = deviceIds.get(jid); - if (ids != null && !ids.isEmpty()) { - for (Integer foreignId : ids) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); - } - } - } - } - } else { - mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR); - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); - } - } - Set ownIds = this.deviceIds.get(account.getJid().asBareJid()); - for (Integer ownId : (ownIds != null ? ownIds : new HashSet())) { - SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().asBareJid() + ":" + ownId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); - } - } - } - } - - return addresses; - } - - public boolean createSessionsIfNeeded(final Conversation conversation) { - final List jidsWithEmptyDeviceList = getCryptoTargets(conversation); - for (Iterator iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext(); ) { - final Jid jid = iterator.next(); - if (!hasEmptyDeviceList(jid)) { - iterator.remove(); - } - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); - if (jidsWithEmptyDeviceList.size() > 0) { - fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); - return true; - } else { - return createSessionsIfNeededActual(conversation); - } - } - - private boolean createSessionsIfNeededActual(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); - boolean newSessions = false; - Set addresses = findDevicesWithoutSession(conversation); - for (SignalProtocolAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); - FetchStatus status = fetchStatusMap.get(address); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(address); - newSessions = true; - } else if (status == FetchStatus.PENDING) { - newSessions = true; - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); - } - } - - return newSessions; - } - - public boolean trustedSessionVerified(final Conversation conversation) { - final Set sessions = new HashSet<>(); - sessions.addAll(findSessionsForConversation(conversation)); - sessions.addAll(findOwnSessions()); - boolean verified = false; - for (XmppAxolotlSession session : sessions) { - if (session.getTrust().isTrustedAndActive()) { - if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { - verified = true; - } else { - return false; - } - } - } - return verified; - } - - public boolean hasPendingKeyFetches(List jids) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); - if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) { - return true; - } - synchronized (this.fetchDeviceIdsMap) { - for (Jid jid : jids) { - SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.asBareJid().toString(), 0); - if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) { - return true; - } - } - } - return false; - } - - @Nullable - private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation c) { - Set remoteSessions = findSessionsForConversation(c); - final boolean acceptEmpty = (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) || c.getContact().isSelf(); - Collection ownSessions = findOwnSessions(); - if (remoteSessions.isEmpty() && !acceptEmpty) { - return false; - } - for (XmppAxolotlSession session : remoteSessions) { - axolotlMessage.addDevice(session); - } - for (XmppAxolotlSession session : ownSessions) { - axolotlMessage.addDevice(session); - } - - return true; - } - - //this is being used for private muc messages only - private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { - if (jid == null) { - return false; - } - HashSet sessions = new HashSet<>(); - sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); - if (sessions.isEmpty()) { - return false; - } - sessions.addAll(findOwnSessions()); - for(XmppAxolotlSession session : sessions) { - axolotlMessage.addDevice(session); - } - return true; - } - - @Nullable - public XmppAxolotlMessage encrypt(Message message) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } - try { - axolotlMessage.encrypt(content); - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); - return null; - } - - final boolean success; - if (message.isPrivateMessage()) { - success = buildHeader(axolotlMessage, message.getTrueCounterpart()); - } else { - success = buildHeader(axolotlMessage, (Conversation) message.getConversation()); - } - return success ? axolotlMessage : null; - } - - public void preparePayloadMessage(final Message message, final boolean delay) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = encrypt(message); - if (axolotlMessage == null) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - //mXmppConnectionService.updateConversationUi(); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay); - } - } - }); - } - - public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { - executor.execute(new Runnable() { - @Override - public void run() { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - onMessageCreatedCallback.run(axolotlMessage); - } else { - onMessageCreatedCallback.run(null); - } - } - }); - } - - public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { - XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); - if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); - messageCache.remove(message.getUuid()); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); - } - return axolotlMessage; - } - - private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - return (identityKey != null) - ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) - : null; - } - - private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { - SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toString(), message.getSenderDeviceId()); - return getReceivingSession(senderAddress); - - } - - private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { - XmppAxolotlSession session = sessions.get(senderAddress); - if (session == null) { - //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message); - session = recreateUncachedSession(senderAddress); - if (session == null) { - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); - } - } - return session; - } - - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - - XmppAxolotlSession session = getReceivingSession(message); - int ownDeviceId = getOwnDeviceId(); - try { - plaintextMessage = message.decrypt(session, ownDeviceId); - Integer preKeyId = session.getPreKeyIdAndReset(); - if (preKeyId != null) { - postPreKeyMessageHandling(session, postponePreKeyMessageHandling); - } - } catch (NotEncryptedForThisDeviceException e) { - if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); - } else { - throw e; - } - } catch (final BrokenSessionException e) { - throw e; - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e); - } - - if (session.isFresh() && plaintextMessage != null) { - putFreshSession(session); - } - - return plaintextMessage; - } - - public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { - Log.e(Config.LOGTAG,account.getJid().asBareJid()+": broken session with "+e.getSignalProtocolAddress().toString()+" detected", e); - if (postpone) { - postponedHealing.add(e.getSignalProtocolAddress()); - } else { - notifyRequiresHealing(e.getSignalProtocolAddress()); - } - } - - private void notifyRequiresHealing(final SignalProtocolAddress signalProtocolAddress) { - if (healingAttempts.add(signalProtocolAddress)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": attempt to heal "+signalProtocolAddress); - buildSessionFromPEP(signalProtocolAddress, new OnSessionBuildFromPep() { - @Override - public void onSessionBuildSuccessful() { - Log.d(Config.LOGTAG, "successfully build new session from pep after detecting broken session"); - completeSession(getReceivingSession(signalProtocolAddress)); - } - - @Override - public void onSessionBuildFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to build new session from pep after detecting broken session"); - } - }); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": do not attempt to heal "+signalProtocolAddress+" again"); - } - } - - private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { - if (postpone) { - postponedSessions.add(session); - } else { - if (axolotlStore.flushPreKeys()) { - publishBundlesIfNeeded(false, false); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": nothing to flush. Not republishing key"); - } - if (trustedOrPreviouslyResponded(session)) { - completeSession(session); - } - } - } - - public void processPostponed() { - if (postponedSessions.size() > 0) { - if (axolotlStore.flushPreKeys()) { - publishBundlesIfNeeded(false, false); - } - } - final Iterator iterator = postponedSessions.iterator(); - while (iterator.hasNext()) { - final XmppAxolotlSession session = iterator.next(); - if (trustedOrPreviouslyResponded(session)) { - completeSession(session); - } - iterator.remove(); - } - final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); - while (postponedHealingAttemptsIterator.hasNext()) { - notifyRequiresHealing(postponedHealingAttemptsIterator.next()); - postponedHealingAttemptsIterator.remove(); - } - } - - - private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) { - try { - return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName())); - } catch (IllegalArgumentException e) { - return false; - } - } - - public boolean trustedOrPreviouslyResponded(Jid jid) { - final Contact contact = account.getRoster().getContact(jid); - if (contact.showInRoster() || contact.isSelf()) { - return true; - } - final Conversation conversation = mXmppConnectionService.find(account, jid); - return conversation != null && conversation.sentMessagesCount() > 0; - } - - private void completeSession(XmppAxolotlSession session) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - axolotlMessage.addDevice(session, true); - try { - final Jid jid = Jid.of(session.getRemoteAddress().getName()); - MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); - mXmppConnectionService.sendMessagePacket(account, packet); - } catch (IllegalArgumentException e) { - throw new Error("Remote addresses are created from jid and should convert back to jid", e); - } - } - - - public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { - final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; - final XmppAxolotlSession session = getReceivingSession(message); - try { - keyTransportMessage = message.getParameters(session, getOwnDeviceId()); - Integer preKeyId = session.getPreKeyIdAndReset(); - if (preKeyId != null) { - postPreKeyMessageHandling(session, postponePreKeyMessageHandling); - } - } catch (CryptoFailedException e) { - Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage()); - return null; - } - - if (session.isFresh() && keyTransportMessage != null) { - putFreshSession(session); - } - - return keyTransportMessage; - } - - private void putFreshSession(XmppAxolotlSession session) { - sessions.put(session); - if (Config.X509_VERIFICATION) { - if (session.getIdentityKey() != null) { - verifySessionWithPEP(session); - } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); - } - } - } + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify"; + public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; + public static final String PEP_OMEMO_WHITELISTED = PEP_PREFIX + ".whitelisted"; + + public static final String LOGPREFIX = "AxolotlService"; + + private static final int NUM_KEYS_TO_PUBLISH = 100; + private static final int publishTriesThreshold = 3; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final Map> deviceIds; + private final Map messageCache; + private final FetchStatusMap fetchStatusMap; + private final Map fetchDeviceListStatus = new HashMap<>(); + private final HashMap> fetchDeviceIdsMap = new HashMap<>(); + private final SerialSingleThreadExecutor executor; + private final Set healingAttempts = new HashSet<>(); + private final HashSet cleanedOwnDeviceIds = new HashSet<>(); + private final Set PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>(); + private int numPublishTriesOnEmptyPep = 0; + private boolean pepBroken = false; + private int lastDeviceListNotificationHash = 0; + private Set postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment + private Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup + private AtomicBoolean changeAccessMode = new AtomicBoolean(false); + + public AxolotlService(Account account, XmppConnectionService connectionService) { + if (account == null || connectionService == null) { + throw new IllegalArgumentException("account and service cannot be null"); + } + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor("Axolotl"); + } + + public static String getLogprefix(Account account) { + return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): "; + } + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (Config.supportOmemo() + && account.getXmppConnection() != null + && account.getXmppConnection().getFeatures().pep()) { + publishBundlesIfNeeded(true, false); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping OMEMO initialization"); + } + } + + private boolean hasErrorFetchingDeviceList(Jid jid) { + Boolean status = fetchDeviceListStatus.get(jid); + return status != null && !status; + } + + public boolean hasErrorFetchingDeviceList(List jids) { + for (Jid jid : jids) { + if (hasErrorFetchingDeviceList(jid)) { + return true; + } + } + return false; + } + + public boolean fetchMapHasErrors(List jids) { + for (Jid jid : jids) { + if (deviceIds.get(jid) != null) { + for (Integer foreignId : this.deviceIds.get(jid)) { + SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) { + return true; + } + } + } + } + return false; + } + + public void preVerifyFingerprint(Contact contact, String fingerprint) { + axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint); + } + + public void preVerifyFingerprint(Account account, String fingerprint) { + axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint); + } + + public boolean hasVerifiedKeys(String name) { + for (XmppAxolotlSession session : this.sessions.getAll(name).values()) { + if (session.getTrust().isVerified()) { + return true; + } + } + return false; + } + + public String getOwnFingerprint() { + return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); + } + + public Set getKeysWithTrust(FingerprintStatus status) { + return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); + } + + public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { + return axolotlStore.getContactKeysWithTrust(jid.asBareJid().toString(), status); + } + + public Set getKeysWithTrust(FingerprintStatus status, List jids) { + Set keys = new HashSet<>(); + for (Jid jid : jids) { + keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), status)); + } + return keys; + } + + public Set findCounterpartsBySourceId(int sid) { + return sessions.findCounterpartsForSourceId(sid); + } + + public long getNumTrustedKeys(Jid jid) { + return axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()); + } + + public boolean anyTargetHasNoTrustedKeys(List jids) { + for (Jid jid : jids) { + if (axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()) == 0) { + return true; + } + } + return false; + } + + private SignalProtocolAddress getAddressForJid(Jid jid) { + return new SignalProtocolAddress(jid.toString(), 0); + } + + public Collection findOwnSessions() { + SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); + ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); + Collections.sort(s); + return s; + } + + public Collection findSessionsForContact(Contact contact) { + SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); + ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); + Collections.sort(s); + return s; + } + + private Set findSessionsForConversation(Conversation conversation) { + if (conversation.getContact().isSelf()) { + //will be added in findOwnSessions() + return Collections.emptySet(); + } + HashSet sessions = new HashSet<>(); + for (Jid jid : conversation.getAcceptedCryptoTargets()) { + sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); + } + return sessions; + } + + private boolean hasAny(Jid jid) { + return sessions.hasAny(getAddressForJid(jid)); + } + + public boolean isPepBroken() { + return this.pepBroken; + } + + public void resetBrokenness() { + this.pepBroken = false; + this.numPublishTriesOnEmptyPep = 0; + this.lastDeviceListNotificationHash = 0; + this.healingAttempts.clear(); + } + + public void clearErrorsInFetchStatusMap(Jid jid) { + fetchStatusMap.clearErrorFor(jid); + fetchDeviceListStatus.remove(jid); + } + + public void regenerateKeys(boolean wipeOther) { + axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); + fetchDeviceIdsMap.clear(); + fetchDeviceListStatus.clear(); + publishBundlesIfNeeded(true, wipeOther); + } + + public void destroy() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use"); + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + } + + public AxolotlService makeNew() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": make new axolotl service"); + return new AxolotlService(this.account, this.mXmppConnectionService); + } + + public int getOwnDeviceId() { + return axolotlStore.getLocalRegistrationId(); + } + + public SignalProtocolAddress getOwnAxolotlAddress() { + return new SignalProtocolAddress(account.getJid().asBareJid().toString(), getOwnDeviceId()); + } + + public Set getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().asBareJid()); + } + + public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { + final int hash = deviceIds.hashCode(); + final boolean me = jid.asBareJid().equals(account.getJid().asBareJid()); + if (me) { + if (hash != 0 && hash == this.lastDeviceListNotificationHash) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list"); + return; + } + this.lastDeviceListNotificationHash = hash; + } + boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId()); + if (me) { + deviceIds.remove(getOwnDeviceId()); + } + Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); + expiredDevices.removeAll(deviceIds); + for (Integer deviceId : expiredDevices) { + SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null) { + if (session.getTrust().isActive()) { + session.setTrust(session.getTrust().toInactive()); + } + } + } + Set newDevices = new HashSet<>(deviceIds); + for (Integer deviceId : newDevices) { + SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + XmppAxolotlSession session = sessions.get(address); + if (session != null && session.getFingerprint() != null) { + if (!session.getTrust().isActive()) { + Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint()); + session.setTrust(session.getTrust().toActive()); + } + } + } + if (me) { + if (Config.OMEMO_AUTO_EXPIRY != 0) { + needsPublishing |= deviceIds.removeAll(getExpiredDevices()); + } + needsPublishing |= this.changeAccessMode.get(); + for (Integer deviceId : deviceIds) { + SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + if (sessions.get(ownDeviceAddress) == null) { + FetchStatus status = fetchStatusMap.get(ownDeviceAddress); + if (status == null || status == FetchStatus.TIMEOUT) { + fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING); + this.buildSessionFromPEP(ownDeviceAddress); + } + } + } + if (needsPublishing) { + publishOwnDeviceId(deviceIds); + } + } + final Set oldSet = this.deviceIds.get(jid); + final boolean changed = oldSet == null || oldSet.hashCode() != hash; + this.deviceIds.put(jid, deviceIds); + if (changed) { + mXmppConnectionService.updateConversationUi(); //update the lock icon + mXmppConnectionService.keyStatusUpdated(null); + if (me) { + mXmppConnectionService.updateAccountUi(); + } + } else { + Log.d(Config.LOGTAG, "skipped device list update because it hasn't changed"); + } + } + + public void wipeOtherPepDevices() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + return; + } + Set deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + publishDeviceIdsAndRefineAccessModel(deviceIds); + } + + public void distrustFingerprint(final String fingerprint) { + final String fp = fingerprint.replaceAll("\\s", ""); + final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp); + axolotlStore.setFingerprintStatus(fp, fingerprintStatus.toUntrusted()); + } + + private void publishOwnDeviceIdIfNeeded() { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); + } else { + //TODO consider calling registerDevices only after item-not-found to account for broken PEPs + Element item = mXmppConnectionService.getIqParser().getItem(packet); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); + registerDevices(account.getJid().asBareJid(), deviceIds); + } + } + }); + } + + private Set getExpiredDevices() { + Set devices = new HashSet<>(); + for (XmppAxolotlSession session : findOwnSessions()) { + if (session.getTrust().isActive()) { + long diff = System.currentTimeMillis() - session.getTrust().getLastActivation(); + if (diff > Config.OMEMO_AUTO_EXPIRY) { + long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint()); + long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0)); + if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) { + devices.add(session.getRemoteAddress().getDeviceId()); + session.setTrust(session.getTrust().toInactive()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago"); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago"); + } + } //TODO print last activation diff + } + } + return devices; + } + + private void publishOwnDeviceId(Set deviceIds) { + Set deviceIdsCopy = new HashSet<>(deviceIds); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids"); + if (deviceIdsCopy.isEmpty()) { + if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); + pepBroken = true; + return; + } else { + numPublishTriesOnEmptyPep++; + Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); + } + } else { + numPublishTriesOnEmptyPep = 0; + } + deviceIdsCopy.add(getOwnDeviceId()); + publishDeviceIdsAndRefineAccessModel(deviceIdsCopy); + } + + private void publishDeviceIdsAndRefineAccessModel(Set ids) { + publishDeviceIdsAndRefineAccessModel(ids, true); + } + + private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { + final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; + final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet); + if (firstAttempt && preConditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); + mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceIdsAndRefineAccessModel(ids, false); + } + + @Override + public void onPushFailed() { + publishDeviceIdsAndRefineAccessModel(ids, false); + } + }); + } else { + if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); + account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (packet.getType() == IqPacket.TYPE.ERROR) { + if (preConditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt"); + } else if (error != null) { + pepBroken = true; + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); + } + + } + } + } + }); + } + + public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, + final Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + try { + IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); + PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); + X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG()); + verifier.update(axolotlPublicKey.serialize()); + byte[] signature = verifier.sign(); + IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, IqPacket packet) { + String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } + + @Override + public void onPushFailed() { + Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); + } + }); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { + if (pepBroken) { + Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); + return; + } + + if (account.getXmppConnection().getFeatures().pepPublishOptions()) { + this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE)); + } else { + if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + } + if (this.changeAccessMode.get()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); + } + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + return; //ignore timeout. do nothing + } + + if (packet.getType() == IqPacket.TYPE.ERROR) { + Element error = packet.findChild("error"); + if (error == null || !error.hasChild("item-not-found")) { + pepBroken = true; + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); + return; + } + } + + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + boolean flush = false; + if (bundle == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); + try { + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + + // Validate PreKeys + Set preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); + } + + + if (changed || changeAccessMode.get()) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } else { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); + } + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); + if (wipe) { + wipeOtherPepDevices(); + } else if (announce) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + }); + } + + private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, + Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true); + } + + private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord, + final Set preKeyRecords, + final boolean announceAfter, + final boolean wipe, + final boolean firstAttempt) { + final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, getOwnDeviceId(), publishOptions); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing..."); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, IqPacket packet) { + final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet); + if (firstAttempt && preconditionNotMet) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); + final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); + } + + @Override + public void onPushFailed() { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); + } + }); + } else if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } else if (packet.getType() == IqPacket.TYPE.ERROR) { + if (preconditionNotMet) { + Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt"); + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.toString()); + } + pepBroken = true; + } + } + }); + } + + public void deleteOmemoIdentity() { + final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); + final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node); + mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null); + final Set ownDeviceIds = getOwnDeviceIds(); + publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); + } + + public List getCryptoTargets(Conversation conversation) { + final List jids; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + jids = new ArrayList<>(); + jids.add(conversation.getJid().asBareJid()); + } else { + jids = conversation.getMucOptions().getMembers(false); + } + return jids; + } + + public FingerprintStatus getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintStatus(fingerprint); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + + public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { + axolotlStore.setFingerprintStatus(fingerprint, status); + } + + private void verifySessionWithPEP(final XmppAxolotlSession session) { + Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + final SignalProtocolAddress address = session.getRemoteAddress(); + final IdentityKey identityKey = session.getIdentityKey(); + try { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.of(address.getName()), address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Pair verification = mXmppConnectionService.getIqParser().verification(packet); + if (verification != null) { + try { + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); + setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.of(address.getName()); + Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + //ignored + } + finishBuildingSessionsFromPEP(address); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } + } + } catch (Exception e) { + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "no verification found"); + } + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + }); + } catch (IllegalArgumentException e) { + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + } + } + + private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { + SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + Map own = fetchStatusMap.getAll(ownAddress.getName()); + Map remote = fetchStatusMap.getAll(address.getName()); + if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { + FetchStatus report = null; + if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { + report = FetchStatus.SUCCESS; + } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { + report = FetchStatus.SUCCESS_VERIFIED; + } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { + report = FetchStatus.SUCCESS_TRUSTED; + } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { + report = FetchStatus.ERROR; + } + mXmppConnectionService.keyStatusUpdated(report); + } + if (Config.REMOVE_BROKEN_DEVICES) { + Set ownDeviceIds = new HashSet<>(getOwnDeviceIds()); + boolean publish = false; + for (Map.Entry entry : own.entrySet()) { + int id = entry.getKey(); + if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) { + publish = true; + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error fetching own device with id " + id + ". removing from announcement"); + } + } + if (publish) { + publishOwnDeviceId(ownDeviceIds); + } + } + } + + public boolean hasEmptyDeviceList(Jid jid) { + return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty()); + } + + public void fetchDeviceIds(final Jid jid) { + fetchDeviceIds(jid, null); + } + + private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { + IqPacket packet; + synchronized (this.fetchDeviceIdsMap) { + List callbacks = this.fetchDeviceIdsMap.get(jid); + if (callbacks != null) { + if (callback != null) { + callbacks.add(callback); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); + packet = null; + } else { + callbacks = new ArrayList<>(); + if (callback != null) { + callbacks.add(callback); + } + this.fetchDeviceIdsMap.put(jid, callbacks); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); + packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); + } + } + if (packet != null) { + mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + fetchDeviceListStatus.put(jid, true); + Element item = mXmppConnectionService.getIqParser().getItem(response); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + registerDevices(jid, deviceIds); + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, deviceIds); + } + } + } else { + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + fetchDeviceListStatus.remove(jid); + } else { + fetchDeviceListStatus.put(jid, false); + } + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, null); + } + } + } + }); + } + } + + private void fetchDeviceIds(List jids, final OnMultipleDeviceIdFetched callback) { + final ArrayList unfinishedJids = new ArrayList<>(jids); + synchronized (unfinishedJids) { + for (Jid jid : unfinishedJids) { + fetchDeviceIds(jid, (j, deviceIds) -> { + synchronized (unfinishedJids) { + unfinishedJids.remove(j); + if (unfinishedJids.size() == 0 && callback != null) { + callback.fetched(); + } + } + }); + } + } + } + + private void buildSessionFromPEP(final SignalProtocolAddress address) { + buildSessionFromPEP(address, null); + } + + private void buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); + if (address.equals(getOwnAxolotlAddress())) { + throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); + } + + final Jid jid = Jid.of(address.getName()); + final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + } else if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + return; + } + + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); + sessions.put(address, session); + if (Config.X509_VERIFICATION) { + verifySessionWithPEP(session); //TODO; maybe inject callback in here too + } else { + FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); + FetchStatus fetchStatus; + if (status != null && status.isVerified()) { + fetchStatus = FetchStatus.SUCCESS_VERIFIED; + } else if (status != null && status.isTrusted()) { + fetchStatus = FetchStatus.SUCCESS_TRUSTED; + } else { + fetchStatus = FetchStatus.SUCCESS; + } + fetchStatusMap.put(address, fetchStatus); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildSuccessful(); + } + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } + if (callback != null) { + callback.onSessionBuildFailed(); + } + } + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Element error = packet.findChild("error"); + boolean itemNotFound = error != null && error.hasChild("item-not-found"); + Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); + finishBuildingSessionsFromPEP(address); + if (oneOfOurs && itemNotFound && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } + if (callback != null) { + callback.onSessionBuildFailed(); + } + } + }); + } + + private void removeFromDeviceAnnouncement(Integer id) { + HashSet temp = new HashSet<>(getOwnDeviceIds()); + if (temp.remove(id)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " remove own device id " + id + " from announcement. devices left:" + temp); + publishOwnDeviceId(temp); + } + } + + public Set findDevicesWithoutSession(final Conversation conversation) { + Set addresses = new HashSet<>(); + for (Jid jid : getCryptoTargets(conversation)) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); + final Set ids = deviceIds.get(jid); + if (ids != null && !ids.isEmpty()) { + for (Integer foreignId : ids) { + SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + } + } + } + } + } else { + mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); + } + } + Set ownIds = this.deviceIds.get(account.getJid().asBareJid()); + for (Integer ownId : (ownIds != null ? ownIds : new HashSet())) { + SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); + if (sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if (identityKey != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().asBareJid() + ":" + ownId); + if (fetchStatusMap.get(address) != FetchStatus.ERROR) { + addresses.add(address); + } else { + Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + } + } + } + } + + return addresses; + } + + public boolean createSessionsIfNeeded(final Conversation conversation) { + final List jidsWithEmptyDeviceList = getCryptoTargets(conversation); + for (Iterator iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext(); ) { + final Jid jid = iterator.next(); + if (!hasEmptyDeviceList(jid)) { + iterator.remove(); + } + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); + if (jidsWithEmptyDeviceList.size() > 0) { + fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); + return true; + } else { + return createSessionsIfNeededActual(conversation); + } + } + + private boolean createSessionsIfNeededActual(final Conversation conversation) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set addresses = findDevicesWithoutSession(conversation); + for (SignalProtocolAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if (status == null || status == FetchStatus.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(address); + newSessions = true; + } else if (status == FetchStatus.PENDING) { + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + } + } + + return newSessions; + } + + public boolean trustedSessionVerified(final Conversation conversation) { + final Set sessions = new HashSet<>(); + sessions.addAll(findSessionsForConversation(conversation)); + sessions.addAll(findOwnSessions()); + boolean verified = false; + for (XmppAxolotlSession session : sessions) { + if (session.getTrust().isTrustedAndActive()) { + if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { + verified = true; + } else { + return false; + } + } + } + return verified; + } + + public boolean hasPendingKeyFetches(List jids) { + SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) { + return true; + } + synchronized (this.fetchDeviceIdsMap) { + for (Jid jid : jids) { + SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.asBareJid().toString(), 0); + if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) { + return true; + } + } + } + return false; + } + + @Nullable + private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation c) { + Set remoteSessions = findSessionsForConversation(c); + final boolean acceptEmpty = (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) || c.getContact().isSelf(); + Collection ownSessions = findOwnSessions(); + if (remoteSessions.isEmpty() && !acceptEmpty) { + return false; + } + for (XmppAxolotlSession session : remoteSessions) { + axolotlMessage.addDevice(session); + } + for (XmppAxolotlSession session : ownSessions) { + axolotlMessage.addDevice(session); + } + + return true; + } + + //this is being used for private muc messages only + private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { + if (jid == null) { + return false; + } + HashSet sessions = new HashSet<>(); + sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); + if (sessions.isEmpty()) { + return false; + } + sessions.addAll(findOwnSessions()); + for (XmppAxolotlSession session : sessions) { + axolotlMessage.addDevice(session); + } + return true; + } + + @Nullable + public XmppAxolotlMessage encrypt(Message message) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } + try { + axolotlMessage.encrypt(content); + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + return null; + } + + final boolean success; + if (message.isPrivateMessage()) { + success = buildHeader(axolotlMessage, message.getTrueCounterpart()); + } else { + success = buildHeader(axolotlMessage, (Conversation) message.getConversation()); + } + return success ? axolotlMessage : null; + } + + public void preparePayloadMessage(final Message message, final boolean delay) { + executor.execute(new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); + } + + public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { + executor.execute(new Runnable() { + @Override + public void run() { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + if (buildHeader(axolotlMessage, conversation)) { + onMessageCreatedCallback.run(axolotlMessage); + } else { + onMessageCreatedCallback.run(null); + } + } + }); + } + + public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { + XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); + if (axolotlMessage != null) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); + } + return axolotlMessage; + } + + private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + return (identityKey != null) + ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) + : null; + } + + private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { + SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toString(), message.getSenderDeviceId()); + return getReceivingSession(senderAddress); + + } + + private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + session = recreateUncachedSession(senderAddress); + if (session == null) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } + } + return session; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + + XmppAxolotlSession session = getReceivingSession(message); + int ownDeviceId = getOwnDeviceId(); + try { + plaintextMessage = message.decrypt(session, ownDeviceId); + Integer preKeyId = session.getPreKeyIdAndReset(); + if (preKeyId != null) { + postPreKeyMessageHandling(session, postponePreKeyMessageHandling); + } + } catch (NotEncryptedForThisDeviceException e) { + if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); + } else { + throw e; + } + } catch (final BrokenSessionException e) { + throw e; + } catch (final OutdatedSenderException e) { + Log.e(Config.LOGTAG,account.getJid().asBareJid()+": "+e.getMessage()); + throw e; + } catch (CryptoFailedException e) { + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e); + } + + if (session.isFresh() && plaintextMessage != null) { + putFreshSession(session); + } + + return plaintextMessage; + } + + public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { + Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": broken session with " + e.getSignalProtocolAddress().toString() + " detected", e); + if (postpone) { + postponedHealing.add(e.getSignalProtocolAddress()); + } else { + notifyRequiresHealing(e.getSignalProtocolAddress()); + } + } + + private void notifyRequiresHealing(final SignalProtocolAddress signalProtocolAddress) { + if (healingAttempts.add(signalProtocolAddress)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); + buildSessionFromPEP(signalProtocolAddress, new OnSessionBuildFromPep() { + @Override + public void onSessionBuildSuccessful() { + Log.d(Config.LOGTAG, "successfully build new session from pep after detecting broken session"); + completeSession(getReceivingSession(signalProtocolAddress)); + } + + @Override + public void onSessionBuildFailed() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to build new session from pep after detecting broken session"); + } + }); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt to heal " + signalProtocolAddress + " again"); + } + } + + private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { + if (postpone) { + postponedSessions.add(session); + } else { + if (axolotlStore.flushPreKeys()) { + publishBundlesIfNeeded(false, false); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); + } + if (trustedOrPreviouslyResponded(session)) { + completeSession(session); + } + } + } + + public void processPostponed() { + if (postponedSessions.size() > 0) { + if (axolotlStore.flushPreKeys()) { + publishBundlesIfNeeded(false, false); + } + } + final Iterator iterator = postponedSessions.iterator(); + while (iterator.hasNext()) { + final XmppAxolotlSession session = iterator.next(); + if (trustedOrPreviouslyResponded(session)) { + completeSession(session); + } + iterator.remove(); + } + final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); + while (postponedHealingAttemptsIterator.hasNext()) { + notifyRequiresHealing(postponedHealingAttemptsIterator.next()); + postponedHealingAttemptsIterator.remove(); + } + } + + private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) { + try { + return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName())); + } catch (IllegalArgumentException e) { + return false; + } + } + + public boolean trustedOrPreviouslyResponded(Jid jid) { + final Contact contact = account.getRoster().getContact(jid); + if (contact.showInRoster() || contact.isSelf()) { + return true; + } + final Conversation conversation = mXmppConnectionService.find(account, jid); + return conversation != null && conversation.sentMessagesCount() > 0; + } + + private void completeSession(XmppAxolotlSession session) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + axolotlMessage.addDevice(session, true); + try { + final Jid jid = Jid.of(session.getRemoteAddress().getName()); + MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + mXmppConnectionService.sendMessagePacket(account, packet); + } catch (IllegalArgumentException e) { + throw new Error("Remote addresses are created from jid and should convert back to jid", e); + } + } + + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { + final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; + final XmppAxolotlSession session = getReceivingSession(message); + try { + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + Integer preKeyId = session.getPreKeyIdAndReset(); + if (preKeyId != null) { + postPreKeyMessageHandling(session, postponePreKeyMessageHandling); + } + } catch (CryptoFailedException e) { + Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage()); + return null; + } + + if (session.isFresh() && keyTransportMessage != null) { + putFreshSession(session); + } + + return keyTransportMessage; + } + + private void putFreshSession(XmppAxolotlSession session) { + sessions.put(session); + if (Config.X509_VERIFICATION) { + if (session.getIdentityKey() != null) { + verifySessionWithPEP(session); + } else { + Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); + } + } + } + + public enum FetchStatus { + PENDING, + SUCCESS, + SUCCESS_VERIFIED, + TIMEOUT, + SUCCESS_TRUSTED, + ERROR + } + + public interface OnDeviceIdsFetched { + void fetched(Jid jid, Set deviceIds); + } + + + public interface OnMultipleDeviceIdFetched { + void fetched(); + } + + interface OnSessionBuildFromPep { + void onSessionBuildSuccessful(); + + void onSessionBuildFailed(); + } + + private static class AxolotlAddressMap { + protected final Object MAP_LOCK = new Object(); + protected Map> map; + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(SignalProtocolAddress address, T value) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(SignalProtocolAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(String name) { + synchronized (MAP_LOCK) { + Map devices = map.get(name); + if (devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(SignalProtocolAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + public void clear() { + map.clear(); + } + + } + + private static class SessionMap extends AxolotlAddressMap { + private final XmppConnectionService xmppConnectionService; + private final Account account; + + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + super(); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); + } + + public Set findCounterpartsForSourceId(Integer sid) { + Set candidates = new HashSet<>(); + synchronized (MAP_LOCK) { + for (Map.Entry> entry : map.entrySet()) { + String key = entry.getKey(); + if (entry.getValue().containsKey(sid)) { + candidates.add(Jid.of(key)); + } + } + } + return candidates; + } + + private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { + for (Integer deviceId : deviceIds) { + SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); + IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + if (Config.X509_VERIFICATION) { + X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); + if (certificate != null) { + Bundle information = CryptoHelper.extractCertificateInformation(certificate); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.of(bareJid); + Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + //ignored + } + } + } + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + } + } + + private void fillMap(SQLiteAxolotlStore store) { + List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); + putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); + for (String address : store.getKnownAddresses()) { + deviceIds = store.getSubDeviceSessions(address); + putDevicesForJid(address, deviceIds, store); + } + } + + @Override + public void put(SignalProtocolAddress address, XmppAxolotlSession value) { + super.put(address, value); + value.setNotFresh(); + } + + public void put(XmppAxolotlSession session) { + this.put(session.getRemoteAddress(), session); + } + } + + private static class FetchStatusMap extends AxolotlAddressMap { + + public void clearErrorFor(Jid jid) { + synchronized (MAP_LOCK) { + Map devices = this.map.get(jid.asBareJid().toString()); + if (devices == null) { + return; + } + for (Map.Entry entry : devices.entrySet()) { + if (entry.getValue() == FetchStatus.ERROR) { + Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); + entry.setValue(FetchStatus.TIMEOUT); + } + } + } + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java new file mode 100644 index 000000000..b684c678f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class OutdatedSenderException extends CryptoFailedException { + + public OutdatedSenderException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index f411fe4d2..13082deac 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -27,295 +27,293 @@ import eu.siacs.conversations.xml.Element; import rocks.xmpp.addr.Jid; public class XmppAxolotlMessage { - public static final String CONTAINERTAG = "encrypted"; - private static final String HEADER = "header"; - private static final String SOURCEID = "sid"; - private static final String KEYTAG = "key"; - private static final String REMOTEID = "rid"; - private static final String IVTAG = "iv"; - private static final String PAYLOAD = "payload"; + public static final String CONTAINERTAG = "encrypted"; + private static final String HEADER = "header"; + private static final String SOURCEID = "sid"; + private static final String KEYTAG = "key"; + private static final String REMOTEID = "rid"; + private static final String IVTAG = "iv"; + private static final String PAYLOAD = "payload"; - private static final String KEYTYPE = "AES"; - private static final String CIPHERMODE = "AES/GCM/NoPadding"; - private static final String PROVIDER = "BC"; + private static final String KEYTYPE = "AES"; + private static final String CIPHERMODE = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; + private final List keys; + private final Jid from; + private final int sourceDeviceId; + private byte[] innerKey; + private byte[] ciphertext = null; + private byte[] authtagPlusInnerKey = null; + private byte[] iv = null; - private byte[] innerKey; - private byte[] ciphertext = null; - private byte[] authtagPlusInnerKey = null; - private byte[] iv = null; - private final List keys; - private final Jid from; - private final int sourceDeviceId; + private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { + this.from = from; + Element header = axolotlMessage.findChild(HEADER); + try { + this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid source id"); + } + List keyElements = header.getChildren(); + this.keys = new ArrayList<>(); + for (Element keyElement : keyElements) { + switch (keyElement.getName()) { + case KEYTAG: + try { + Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); + byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); + boolean isPreKey = keyElement.getAttributeAsBoolean("prekey"); + this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid remote id"); + } + break; + case IVTAG: + if (this.iv != null) { + throw new IllegalArgumentException("Duplicate iv entry"); + } + iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); + break; + default: + Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString()); + break; + } + } + final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX); + if (payloadElement != null) { + ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT); + } + } - public static class XmppAxolotlPlaintextMessage { - private final String plaintext; - private final String fingerprint; + XmppAxolotlMessage(Jid from, int sourceDeviceId) { + this.from = from; + this.sourceDeviceId = sourceDeviceId; + this.keys = new ArrayList<>(); + this.iv = generateIv(); + this.innerKey = generateKey(); + } - XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { - this.plaintext = plaintext; - this.fingerprint = fingerprint; - } + public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException { + final Element header = axolotlMessage.findChild(HEADER); + if (header == null) { + throw new IllegalArgumentException("No header found"); + } + try { + return Integer.parseInt(header.getAttribute(SOURCEID)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid source id"); + } + } - public String getPlaintext() { - return plaintext; - } + public static XmppAxolotlMessage fromElement(Element element, Jid from) { + return new XmppAxolotlMessage(element, from); + } + + private static byte[] generateKey() { + try { + KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); + generator.init(128); + return generator.generateKey().getEncoded(); + } catch (NoSuchAlgorithmException e) { + Log.e(Config.LOGTAG, e.getMessage()); + return null; + } + } + + private static byte[] generateIv() { + final SecureRandom random = new SecureRandom(); + byte[] iv = new byte[Config.TWELVE_BYTE_IV ? 12 : 16]; + random.nextBytes(iv); + return iv; + } + + private static byte[] getPaddedBytes(String plaintext) { + int plainLength = plaintext.getBytes().length; + int pad = Math.max(64, (plainLength / 32 + 1) * 32) - plainLength; + SecureRandom random = new SecureRandom(); + int left = random.nextInt(pad); + int right = pad - left; + StringBuilder builder = new StringBuilder(plaintext); + for (int i = 0; i < left; ++i) { + builder.insert(0, random.nextBoolean() ? "\t" : " "); + } + for (int i = 0; i < right; ++i) { + builder.append(random.nextBoolean() ? "\t" : " "); + } + return builder.toString().getBytes(); + } + + public boolean hasPayload() { + return ciphertext != null; + } + + void encrypt(String plaintext) throws CryptoFailedException { + try { + SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); + this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes()); + if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) { + this.authtagPlusInnerKey = new byte[16 + 16]; + byte[] ciphertext = new byte[this.ciphertext.length - 16]; + System.arraycopy(this.ciphertext, 0, ciphertext, 0, ciphertext.length); + System.arraycopy(this.ciphertext, ciphertext.length, authtagPlusInnerKey, 16, 16); + System.arraycopy(this.innerKey, 0, authtagPlusInnerKey, 0, this.innerKey.length); + this.ciphertext = ciphertext; + } + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException + | InvalidAlgorithmParameterException e) { + throw new CryptoFailedException(e); + } + } + + public Jid getFrom() { + return this.from; + } + + int getSenderDeviceId() { + return sourceDeviceId; + } + + void addDevice(XmppAxolotlSession session) { + addDevice(session, false); + } + + void addDevice(XmppAxolotlSession session, boolean ignoreSessionTrust) { + XmppAxolotlSession.AxolotlKey key; + if (authtagPlusInnerKey != null) { + key = session.processSending(authtagPlusInnerKey, ignoreSessionTrust); + } else { + key = session.processSending(innerKey, ignoreSessionTrust); + } + if (key != null) { + keys.add(key); + } + } + + public byte[] getInnerKey() { + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toElement() { + Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); + Element headerElement = encryptionElement.addChild(HEADER); + headerElement.setAttribute(SOURCEID, sourceDeviceId); + for (XmppAxolotlSession.AxolotlKey key : keys) { + Element keyElement = new Element(KEYTAG); + keyElement.setAttribute(REMOTEID, key.deviceId); + if (key.prekey) { + keyElement.setAttribute("prekey", "true"); + } + keyElement.setContent(Base64.encodeToString(key.key, Base64.NO_WRAP)); + headerElement.addChild(keyElement); + } + headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); + if (ciphertext != null) { + Element payload = encryptionElement.addChild(PAYLOAD); + payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); + } + return encryptionElement; + } + + private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + ArrayList possibleKeys = new ArrayList<>(); + for (XmppAxolotlSession.AxolotlKey key : keys) { + if (key.deviceId == sourceDeviceId) { + possibleKeys.add(key); + } + } + if (possibleKeys.size() == 0) { + throw new NotEncryptedForThisDeviceException(); + } + return session.processReceiving(possibleKeys); + } + + XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV()); + } + + public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + XmppAxolotlPlaintextMessage plaintextMessage = null; + byte[] key = unpackKey(session, sourceDeviceId); + if (key != null) { + try { + if (key.length < 32) { + throw new OutdatedSenderException("Key did not contain auth tag. Sender needs to update their OMEMO client"); + } + final int authTagLength = key.length - 16; + byte[] newCipherText = new byte[key.length - 16 + ciphertext.length]; + byte[] newKey = new byte[16]; + System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length); + System.arraycopy(key, 16, newCipherText, ciphertext.length, authTagLength); + System.arraycopy(key, 0, newKey, 0, newKey.length); + ciphertext = newCipherText; + key = newKey; + + final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint()); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException | NoSuchProviderException e) { + throw new CryptoFailedException(e); + } + } + return plaintextMessage; + } + + public static class XmppAxolotlPlaintextMessage { + private final String plaintext; + private final String fingerprint; + + XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { + this.plaintext = plaintext; + this.fingerprint = fingerprint; + } + + public String getPlaintext() { + return plaintext; + } - public String getFingerprint() { - return fingerprint; - } - } + public String getFingerprint() { + return fingerprint; + } + } - public static class XmppAxolotlKeyTransportMessage { - private final String fingerprint; - private final byte[] key; - private final byte[] iv; + public static class XmppAxolotlKeyTransportMessage { + private final String fingerprint; + private final byte[] key; + private final byte[] iv; - XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { - this.fingerprint = fingerprint; - this.key = key; - this.iv = iv; - } + XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { + this.fingerprint = fingerprint; + this.key = key; + this.iv = iv; + } - public String getFingerprint() { - return fingerprint; - } + public String getFingerprint() { + return fingerprint; + } - public byte[] getKey() { - return key; - } + public byte[] getKey() { + return key; + } - public byte[] getIv() { - return iv; - } - } - - public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException { - final Element header = axolotlMessage.findChild(HEADER); - if (header == null) { - throw new IllegalArgumentException("No header found"); - } - try { - return Integer.parseInt(header.getAttribute(SOURCEID)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid source id"); - } - } - - private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { - this.from = from; - Element header = axolotlMessage.findChild(HEADER); - try { - this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid source id"); - } - List keyElements = header.getChildren(); - this.keys = new ArrayList<>(); - for (Element keyElement : keyElements) { - switch (keyElement.getName()) { - case KEYTAG: - try { - Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); - byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); - boolean isPreKey =keyElement.getAttributeAsBoolean("prekey"); - this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key,isPreKey)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid remote id"); - } - break; - case IVTAG: - if (this.iv != null) { - throw new IllegalArgumentException("Duplicate iv entry"); - } - iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); - break; - default: - Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString()); - break; - } - } - final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX); - if (payloadElement != null) { - ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT); - } - } - - XmppAxolotlMessage(Jid from, int sourceDeviceId) { - this.from = from; - this.sourceDeviceId = sourceDeviceId; - this.keys = new ArrayList<>(); - this.iv = generateIv(); - this.innerKey = generateKey(); - } - - public static XmppAxolotlMessage fromElement(Element element, Jid from) { - return new XmppAxolotlMessage(element, from); - } - - private static byte[] generateKey() { - try { - KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); - generator.init(128); - return generator.generateKey().getEncoded(); - } catch (NoSuchAlgorithmException e) { - Log.e(Config.LOGTAG, e.getMessage()); - return null; - } - } - - private static byte[] generateIv() { - final SecureRandom random = new SecureRandom(); - byte[] iv = new byte[Config.TWELVE_BYTE_IV ? 12 : 16]; - random.nextBytes(iv); - return iv; - } - - public boolean hasPayload() { - return ciphertext != null; - } - - void encrypt(String plaintext) throws CryptoFailedException { - try { - SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes()); - if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) { - this.authtagPlusInnerKey = new byte[16+16]; - byte[] ciphertext = new byte[this.ciphertext.length - 16]; - System.arraycopy(this.ciphertext,0,ciphertext,0,ciphertext.length); - System.arraycopy(this.ciphertext,ciphertext.length,authtagPlusInnerKey,16,16); - System.arraycopy(this.innerKey,0,authtagPlusInnerKey,0,this.innerKey.length); - this.ciphertext = ciphertext; - } - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException - | InvalidAlgorithmParameterException e) { - throw new CryptoFailedException(e); - } - } - - private static byte[] getPaddedBytes(String plaintext) { - int plainLength = plaintext.getBytes().length; - int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength; - SecureRandom random = new SecureRandom(); - int left = random.nextInt(pad); - int right = pad - left; - StringBuilder builder = new StringBuilder(plaintext); - for(int i = 0; i < left; ++i) { - builder.insert(0,random.nextBoolean() ? "\t" : " "); - } - for(int i = 0; i < right; ++i) { - builder.append(random.nextBoolean() ? "\t" : " "); - } - return builder.toString().getBytes(); - } - - public Jid getFrom() { - return this.from; - } - - int getSenderDeviceId() { - return sourceDeviceId; - } - - void addDevice(XmppAxolotlSession session) { - addDevice(session, false); - } - - void addDevice(XmppAxolotlSession session, boolean ignoreSessionTrust) { - XmppAxolotlSession.AxolotlKey key; - if (authtagPlusInnerKey != null) { - key = session.processSending(authtagPlusInnerKey, ignoreSessionTrust); - } else { - key = session.processSending(innerKey, ignoreSessionTrust); - } - if (key != null) { - keys.add(key); - } - } - - public byte[] getInnerKey() { - return innerKey; - } - - public byte[] getIV() { - return this.iv; - } - - public Element toElement() { - Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); - Element headerElement = encryptionElement.addChild(HEADER); - headerElement.setAttribute(SOURCEID, sourceDeviceId); - for(XmppAxolotlSession.AxolotlKey key : keys) { - Element keyElement = new Element(KEYTAG); - keyElement.setAttribute(REMOTEID, key.deviceId); - if (key.prekey) { - keyElement.setAttribute("prekey","true"); - } - keyElement.setContent(Base64.encodeToString(key.key, Base64.NO_WRAP)); - headerElement.addChild(keyElement); - } - headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); - if (ciphertext != null) { - Element payload = encryptionElement.addChild(PAYLOAD); - payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); - } - return encryptionElement; - } - - private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - ArrayList possibleKeys = new ArrayList<>(); - for(XmppAxolotlSession.AxolotlKey key : keys) { - if (key.deviceId == sourceDeviceId) { - possibleKeys.add(key); - } - } - if (possibleKeys.size() == 0) { - throw new NotEncryptedForThisDeviceException(); - } - return session.processReceiving(possibleKeys); - } - - XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV()); - } - - public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - XmppAxolotlPlaintextMessage plaintextMessage = null; - byte[] key = unpackKey(session, sourceDeviceId); - if (key != null) { - try { - //TODO remove support for *not* having auth tag in key - if (key.length >= 32) { - int authtaglength = key.length - 16; - Log.d(Config.LOGTAG,"found auth tag as part of omemo key"); - byte[] newCipherText = new byte[key.length - 16 + ciphertext.length]; - byte[] newKey = new byte[16]; - System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length); - System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength); - System.arraycopy(key,0,newKey,0,newKey.length); - ciphertext = newCipherText; - key = newKey; - } - - Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - - String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint()); - - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException - | BadPaddingException | NoSuchProviderException e) { - throw new CryptoFailedException(e); - } - } - return plaintextMessage; - } + public byte[] getIv() { + return iv; + } + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 2187617e3..ec90e294f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -76,8 +76,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private Jid nextCounterpart; private transient MucOptions mucOptions = null; private boolean messagesLeftOnServer = true; - private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; - private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; + private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; private String mFirstMamReference = null; public Conversation(final String name, final Account account, final Jid contactJid, diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 116906fef..74a3c538d 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -94,7 +94,7 @@ public class MucOptions { public void resetChatState() { synchronized (users) { for (User user : users) { - user.chatState = Config.DEFAULT_CHATSTATE; + user.chatState = Config.DEFAULT_CHAT_STATE; } } } @@ -746,7 +746,7 @@ public class MucOptions { private long pgpKeyId = 0; private Avatar avatar; private MucOptions options; - private ChatState chatState = Config.DEFAULT_CHATSTATE; + private ChatState chatState = Config.DEFAULT_CHAT_STATE; public User(MucOptions options, Jid fullJid) { this.options = options; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 84bee6408..ed7f6ad96 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -19,6 +19,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.BrokenSessionException; import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException; +import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; @@ -140,6 +141,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } catch (NotEncryptedForThisDeviceException e) { return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status); + } catch (OutdatedSenderException e) { + return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); } if (plaintextMessage != null) { Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index df2aaec19..828721801 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -34,7 +34,6 @@ import android.provider.ContactsContract; import android.security.KeyChain; import android.support.annotation.BoolRes; import android.support.annotation.IntegerRes; -import android.support.annotation.NonNull; import android.support.v4.app.RemoteInput; import android.support.v4.content.ContextCompat; import android.text.TextUtils; @@ -1515,7 +1514,7 @@ public class XmppConnectionService extends Service { if (delay) { mMessageGenerator.addDelay(packet, message.getTimeSent()); } - if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { if (this.sendChatStates()) { packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); } @@ -2514,7 +2513,7 @@ public class XmppConnectionService extends Service { if (conversation.getMode() == Conversation.MODE_MULTI) { conversation.getMucOptions().resetChatState(); } else { - conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE); + conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE); } } for (Account account : getAccounts()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 724855ee7..8e2e228d4 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1721,7 +1721,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } public void privateMessageWith(final Jid counterpart) { - if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { activity.xmppConnectionService.sendChatState(conversation); } this.binding.textinput.setText(""); @@ -1859,7 +1859,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void updateChatState(final Conversation conversation, final String msg) { - ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED; + ChatState state = msg.length() == 0 ? Config.DEFAULT_CHAT_STATE : ChatState.PAUSED; Account.State status = conversation.getAccount().getStatus(); if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) { activity.xmppConnectionService.sendChatState(conversation); @@ -2619,7 +2619,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return; } Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { service.sendChatState(conversation); } if (storeNextMessage()) { diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml index 18b62de73..52c38ffd1 100644 --- a/src/main/res/values/about.xml +++ b/src/main/res/values/about.xml @@ -31,7 +31,7 @@ Conversations • the very last word in instant messaging. - \n\nCopyright © 2014-2019 Daniel Gultsch + \n\nCopyright © 2014-2020 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or