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 305fd4010..40d94310f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -55,6 +55,7 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.PublishOptions; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @@ -77,11 +78,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private final Map> deviceIds; private final Map messageCache; private final FetchStatusMap fetchStatusMap; - private final HashMap> fetchDeviceIdsMap = new HashMap<>(); + private final HashMap> fetchDeviceIdsMap = new HashMap<>(); private final SerialSingleThreadExecutor executor; 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 AtomicBoolean changeAccessMode = new AtomicBoolean(false); @@ -92,12 +94,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { && account.getXmppConnection().getFeatures().pep()) { publishBundlesIfNeeded(true, false); } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": skipping OMEMO initialization"); } } public boolean fetchMapHasErrors(List jids) { - for(Jid jid : jids) { + for (Jid jid : jids) { if (deviceIds.get(jid) != null) { for (Integer foreignId : this.deviceIds.get(jid)) { SignalProtocolAddress address = new SignalProtocolAddress(jid.toPreppedString(), foreignId); @@ -119,7 +121,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public boolean hasVerifiedKeys(String name) { - for(XmppAxolotlSession session : this.sessions.getAll(name).values()) { + for (XmppAxolotlSession session : this.sessions.getAll(name).values()) { if (session.getTrust().isVerified()) { return true; } @@ -194,14 +196,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { for (Integer deviceId : deviceIds) { SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); - if(Config.X509_VERIFICATION) { + 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.fromString(bareJid); - Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); account.getRoster().getContact(jid).setCommonName(cn); } catch (final InvalidJidException ignored) { //ignored @@ -215,7 +217,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void fillMap(SQLiteAxolotlStore store) { List deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString()); putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store); - for (String address : store.getKnownAddresses()) { + for (String address : store.getKnownAddresses()) { deviceIds = store.getSubDeviceSessions(address); putDevicesForJid(address, deviceIds, store); } @@ -249,9 +251,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (devices == null) { return; } - for(Map.Entry entry : devices.entrySet()) { + for (Map.Entry entry : devices.entrySet()) { if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")"); + Log.d(Config.LOGTAG, "resetting error for " + jid.toBareJid() + "(" + entry.getKey() + ")"); entry.setValue(FetchStatus.TIMEOUT); } } @@ -294,7 +296,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Set getKeysWithTrust(FingerprintStatus status, List jids) { Set keys = new HashSet<>(); - for(Jid jid : jids) { + for (Jid jid : jids) { keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status)); } return keys; @@ -305,7 +307,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public boolean anyTargetHasNoTrustedKeys(List jids) { - for(Jid jid : jids) { + for (Jid jid : jids) { if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) { return true; } @@ -325,7 +327,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } - public Collection findSessionsForContact(Contact contact) { SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); @@ -335,7 +336,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private Set findSessionsForConversation(Conversation conversation) { HashSet sessions = new HashSet<>(); - for(Jid jid : conversation.getAcceptedCryptoTargets()) { + for (Jid jid : conversation.getAcceptedCryptoTargets()) { sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); } return sessions; @@ -368,13 +369,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void destroy() { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": destroying old axolotl service. no longer in use"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": destroying old axolotl service. no longer in use"); mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); } public AxolotlService makeNew() { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": make new axolotl service"); - return new AxolotlService(this.account,this.mXmppConnectionService); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": make new axolotl service"); + return new AxolotlService(this.account, this.mXmppConnectionService); } public int getOwnDeviceId() { @@ -382,7 +383,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public SignalProtocolAddress getOwnAxolotlAddress() { - return new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId()); + return new SignalProtocolAddress(account.getJid().toBareJid().toPreppedString(), getOwnDeviceId()); } public Set getOwnDeviceIds() { @@ -420,7 +421,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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()); + Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint()); session.setTrust(session.getTrust().toActive()); } } @@ -462,7 +463,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void distrustFingerprint(final String fingerprint) { final String fp = fingerprint.replaceAll("\\s", ""); final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp); - axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted()); + axolotlStore.setFingerprintStatus(fp, fingerprintStatus.toUntrusted()); } public void publishOwnDeviceIdIfNeeded() { @@ -479,8 +480,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else { Element item = mXmppConnectionService.getIqParser().getItem(packet); Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds); - registerDevices(account.getJid().toBareJid(),deviceIds); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": retrieved own device list: " + deviceIds); + registerDevices(account.getJid().toBareJid(), deviceIds); } } }); @@ -488,18 +489,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private Set getExpiredDevices() { Set devices = new HashSet<>(); - for(XmppAxolotlSession session : findOwnSessions()) { + 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)); + 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().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago"); } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago"); } } } @@ -527,7 +528,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void publishDeviceIdsAndRefineAccessModel(Set ids) { - publishDeviceIdsAndRefineAccessModel(ids,true); + publishDeviceIdsAndRefineAccessModel(ids, true); } private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { @@ -537,8 +538,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; - if (firstAttempt && error != null && error.hasChild("precondition-not-met",Namespace.PUBSUB_ERROR)) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for device list. pushing node configuration"); + if (firstAttempt && error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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() { @@ -551,9 +552,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } }); } else { - if (AxolotlService.this.changeAccessMode.compareAndSet(true,false)) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": done changing access mode"); - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE,false); + if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": done changing access mode"); + account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); mXmppConnectionService.databaseBackend.updateAccount(account); } if (packet.getType() == IqPacket.TYPE.ERROR) { @@ -566,39 +567,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + 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.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()); + 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(); + 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"); + 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"); + Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); } }); } }); - } catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -612,7 +613,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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)) { + if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server doesn’t support publish-options. setting for later access mode change"); mXmppConnectionService.databaseBackend.updateAccount(account); } @@ -728,38 +729,38 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - publishDeviceBundle(signedPreKeyRecord,preKeyRecords,announceAfter,wipe,true); + 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 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); + 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) { Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; if (firstAttempt && error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR)) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": precondition wasn't met for bundle. pushing node configuration"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": 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); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); } @Override public void onPushFailed() { - publishDeviceBundle(signedPreKeyRecord,preKeyRecords, announceAfter, wipe, false); + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); } }); } else if (packet.getType() == IqPacket.TYPE.RESULT) { @@ -790,16 +791,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().nonanonymous() && conversation.getMucOptions().membersOnly()); } - public Pair isConversationAxolotlCapableDetailed(Conversation conversation) { + public Pair isConversationAxolotlCapableDetailed(Conversation conversation) { if (conversation.getMode() == Conversation.MODE_SINGLE || (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) { final List jids = getCryptoTargets(conversation); - for(Jid jid : jids) { + for (Jid jid : jids) { if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) { if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) { - return new Pair<>(AxolotlCapability.MISSING_KEYS,jid); + return new Pair<>(AxolotlCapability.MISSING_KEYS, jid); } else { - return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid); + return new Pair<>(AxolotlCapability.MISSING_PRESENCE, jid); } } } @@ -845,7 +846,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Pair verification = mXmppConnectionService.getIqParser().verification(packet); + Pair verification = mXmppConnectionService.getIqParser().verification(packet); if (verification != null) { try { Signature verifier = Signature.getInstance("sha256WithRSA"); @@ -855,7 +856,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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); + 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); @@ -863,7 +864,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { try { final String cn = information.getString("subject_cn"); final Jid jid = Jid.fromString(address.getName()); - Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); account.getRoster().getContact(jid).setCommonName(cn); } catch (final InvalidJidException ignored) { //ignored @@ -871,14 +872,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { finishBuildingSessionsFromPEP(address); return; } catch (Exception e) { - Log.d(Config.LOGTAG,"could not verify certificate"); + 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"); + Log.d(Config.LOGTAG, "no verification found"); } fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); @@ -938,7 +939,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void fetchDeviceIds(final Jid jid) { - fetchDeviceIds(jid,null); + fetchDeviceIds(jid, null); } public void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { @@ -948,14 +949,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callbacks.add(callback); } - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for "+jid+" already running. adding callback"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); } else { callbacks = new ArrayList<>(); if (callback != null) { callbacks.add(callback); } - this.fetchDeviceIdsMap.put(jid,callbacks); - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching device ids for " + jid); + this.fetchDeviceIdsMap.put(jid, callbacks); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching device ids for " + jid); IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override @@ -967,14 +968,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); registerDevices(jid, deviceIds); if (callbacks != null) { - for(OnDeviceIdsFetched callback : callbacks) { + for (OnDeviceIdsFetched callback : callbacks) { callback.fetched(jid, deviceIds); } } } else { Log.d(Config.LOGTAG, packet.toString()); if (callbacks != null) { - for(OnDeviceIdsFetched callback : callbacks) { + for (OnDeviceIdsFetched callback : callbacks) { callback.fetched(jid, null); } } @@ -1086,7 +1087,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Set findDevicesWithoutSession(final Conversation conversation) { Set addresses = new HashSet<>(); - for(Jid jid : getCryptoTargets(conversation)) { + for (Jid jid : getCryptoTargets(conversation)) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); if (deviceIds.get(jid) != null) { for (Integer foreignId : this.deviceIds.get(jid)) { @@ -1126,7 +1127,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (fetchStatusMap.get(address) != FetchStatus.ERROR) { addresses.add(address); } else { - Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken"); + Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); } } } @@ -1138,13 +1139,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public boolean createSessionsIfNeeded(final Conversation conversation) { final List jidsWithEmptyDeviceList = getCryptoTargets(conversation); - for(Iterator iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext();) { + for (Iterator iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext(); ) { final Jid jid = iterator.next(); if (!hasEmptyDeviceList(jid)) { iterator.remove(); } } - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": createSessionsIfNeeded() - jids with empty device list: "+jidsWithEmptyDeviceList); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); if (jidsWithEmptyDeviceList.size() > 0) { fetchDeviceIds(jidsWithEmptyDeviceList, new OnMultipleDeviceIdFetched() { @Override @@ -1183,7 +1184,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Set sessions = findSessionsForConversation(conversation); sessions.addAll(findOwnSessions()); boolean verified = false; - for(XmppAxolotlSession session : sessions) { + for (XmppAxolotlSession session : sessions) { if (session.getTrust().isTrustedAndActive()) { if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { verified = true; @@ -1243,7 +1244,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); return null; } - if (!buildHeader(axolotlMessage,message.getConversation())) { + //TODO: fix this for MUC PMs - Don't encrypt to all participants + if (!buildHeader(axolotlMessage, message.getConversation())) { return null; } @@ -1272,7 +1274,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Override public void run() { final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage,conversation)) { + if (buildHeader(axolotlMessage, conversation)) { onMessageCreatedCallback.run(axolotlMessage); } else { onMessageCreatedCallback.run(null); @@ -1324,7 +1326,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { postPreKeyMessageHandling(session, preKeyId, postponePreKeyMessageHandling); } } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage()); + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom() + ": " + e.getMessage()); } if (session.isFresh() && plaintextMessage != null) { @@ -1335,11 +1337,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void postPreKeyMessageHandling(final XmppAxolotlSession session, int preKeyId, final boolean postpone) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": postPreKeyMessageHandling() preKeyId="+preKeyId+", postpone="+Boolean.toString(postpone)); - //TODO: do not republish if we already removed this preKeyId - publishBundlesIfNeeded(false, false); + if (postpone) { + postponedSessions.add(session); + } else { + //TODO: do not republish if we already removed this preKeyId + publishBundlesIfNeeded(false, false); + completeSession(session); + } } + public void processPostponed() { + if (postponedSessions.size() > 0) { + publishBundlesIfNeeded(false, false); + } + Iterator iterator = postponedSessions.iterator(); + while (iterator.hasNext()) { + completeSession(iterator.next()); + iterator.remove(); + } + } + + private void completeSession(XmppAxolotlSession session) { + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId()); + axolotlMessage.addDevice(session); + try { + Jid jid = Jid.fromString(session.getRemoteAddress().getName()); + MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + mXmppConnectionService.sendMessagePacket(account, packet); + } catch (InvalidJidException 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) { XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; @@ -1351,7 +1381,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { postPreKeyMessageHandling(session, preKeyId, postponePreKeyMessageHandling); } } catch (CryptoFailedException e) { - Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage()); + Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage()); keyTransportMessage = null; } @@ -1363,13 +1393,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void putFreshSession(XmppAxolotlSession session) { - Log.d(Config.LOGTAG,"put fresh session"); + Log.d(Config.LOGTAG, "put fresh session"); sessions.put(session); if (Config.X509_VERIFICATION) { if (session.getIdentityKey() != null) { verifySessionWithPEP(session); } else { - Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); + Log.e(Config.LOGTAG, account.getJid().toBareJid() + ": identity key was empty after reloading for x509 verification"); } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 87fb298f2..559442288 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -45,8 +45,7 @@ public class IqGenerator extends AbstractGenerator { final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); packet.setId(request.getId()); packet.setTo(request.getFrom()); - final Element query = packet.addChild("query", - "http://jabber.org/protocol/disco#info"); + final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); query.setAttribute("node", request.query().getAttribute("node")); final Element identity = query.addChild("identity"); identity.setAttribute("category", "client"); @@ -91,6 +90,12 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket purgeOfflineMessages() { + final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.addChild("offline",Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); + return packet; + } + protected IqPacket publish(final String node, final Element item, final Bundle options) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element pubsub = packet.addChild("pubsub", diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index d82706a40..5a1eded20 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -91,6 +91,15 @@ public class MessageGenerator extends AbstractGenerator { return packet; } + public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setTo(to); + packet.setAxolotlMessage(axolotlMessage.toElement()); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } + private static boolean recipientSupportsOmemo(Message message) { Contact c = message.getContact(); return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index ef5c6755d..444b47132 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -11,6 +11,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.generator.AbstractGenerator; @@ -222,6 +223,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } + public boolean inCatchup(Account account) { + synchronized (this.queries) { + for(Query query : queries) { + if (query.account == account && query.isCatchup() && query.getWith() == null) { + return true; + } + } + } + return false; + } + public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { synchronized (this.queries) { for(Query query : queries) { @@ -268,6 +280,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { if (query.isCatchup() && query.getActualMessageCount() > 0) { mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount()); } + query.account.getAxolotlService().processPostponed(); } else { final Query nextQuery; if (query.getPagingOrder() == PagingOrder.NORMAL) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0276b73d0..26a14ee4d 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -303,6 +303,15 @@ public class XmppConnectionService extends Service { mJingleConnectionManager.cancelInTransmission(); fetchRosterFromServer(account); fetchBookmarks(account); + final boolean flexible= account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); + final boolean catchup = getMessageArchiveService().inCatchup(account); + if (flexible && catchup) { + sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, acc.getJid().toBareJid()+": successfully purged offline messages"); + } + }); + } sendPresence(account); if (mPushManagementService.available(account)) { mPushManagementService.registerPushTokenOnServer(account); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index f63b5f331..e5ef11001 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -16,4 +16,5 @@ public final class Namespace { public static final String PUBSUB_PUBLISH_OPTIONS = "http://jabber.org/protocol/pubsub#publish-options"; public static final String PUBSUB_ERROR = "http://jabber.org/protocol/pubsub#errors"; public static final String NICK = "http://jabber.org/protocol/nick"; + public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 12f601a0c..e98b37b6c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1681,6 +1681,10 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getServer(), "urn:xmpp:reporting:reason:spam:0"); } + public boolean flexibleOfflineMessageRetrieval() { + return hasDiscoFeature(account.getServer(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); + } + public boolean register() { return hasDiscoFeature(account.getServer(), Namespace.REGISTER); }