From 7cf406f944fc62a9fcdf508ac2254bfd30a3003f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 15 Apr 2025 19:20:28 +0200 Subject: [PATCH] refactor pep event parsing to new api --- .../crypto/axolotl/AxolotlService.java | 1709 +++++++++++------ .../conversations/entities/Bookmark.java | 40 +- .../conversations/parser/MessageParser.java | 133 +- .../services/XmppConnectionService.java | 127 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../siacs/conversations/xmpp/pep/Avatar.java | 163 +- .../xmpp/model/bookmark/Conference.java | 22 - .../android/xmpp/model/bookmark/Storage.java | 12 + .../xmpp/model/bookmark/package-info.java | 4 +- .../xmpp/model/bookmark2/Conference.java | 32 + .../{bookmark => bookmark2}/Extensions.java | 2 +- .../model/{bookmark => bookmark2}/Nick.java | 2 +- .../xmpp/model/bookmark2/package-info.java | 5 + .../android/xmpp/model/mds/Displayed.java | 5 + .../android/xmpp/model/pubsub/Items.java | 5 + .../xmpp/model/pubsub/event/Action.java | 14 + .../xmpp/model/pubsub/event/Delete.java | 11 + .../xmpp/model/pubsub/event/Event.java | 14 +- .../xmpp/model/pubsub/event/Purge.java | 3 +- .../xmpp/model/pubsub/event/Retract.java | 3 +- .../xmpp/model/storage/PrivateStorage.java | 13 + 21 files changed, 1492 insertions(+), 828 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java rename src/main/java/im/conversations/android/xmpp/model/{bookmark => bookmark2}/Extensions.java (81%) rename src/main/java/im/conversations/android/xmpp/model/{bookmark => bookmark2}/Nick.java (79%) create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java 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 938dc794119539f7ff25dc6d6e23b25cc72bb2ee..53757f939072473856f5f8ea8cfbf39dabb66c0d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -6,10 +6,8 @@ import android.os.Bundle; import android.security.KeyChain; import android.util.Log; import android.util.Pair; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -17,38 +15,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.SessionBuilder; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.UntrustedIdentityException; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; - -import java.security.PrivateKey; -import java.security.Security; -import java.security.Signature; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -71,6 +37,35 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportIn import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.model.stanza.Iq; +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.SessionBuilder; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.UntrustedIdentityException; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.PreKeyBundle; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.util.KeyHelper; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @@ -102,8 +97,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private int numPublishTriesOnEmptyPep = 0; private boolean pepBroken = false; private int lastDeviceListNotificationHash = 0; - private final Set postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment - private final Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup + private final Set postponedSessions = + new HashSet<>(); // sessions stored here will receive after mam catchup treatment + private final Set postponedHealing = + new HashSet<>(); // addresses stored here will need a healing notification after mam + // catchup private final AtomicBoolean changeAccessMode = new AtomicBoolean(false); public AxolotlService(Account account, XmppConnectionService connectionService) { @@ -156,7 +154,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { for (Jid jid : jids) { if (deviceIds.get(jid) != null) { for (Integer foreignId : this.deviceIds.get(jid)) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.toString(), foreignId); if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) { return true; } @@ -167,11 +166,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void preVerifyFingerprint(Contact contact, String fingerprint) { - axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), 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); + axolotlStore.preVerifyFingerprint( + account, account.getJid().asBareJid().toString(), fingerprint); } public boolean hasVerifiedKeys(String name) { @@ -184,11 +185,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public String getOwnFingerprint() { - return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); + return CryptoHelper.bytesToHex( + axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); } public Set getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); + return axolotlStore.getContactKeysWithTrust( + account.getJid().asBareJid().toString(), status); } public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { @@ -226,21 +229,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Collection findOwnSessions() { SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); + 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()); + 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() + // will be added in findOwnSessions() return Collections.emptySet(); } HashSet sessions = new HashSet<>(); @@ -280,7 +285,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void destroy() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": destroying old axolotl service. no longer in use"); mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); } @@ -306,7 +314,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": ignoring duplicate own device id list"); return; } this.lastDeviceListNotificationHash = hash; @@ -315,10 +325,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (me) { deviceIds.remove(getOwnDeviceId()); } - final Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); + final Set expiredDevices = + new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); expiredDevices.removeAll(deviceIds); for (Integer deviceId : expiredDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); XmppAxolotlSession session = sessions.get(address); if (session != null && session.getFingerprint() != null) { if (session.getTrust().isActive()) { @@ -328,11 +340,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } final Set newDevices = ImmutableSet.copyOf(deviceIds); for (final Integer deviceId : newDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + 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()); + Log.d( + Config.LOGTAG, + "reactivating device with fingerprint " + session.getFingerprint()); session.setTrust(session.getTrust().toActive()); } } @@ -343,7 +358,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } needsPublishing |= this.changeAccessMode.get(); for (final Integer deviceId : deviceIds) { - SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + SignalProtocolAddress ownDeviceAddress = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); if (sessions.get(ownDeviceAddress) == null) { FetchStatus status = fetchStatusMap.get(ownDeviceAddress); if (status == null || status == FetchStatus.TIMEOUT) { @@ -363,7 +379,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final boolean changed = oldSet == null || oldSet.hashCode() != hash; this.deviceIds.put(jid, deviceIds); if (changed) { - mXmppConnectionService.updateConversationUi(); //update the lock icon + mXmppConnectionService.updateConversationUi(); // update the lock icon mXmppConnectionService.keyStatusUpdated(null); if (me) { mXmppConnectionService.updateAccountUi(); @@ -375,7 +391,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void wipeOtherPepDevices() { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); return; } Set deviceIds = new HashSet<>(); @@ -391,22 +410,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void publishOwnDeviceIdIfNeeded() { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); return; } - Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (response.getType() == Iq.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 - final Element item = IqParser.getItem(response); - final Set deviceIds = IqParser.deviceIds(item); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); - registerDevices(account.getJid().asBareJid(), deviceIds); - } - - }); + Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveDeviceIds(account.getJid().asBareJid()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.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 + // TODO use new API + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved own device list: " + + deviceIds); + registerDevices(account.getJid().asBareJid(), deviceIds); + } + }); } private Set getExpiredDevices() { @@ -415,16 +451,34 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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 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"); + 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"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": own device " + + session.getFingerprint() + + " was active " + + hours + + " hours ago"); } - } //TODO print last activation diff + } // TODO print last activation diff } } return devices; @@ -435,12 +489,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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..."); + 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 + ")"); + Log.w( + Config.LOGTAG, + getLogprefix(account) + + "Own device list empty, attempting to publish (try " + + numPublishTriesOnEmptyPep + + ")"); } } else { numPublishTriesOnEmptyPep = 0; @@ -453,74 +515,136 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { publishDeviceIdsAndRefineAccessModel(ids, true); } - private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, response -> { - final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response); - 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); + private void publishDeviceIdsAndRefineAccessModel( + final Set ids, final boolean firstAttempt) { + final Bundle publishOptions = + account.getXmppConnection().getFeatures().pepPublishOptions() + ? PublishOptions.openAccess() + : null; + final var publish = + mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); + mXmppConnectionService.sendIqPacket( + account, + publish, + response -> { + final Element error = + response.getType() == Iq.Type.ERROR + ? response.findChild("error") + : null; + final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response); + 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 (response.getType() == Iq.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" + + response.findChild("error")); + } + } } }); - } 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 (response.getType() == Iq.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" + response.findChild("error")); - } - - } - } - }); } - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + 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()); + PrivateKey x509PrivateKey = + KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); + X509Certificate[] chain = + KeyChain.getCertificateChain( + mXmppConnectionService, account.getPrivateKeyAlias()); Signature verifier = Signature.getInstance("sha256WithRSA"); verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); - final Iq packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - 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); - } - }); - }); + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .publishVerification(signature, chain, getOwnDeviceId()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + ": publish verification for device " + + getOwnDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + 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(); } @@ -528,175 +652,310 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); + 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)); + 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"); + 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"); - } - final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - - if (response.getType() == Iq.Type.TIMEOUT) { - return; //ignore timeout. do nothing - } - - if (response.getType() == Iq.Type.ERROR) { - Element error = response.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" + response); - return; - } - } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server gained publish-options capabilities. changing access" + + " model"); + } + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.TIMEOUT) { + return; // ignore timeout. do nothing + } - PreKeyBundle bundle = IqParser.bundle(response); - final Map keys = IqParser.preKeyPublics(response); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response); - 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:" + response); - } - 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; - } + if (response.getType() == Iq.Type.ERROR) { + Element error = response.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" + + response); + return; + } + } - // 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; + PreKeyBundle bundle = IqParser.bundle(response); + final Map keys = IqParser.preKeyPublics(response); + boolean flush = false; + if (bundle == null) { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received invalid bundle:" + + response); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = 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; - } + if (keys == null) { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received invalid prekeys:" + + response); + } + 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 PreKeys - Set preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); + 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 ignored) { + } 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; } - } - } - 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."); - } + // 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(); + 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()); } - } - } 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) { + 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; - final Iq 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, response -> { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response); - 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); + 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; + final Iq 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, + response -> { + final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response); + 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 (response.getType() == Iq.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 (response.getType() == Iq.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: " + + response.toString()); + } + pepBroken = true; } }); - } else if (response.getType() == Iq.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 (response.getType() == Iq.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: " + response.toString()); - } - pepBroken = true; - } - }); } public void deleteOmemoIdentity() { @@ -734,8 +993,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { mXmppConnectionService.updateAccountUi(); } - private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + private ListenableFuture 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(); final Jid jid; @@ -747,64 +1011,89 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } final SettableFuture future = SettableFuture.create(); - final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - Pair verification = IqParser.verification(response); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + Pair verification = IqParser.verification(response); + if (verification != null) { 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 jid1 = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); - account.getRoster().getContact(jid1).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored + 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 jid1 = Jid.of(address.getName()); + Log.d( + Config.LOGTAG, + "setting common name for " + jid1 + " to " + cn); + account.getRoster().getContact(jid1).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + // ignored + } + finishBuildingSessionsFromPEP(address); + future.set(session); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } } - finishBuildingSessionsFromPEP(address); - future.set(session); - return; } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); } + } else { + Log.d(Config.LOGTAG, "no verification found"); } - } 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); - future.set(session); - }); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + future.set(session); + }); return future; } private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + 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)) { + if (own.containsValue(FetchStatus.SUCCESS) + || remote.containsValue(FetchStatus.SUCCESS)) { report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { + } 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)) { + } 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)) { + } else if (own.containsValue(FetchStatus.ERROR) + || remote.containsValue(FetchStatus.ERROR)) { report = FetchStatus.ERROR; } mXmppConnectionService.keyStatusUpdated(report); @@ -814,9 +1103,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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)) { + 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"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error fetching own device with id " + + id + + ". removing from announcement"); } } if (publish) { @@ -841,7 +1137,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callbacks.add(callback); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fetching device ids for " + + jid + + " already running. adding callback"); packet = null; } else { callbacks = new ArrayList<>(); @@ -849,43 +1150,49 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { callbacks.add(callback); } this.fetchDeviceIdsMap.put(jid, callbacks); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": fetching device ids for " + jid); packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); } } if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (response.getType() == Iq.Type.RESULT) { - fetchDeviceListStatus.put(jid, true); - final Element item = IqParser.getItem(response); - final Set deviceIds = IqParser.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() == Iq.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); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.RESULT) { + fetchDeviceListStatus.put(jid, true); + // TODO use new API + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.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() == Iq.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); + } + } } - } - } - }); + }); } } @@ -893,126 +1200,194 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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(); - } - } - }); + fetchDeviceIds( + jid, + (j, deviceIds) -> { + synchronized (unfinishedJids) { + unfinishedJids.remove(j); + if (unfinishedJids.size() == 0 && callback != null) { + callback.fetched(); + } + } + }); } } } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address) { return buildSessionFromPEP(address, null); } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address, OnSessionBuildFromPep callback) { final SettableFuture sessionSettableFuture = SettableFuture.create(); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); + 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?!"); + 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()); - final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, packet -> { - if (packet.getType() == Iq.Type.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); - } else if (packet.getType() == Iq.Type.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final List preKeyBundleList = IqParser.preKeys(packet); - final PreKeyBundle bundle = IqParser.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(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid")); - 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(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found")); - return; - } + final Iq bundlesPacket = + mXmppConnectionService + .getIqGenerator() + .retrieveBundlesForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + bundlesPacket, + packet -> { + if (packet.getType() == Iq.Type.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + sessionSettableFuture.setException( + new CryptoFailedException("Unable to build session. Timeout")); + } else if (packet.getType() == Iq.Type.RESULT) { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received preKey IQ packet, processing..."); + final List preKeyBundleList = IqParser.preKeys(packet); + final PreKeyBundle bundle = IqParser.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(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Invalid")); + 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(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. No suitable PreKey found")); + return; + } - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + 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) { - sessionSettableFuture.setFuture(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; + 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) { + sessionSettableFuture.setFuture( + 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(); + } + sessionSettableFuture.set(session); + } + } 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(); + } + sessionSettableFuture.setException(new CryptoFailedException(e)); } - fetchStatusMap.put(address, fetchStatus); + } 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.onSessionBuildSuccessful(); + callback.onSessionBuildFailed(); } - sessionSettableFuture.set(session); + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Error")); } - } 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(); - } - sessionSettableFuture.setException(new CryptoFailedException(e)); - } - } 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(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error")); - } - }); + }); return sessionSettableFuture; } 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); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " remove own device id " + + id + + " from announcement. devices left:" + + temp); publishOwnDeviceId(temp); } } @@ -1020,47 +1395,95 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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); + 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); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.toString(), foreignId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + 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); + 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); + 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"); + 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!"); + 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); + SignalProtocolAddress address = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + 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); + 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); + 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"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "skipping over " + + address + + " because it's broken"); } } } @@ -1077,9 +1500,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { iterator.remove(); } } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": createSessionsIfNeeded() - jids with empty device list: " + + jidsWithEmptyDeviceList); if (jidsWithEmptyDeviceList.size() > 0) { - fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); + fetchDeviceIds( + jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); return true; } else { return createSessionsIfNeededActual(conversation); @@ -1087,11 +1515,17 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private boolean createSessionsIfNeededActual(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + 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()); + 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); @@ -1100,7 +1534,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else if (status == FetchStatus.PENDING) { newSessions = true; } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already fetching bundle for " + + address.toString()); } } @@ -1125,14 +1563,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public boolean hasPendingKeyFetches(List jids) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + 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)) { + SignalProtocolAddress foreignAddress = + new SignalProtocolAddress(jid.asBareJid().toString(), 0); + if (fetchStatusMap + .getAll(foreignAddress.getName()) + .containsValue(FetchStatus.PENDING) + || this.fetchDeviceIdsMap.containsKey(jid)) { return true; } } @@ -1143,7 +1586,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @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(); + final boolean acceptEmpty = + (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) + || c.getContact().isSelf(); Collection ownSessions = findOwnSessions(); if (remoteSessions.isEmpty() && !acceptEmpty) { return false; @@ -1158,7 +1603,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return true; } - //this is being used for private muc messages only + // this is being used for private muc messages only private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { if (jid == null) { return false; @@ -1177,7 +1622,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Nullable public XmppAxolotlMessage encrypt(Message message) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content; if (message.hasFileOnRemoteHost()) { content = message.getFileParams().url; @@ -1187,7 +1633,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { try { axolotlMessage.encrypt(content); } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); return null; } @@ -1201,31 +1649,42 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } 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); - } - } - }); + 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); + } + } + }); } - private OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException { - final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo(); + private OmemoVerifiedIceUdpTransportInfo encrypt( + final IceUdpTransportInfo element, final XmppAxolotlSession session) + throws CryptoFailedException { + final OmemoVerifiedIceUdpTransportInfo transportInfo = + new OmemoVerifiedIceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); for (final Element child : element.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { - final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + if ("fingerprint".equals(child.getName()) + && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Element fingerprint = + new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content = child.getContent(); axolotlMessage.encrypt(content); axolotlMessage.addDevice(session, true); @@ -1238,25 +1697,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return transportInfo; } - - public ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { + public ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { return Futures.transformAsync( getSession(jid, deviceId), session -> encrypt(rtpContentMap, session), - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { + private ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { if (Config.REQUIRE_RTP_VERIFICATION) { requireVerification(session); } - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(session.getFingerprint()); - for (final Map.Entry> content : rtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + for (final Map.Entry> + content : rtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; try { encryptedTransportInfo = encrypt(descriptionTransport.transport, session); @@ -1265,18 +1728,21 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + encryptedTransportInfo)); } return Futures.immediateFuture( new OmemoVerifiedPayload<>( omemoVerification, - new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) - )); + new OmemoVerifiedRtpContentMap( + rtpContentMap.group, descriptionTransportBuilder.build()))); } private ListenableFuture getSession(final Jid jid, final int deviceId) { - final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + final SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); final XmppAxolotlSession session = sessions.get(address); if (session == null) { return buildSessionFromPEP(address); @@ -1284,26 +1750,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } - public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + public ListenableFuture> decrypt( + OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); - final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); - for (final Map.Entry> content : omemoVerifiedRtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + final ImmutableList.Builder> pepVerificationFutures = + new ImmutableList.Builder<>(); + for (final Map.Entry> + content : omemoVerifiedRtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedPayload decryptedTransport; try { - decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); + decryptedTransport = + decrypt( + (OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, + from, + pepVerificationFutures); } catch (CryptoFailedException e) { return Futures.immediateFailedFuture(e); } omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + decryptedTransport.payload)); } processPostponed(); - final ImmutableList> sessionFutures = pepVerificationFutures.build(); + final ImmutableList> sessionFutures = + pepVerificationFutures.build(); return Futures.transform( Futures.allAsList(sessionFutures), sessions -> { @@ -1314,27 +1793,35 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } return new OmemoVerifiedPayload<>( omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) - ); - + new RtpContentMap( + omemoVerifiedRtpContentMap.group, + descriptionTransportBuilder.build())); }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { + private OmemoVerifiedPayload decrypt( + final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, + final Jid from, + ImmutableList.Builder> pepVerificationFutures) + throws CryptoFailedException { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); final OmemoVerification omemoVerification = new OmemoVerification(); for (final Element child : verifiedIceUdpTransportInfo.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { + if ("fingerprint".equals(child.getName()) + && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); + final Element encrypted = + child.findChildEnsureSingle( + XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); + final XmppAxolotlMessage xmppAxolotlMessage = + XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage); - final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); + final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = + xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); final Integer preKeyId = session.getPreKeyIdAndReset(); if (preKeyId != null) { postponedSessions.add(session); @@ -1359,45 +1846,53 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getTrust().isVerified()) { return; } - throw new NotVerifiedException(String.format( - "session with %s was not verified", - session.getFingerprint() - )); + throw new NotVerifiedException( + String.format("session with %s was not verified", session.getFingerprint())); } - public ListenableFuture prepareKeyTransportMessage(final Conversation conversation) { - return Futures.submit(()->{ - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - return axolotlMessage; - } else { - throw new IllegalStateException("No session to decrypt to"); - } - },executor); + public ListenableFuture prepareKeyTransportMessage( + final Conversation conversation) { + return Futures.submit( + () -> { + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + if (buildHeader(axolotlMessage, conversation)) { + return axolotlMessage; + } else { + throw new IllegalStateException("No session to decrypt to"); + } + }, + executor); } 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()); + 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()); + 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(); + 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()); + SignalProtocolAddress senderAddress = + new SignalProtocolAddress( + message.getFrom().toString(), message.getSenderDeviceId()); return getReceivingSession(senderAddress); - } private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { @@ -1411,7 +1906,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return session; } - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException { + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage( + XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) + throws NotEncryptedForThisDeviceException, + BrokenSessionException, + OutdatedSenderException { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; XmppAxolotlSession session = getReceivingSession(message); @@ -1423,7 +1922,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { postPreKeyMessageHandling(session, postponePreKeyMessageHandling); } } catch (NotEncryptedForThisDeviceException e) { - if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { + if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) + && message.getSenderDeviceId() == ownDeviceId) { Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); } else { throw e; @@ -1434,7 +1934,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), + e); } if (session.isFresh() && plaintextMessage != null) { @@ -1445,7 +1948,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": broken session with " + e.getSignalProtocolAddress().toString() + " detected", e); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": broken session with " + + e.getSignalProtocolAddress().toString() + + " detected", + e); if (postpone) { postponedHealing.add(e.getSignalProtocolAddress()); } else { @@ -1455,32 +1964,51 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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)); - } + 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"); - } - }); + @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"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": do not attempt to heal " + + signalProtocolAddress + + " again"); } } - private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { + 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"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); } if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) { completeSession(session); @@ -1502,7 +2030,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } iterator.remove(); } - final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); + final Iterator postponedHealingAttemptsIterator = + postponedHealing.iterator(); while (postponedHealingAttemptsIterator.hasNext()) { notifyRequiresHealing(postponedHealingAttemptsIterator.next()); postponedHealingAttemptsIterator.remove(); @@ -1527,18 +2056,24 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void completeSession(XmppAxolotlSession session) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); axolotlMessage.addDevice(session, true); try { final Jid jid = Jid.of(session.getRemoteAddress().getName()); - final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + final var 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); + 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) { + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage( + XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; final XmppAxolotlSession session = getReceivingSession(message); try { @@ -1565,7 +2100,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getIdentityKey() != null) { return verifySessionWithPEP(session); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": identity key was empty after reloading for x509 verification"); } } return Futures.immediateFuture(session); @@ -1584,7 +2122,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { void fetched(Jid jid, Set deviceIds); } - public interface OnMultipleDeviceIdFetched { void fetched(); } @@ -1644,14 +2181,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { 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) { + public SessionMap( + XmppConnectionService service, SQLiteAxolotlStore store, Account account) { super(); this.xmppConnectionService = service; this.account = account; @@ -1671,30 +2208,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return candidates; } - private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { + 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(); + IdentityKey identityKey = + store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); if (Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); + X509Certificate certificate = + store.getFingerprintCertificate( + CryptoHelper.bytesToHex( + identityKey.getPublicKey().serialize())); if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); + 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 + // ignored } } } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + this.put( + axolotlAddress, + new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } private void fillMap(SQLiteAxolotlStore store) { - List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); + List deviceIds = + store.getSubDeviceSessions(account.getJid().asBareJid().toString()); putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); for (String address : store.getKnownAddresses()) { deviceIds = store.getSubDeviceSessions(address); @@ -1723,7 +2269,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } for (Map.Entry entry : devices.entrySet()) { if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); + Log.d( + Config.LOGTAG, + "resetting error for " + + jid.asBareJid() + + "(" + + entry.getKey() + + ")"); entry.setValue(FetchStatus.TIMEOUT); } } @@ -1760,6 +2312,5 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public NotVerifiedException(String message) { super(message); } - } } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 2a009482806658f8a7da4dec44796f988dd2d65c..41bd8c978a85bd32a32546f55f88a67d26310ae7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -10,6 +10,9 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.bookmark2.Conference; +import im.conversations.android.xmpp.model.pubsub.PubSub; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; @@ -36,7 +39,8 @@ public class Bookmark extends Element implements ListItem { this.account = account; } - public static Map parseFromStorage(Element storage, Account account) { + public static Map parseFromStorage( + final Storage storage, final Account account) { if (storage == null) { return Collections.emptyMap(); } @@ -57,24 +61,24 @@ public class Bookmark extends Element implements ListItem { return bookmarks; } - public static Map parseFromPubSub(final Element pubSub, final Account account) { + public static Map parseFromPubSub(final PubSub pubSub, final Account account) { if (pubSub == null) { return Collections.emptyMap(); } - final Element items = pubSub.findChild("items"); - if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) { - final Map bookmarks = new HashMap<>(); - for (Element item : items.getChildren()) { - if (item.getName().equals("item")) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - bookmarks.put(bookmark.jid, bookmark); - } - } + final var items = pubSub.getItems(); + if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) { + return Collections.emptyMap(); + } + final Map bookmarks = new HashMap<>(); + for (final var item : items.getItemMap(Conference.class).entrySet()) { + final Bookmark bookmark = + Bookmark.parseFromItem(item.getKey(), item.getValue(), account); + if (bookmark == null) { + continue; } - return bookmarks; + bookmarks.put(bookmark.jid, bookmark); } - return Collections.emptyMap(); + return bookmarks; } public static Bookmark parse(Element element, Account account) { @@ -88,13 +92,13 @@ public class Bookmark extends Element implements ListItem { return bookmark; } - public static Bookmark parseFromItem(Element item, Account account) { - final Element conference = item.findChild("conference", Namespace.BOOKMARKS2); - if (conference == null) { + public static Bookmark parseFromItem( + final String id, final Conference conference, final Account account) { + if (id == null || conference == null) { return null; } final Bookmark bookmark = new Bookmark(account); - bookmark.jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id")); + bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id)); // TODO verify that we only use bare jids and ignore full jids if (bookmark.jid == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index b6194ccd1be5f90ac229a85b32e3143fa8356aba..83e817c2b5c95eaf42ab315d3b3291dc943bb0f4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.AppSettings; @@ -37,15 +38,23 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.avatar.Metadata; +import im.conversations.android.xmpp.model.axolotl.DeviceList; import im.conversations.android.xmpp.model.axolotl.Encrypted; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.bookmark2.Conference; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.markers.Displayed; +import im.conversations.android.xmpp.model.nick.Nick; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.oob.OutOfBandData; +import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.pubsub.event.Delete; import im.conversations.android.xmpp.model.pubsub.event.Event; +import im.conversations.android.xmpp.model.pubsub.event.Purge; import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.receipts.Request; import im.conversations.android.xmpp.model.unique.StanzaId; @@ -53,6 +62,7 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -244,11 +254,13 @@ public class MessageParser extends AbstractParser return null; } - private void parseEvent(final Event event, final Jid from, final Account account) { - final Element items = event.findChild("items"); - final String node = items == null ? null : items.getAttribute("node"); + private void parseEvent(final Items items, final Jid from, final Account account) { + final String node = items.getNode(); if ("urn:xmpp:avatar:metadata".equals(node)) { - Avatar avatar = Avatar.parseMetadata(items); + // TODO support retract + final var entry = items.getFirstItemWithId(Metadata.class); + final var avatar = + entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); if (avatar != null) { avatar.owner = from.asBareJid(); if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { @@ -274,24 +286,27 @@ public class MessageParser extends AbstractParser } } } else if (Namespace.NICK.equals(node)) { - final Element i = items.findChild("item"); - final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK); + final var nickItem = items.getFirstItem(Nick.class); + final String nick = nickItem == null ? null : nickItem.getContent(); if (nick != null) { setNick(account, from, nick); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Element item = items.findChild("item"); - final Set deviceIds = IqParser.deviceIds(item); - Log.d( - Config.LOGTAG, - AxolotlService.getLogprefix(account) - + "Received PEP device list " - + deviceIds - + " update from " - + from - + ", processing... "); - final AxolotlService axolotlService = account.getAxolotlService(); - axolotlService.registerDevices(from, deviceIds); + final var deviceList = items.getFirstItem(DeviceList.class); + if (deviceList != null) { + final Set deviceIds = deviceList.getDeviceIds(); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received PEP device list " + + deviceIds + + " update from " + + from + + ", processing... "); + final AxolotlService axolotlService = account.getAxolotlService(); + axolotlService.registerDevices(from, new HashSet<>(deviceIds)); + } + } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { final var connection = account.getXmppConnection(); if (connection.getFeatures().bookmarksConversion()) { @@ -302,9 +317,7 @@ public class MessageParser extends AbstractParser + ": received storage:bookmark notification even though we" + " opted into bookmarks:1"); } - final Element i = items.findChild("item"); - final Element storage = - i == null ? null : i.findChild("storage", Namespace.BOOKMARKS); + final var storage = items.getFirstItem(Storage.class); final Map bookmarks = Bookmark.parseFromStorage(storage, account); mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); Log.d( @@ -318,17 +331,19 @@ public class MessageParser extends AbstractParser + " not detected"); } } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - final Element item = items.findChild("item"); - final Element retract = items.findChild("retract"); - if (item != null) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - account.putBookmark(bookmark); - mXmppConnectionService.processModifiedBookmark(bookmark); - mXmppConnectionService.updateConversationUi(); + final var retractions = items.getRetractions(); + ; + for (final var item : items.getItemMap(Conference.class).entrySet()) { + final Bookmark bookmark = + Bookmark.parseFromItem(item.getKey(), item.getValue(), account); + if (bookmark == null) { + continue; } + account.putBookmark(bookmark); + mXmppConnectionService.processModifiedBookmark(bookmark); + mXmppConnectionService.updateConversationUi(); } - if (retract != null) { + for (final var retract : retractions) { final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id")); if (id != null) { account.removeBookmark(id); @@ -342,35 +357,37 @@ public class MessageParser extends AbstractParser } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION && Namespace.MDS_DISPLAYED.equals(node) && account.getJid().asBareJid().equals(from)) { - final Element item = items.findChild("item"); - mXmppConnectionService.processMdsItem(account, item); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + " received pubsub notification for node=" - + node); + for (final var item : + items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class) + .entrySet()) { + mXmppConnectionService.processMdsItem(account, item); + } } } - private void parseDeleteEvent(final Element event, final Jid from, final Account account) { - final Element delete = event.findChild("delete"); - final String node = delete == null ? null : delete.getAttribute("node"); + private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) { + final String node = delete.getNode(); if (Namespace.NICK.equals(node)) { - Log.d(Config.LOGTAG, "parsing nick delete event from " + from); setNick(account, from, null); } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); deleteAllBookmarks(account); - } else if (Namespace.AVATAR_METADATA.equals(node) - && account.getJid().asBareJid().equals(from)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node"); + } else if (Namespace.AVATAR_METADATA.equals(node)) { + final boolean isAccount = account.getJid().asBareJid().equals(from); + if (isAccount) { + account.setAvatar(null); + mXmppConnectionService.databaseBackend.updateAccount(account); + mXmppConnectionService.getAvatarService().clear(account); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": deleted avatar metadata node"); + } } } - private void parsePurgeEvent(final Element event, final Jid from, final Account account) { - final Element purge = event.findChild("purge"); - final String node = purge == null ? null : purge.getAttribute("node"); + private void parsePurgeEvent( + @NonNull final Purge purge, final Jid from, final Account account) { + final String node = purge.getNode(); if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks"); deleteAllBookmarks(account); @@ -1394,12 +1411,20 @@ public class MessageParser extends AbstractParser final var event = original.getExtension(Event.class); if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { - if (event.hasChild("items")) { - parseEvent(event, original.getFrom(), account); - } else if (event.hasChild("delete")) { - parseDeleteEvent(event, original.getFrom(), account); - } else if (event.hasChild("purge")) { - parsePurgeEvent(event, original.getFrom(), account); + final var action = event.getAction(); + final var node = action == null ? null : action.getNode(); + if (node == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no node found in PubSub event from " + + original.getFrom()); + } else if (action instanceof Items items) { + parseEvent(items, original.getFrom(), account); + } else if (action instanceof Purge purge) { + parsePurgeEvent(purge, original.getFrom(), account); + } else if (action instanceof Delete delete) { + parseDeleteEvent(delete, from, account); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 32b1ae4645475c275d3e39abf1156635168cb78e..900028152aa061bcc31767fdb2b0e12dfc03abdb 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -137,7 +137,12 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; +import im.conversations.android.xmpp.model.avatar.Metadata; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.mds.Displayed; +import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.storage.PrivateStorage; import java.io.File; import java.security.Security; import java.security.cert.CertificateException; @@ -2093,14 +2098,17 @@ public class XmppConnectionService extends Service { public void fetchBookmarks(final Account account) { final Iq iqPacket = new Iq(Iq.Type.GET); - final Element query = iqPacket.query("jabber:iq:private"); - query.addChild("storage", Namespace.BOOKMARKS); + iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage()); final Consumer callback = (response) -> { if (response.getType() == Iq.Type.RESULT) { - final Element query1 = response.query(); - final Element storage = query1.findChild("storage", "storage:bookmarks"); - Map bookmarks = Bookmark.parseFromStorage(storage, account); + final var privateStorage = response.getExtension(PrivateStorage.class); + if (privateStorage == null) { + return; + } + final var bookmarkStorage = privateStorage.getExtension(Storage.class); + Map bookmarks = + Bookmark.parseFromStorage(bookmarkStorage, account); processBookmarksInitial(account, bookmarks, false); } else { Log.d( @@ -2118,7 +2126,7 @@ public class XmppConnectionService extends Service { retrieve, (response) -> { if (response.getType() == Iq.Type.RESULT) { - final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final var pubsub = response.getExtension(PubSub.class); final Map bookmarks = Bookmark.parseFromPubSub(pubsub, account); processBookmarksInitial(account, bookmarks, true); @@ -2136,30 +2144,34 @@ public class XmppConnectionService extends Service { if (response.getType() != Iq.Type.RESULT) { return; } - final var pubSub = response.findChild("pubsub", Namespace.PUBSUB); - final Element items = pubSub == null ? null : pubSub.findChild("items"); - if (items == null - || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) { + final var pubsub = response.getExtension(PubSub.class); + if (pubsub == null) { return; } - for (final Element child : items.getChildren()) { - if ("item".equals(child.getName())) { - processMdsItem(account, child); + final var items = pubsub.getItems(); + if (items == null) { + return; + } + if (Namespace.MDS_DISPLAYED.equals(items.getNode())) { + for (final var item : + items.getItemMap( + im.conversations.android.xmpp.model.mds.Displayed + .class) + .entrySet()) { + processMdsItem(account, item); } } }); } - public void processMdsItem(final Account account, final Element item) { - final Jid jid = - item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id")); + public void processMdsItem(final Account account, final Map.Entry item) { + final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey())); if (jid == null) { return; } - final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED); - final Element stanzaId = - displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS); - final String id = stanzaId == null ? null : stanzaId.getAttribute("id"); + final var displayed = item.getValue(); + final var stanzaId = displayed.getStanzaId(); + final String id = stanzaId == null ? null : stanzaId.getId(); final Conversation conversation = find(account, jid); if (id != null && conversation != null) { conversation.setDisplayState(id); @@ -4920,16 +4932,20 @@ public class XmppConnectionService extends Service { packet, new Consumer() { - private Avatar parseAvatar(Iq packet) { - Element pubsub = - packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - return Avatar.parseMetadata(items); - } + private Avatar parseAvatar(final Iq packet) { + final var pubsub = packet.getExtension(PubSub.class); + if (pubsub == null) { + return null; + } + final var items = pubsub.getItems(); + if (items == null) { + return null; } - return null; + final var item = items.getFirstItemWithId(Metadata.class); + if (item == null) { + return null; + } + return Avatar.parseMetadata(item.getKey(), item.getValue()); } private boolean errorIsItemNotFound(Iq packet) { @@ -5164,30 +5180,39 @@ public class XmppConnectionService extends Service { account, packet, response -> { - if (response.getType() == Iq.Type.RESULT) { - Element pubsub = - response.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - return; - } - } + if (response.getType() != Iq.Type.RESULT) { + callback.error(0, null); + } + final var pubsub = packet.getExtension(PubSub.class); + if (pubsub == null) { + callback.error(0, null); + return; + } + final var items = pubsub.getItems(); + if (items == null) { + callback.error(0, null); + return; + } + final var item = items.getFirstItemWithId(Metadata.class); + if (item == null) { + callback.error(0, null); + return; + } + final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue()); + if (avatar == null) { + callback.error(0, null); + return; + } + avatar.owner = account.getJid().asBareJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); } - callback.error(0, null); }); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index cc08154f9b4fa0a510765b13619b8d1c16dce493..9095de54e8cc5e076dbec2843ae0e5334fdaa53f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -112,4 +112,5 @@ public final class Namespace { public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; + public static final String PRIVATE_XML_STORAGE = "jabber:iq:private"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index a4397e466a93a1ad107976fa79cefcef9425f082..3109b1d0696ce6b30d966dc137bd91fc9947f472 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -1,102 +1,95 @@ package eu.siacs.conversations.xmpp.pep; import android.util.Base64; - import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.avatar.Metadata; public class Avatar { - public enum Origin { PEP, VCARD } + public enum Origin { + PEP, + VCARD + } public String type; - public String sha1sum; - public String image; - public int height; - public int width; - public long size; - public Jid owner; - public Origin origin = Origin.PEP; //default to maintain compat + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public Jid owner; + public Origin origin = Origin.PEP; // default to maintain compat - public byte[] getImageAsBytes() { - return Base64.decode(image, Base64.DEFAULT); - } + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } - public String getFilename() { - return sha1sum; - } + public String getFilename() { + return sha1sum; + } - public static Avatar parseMetadata(Element items) { - Element item = items.findChild("item"); - if (item == null) { - return null; - } - Element metadata = item.findChild("metadata"); - if (metadata == null) { - return null; - } - String primaryId = item.getAttribute("id"); - if (primaryId == null) { - return null; - } - for (Element child : metadata.getChildren()) { - if (child.getName().equals("info") - && primaryId.equals(child.getAttribute("id"))) { - Avatar avatar = new Avatar(); - String height = child.getAttribute("height"); - String width = child.getAttribute("width"); - String size = child.getAttribute("bytes"); - try { - if (height != null) { - avatar.height = Integer.parseInt(height); - } - if (width != null) { - avatar.width = Integer.parseInt(width); - } - if (size != null) { - avatar.size = Long.parseLong(size); - } - } catch (NumberFormatException e) { - return null; - } - avatar.type = child.getAttribute("type"); - String hash = child.getAttribute("id"); - if (!isValidSHA1(hash)) { - return null; - } - avatar.sha1sum = hash; - avatar.origin = Origin.PEP; - return avatar; - } - } - return null; - } + public static Avatar parseMetadata(final String primaryId, final Metadata metadata) { + if (primaryId == null || metadata == null) { + return null; + } + for (Element child : metadata.getChildren()) { + if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height != null) { + avatar.height = Integer.parseInt(height); + } + if (width != null) { + avatar.width = Integer.parseInt(width); + } + if (size != null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + String hash = child.getAttribute("id"); + if (!isValidSHA1(hash)) { + return null; + } + avatar.sha1sum = hash; + avatar.origin = Origin.PEP; + return avatar; + } + } + return null; + } - @Override - public boolean equals(Object object) { - if (object != null && object instanceof Avatar) { - Avatar other = (Avatar) object; - return other.getFilename().equals(this.getFilename()); - } else { - return false; - } - } + @Override + public boolean equals(Object object) { + if (object != null && object instanceof Avatar) { + Avatar other = (Avatar) object; + return other.getFilename().equals(this.getFilename()); + } else { + return false; + } + } - public static Avatar parsePresence(Element x) { - String hash = x == null ? null : x.findChildContent("photo"); - if (hash == null) { - return null; - } - if (!isValidSHA1(hash)) { - return null; - } - Avatar avatar = new Avatar(); - avatar.sha1sum = hash; - avatar.origin = Origin.VCARD; - return avatar; - } + public static Avatar parsePresence(Element x) { + String hash = x == null ? null : x.findChildContent("photo"); + if (hash == null) { + return null; + } + if (!isValidSHA1(hash)) { + return null; + } + Avatar avatar = new Avatar(); + avatar.sha1sum = hash; + avatar.origin = Origin.VCARD; + return avatar; + } - private static boolean isValidSHA1(String s) { - return s != null && s.matches("[a-fA-F0-9]{40}"); - } + private static boolean isValidSHA1(String s) { + return s != null && s.matches("[a-fA-F0-9]{40}"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java index 0f924e8883b40157d0aa96676ab5f6412e971138..17fed121b1145978b81fe3a8ba5ac521fb2d3de3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java @@ -1,32 +1,10 @@ package im.conversations.android.xmpp.model.bookmark; -import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; -@XmlElement public class Conference extends Extension { public Conference() { super(Conference.class); } - - public boolean isAutoJoin() { - return this.getAttributeAsBoolean("autojoin"); - } - - public String getConferenceName() { - return this.getAttribute("name"); - } - - public void setAutoJoin(boolean autoJoin) { - setAttribute("autojoin", autoJoin); - } - - public Nick getNick() { - return this.getExtension(Nick.class); - } - - public Extensions getExtensions() { - return this.getExtension(Extensions.class); - } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java new file mode 100644 index 0000000000000000000000000000000000000000..2ecac583dd8686251550bf6f66686ec2ee7f2f00 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Storage extends Extension { + + public Storage() { + super(Storage.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java index 1bb963be849a57cdfeae7a2eb1c8930db49c799a..2a50d0f27818cf8cffe9904275e940ad88a298e3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java @@ -1,5 +1,5 @@ -@XmlPackage(namespace = Namespace.BOOKMARKS2) +@XmlPackage(namespace = Namespace.BOOKMARKS) package im.conversations.android.xmpp.model.bookmark; -import im.conversations.android.annotation.XmlPackage; import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java new file mode 100644 index 0000000000000000000000000000000000000000..265c80e3fd7db0d2e56572eb700f94592af12a5e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java @@ -0,0 +1,32 @@ +package im.conversations.android.xmpp.model.bookmark2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Conference extends Extension { + + public Conference() { + super(Conference.class); + } + + public boolean isAutoJoin() { + return this.getAttributeAsBoolean("autojoin"); + } + + public String getConferenceName() { + return this.getAttribute("name"); + } + + public void setAutoJoin(boolean autoJoin) { + setAttribute("autojoin", autoJoin); + } + + public Nick getNick() { + return this.getExtension(Nick.class); + } + + public Extensions getExtensions() { + return this.getExtension(Extensions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java similarity index 81% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java index b9385cf5473de88d5d43d771a57fe88995bc2536..f3d3534faaa49f200f4575160af7786ae260b066 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java @@ -1,4 +1,4 @@ -package im.conversations.android.xmpp.model.bookmark; +package im.conversations.android.xmpp.model.bookmark2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java similarity index 79% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java index ee5efa3864baebb0c848c65769b6b0a8d4a69022..c309e5056b74daa783e7fcc2f68796b1357938ba 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java @@ -1,4 +1,4 @@ -package im.conversations.android.xmpp.model.bookmark; +package im.conversations.android.xmpp.model.bookmark2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d872edde9aaa08dbacb78b603f97512f80860e98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BOOKMARKS2) +package im.conversations.android.xmpp.model.bookmark2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java index 9f5275371c3564ee52d1825bc67fda4327d36514..a49216c27dc2f1ea7f5cd9ee1f1538366127ac88 100644 --- a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java +++ b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java @@ -3,10 +3,15 @@ package im.conversations.android.xmpp.model.mds; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.unique.StanzaId; @XmlElement(namespace = Namespace.MDS_DISPLAYED) public class Displayed extends Extension { public Displayed() { super(Displayed.class); } + + public StanzaId getStanzaId() { + return this.getOnlyExtension(StanzaId.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java index ceb1931ca35cbb1048e5c66f14cfe5ea9eff2303..47fc68c08368afccb4fc20703bd2aeae1aa81728 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java @@ -45,6 +45,11 @@ public interface Items { return Iterables.getFirst(map.values(), null); } + default Map.Entry getFirstItemWithId(final Class clazz) { + final var entries = getItemMap(clazz).entrySet(); + return Iterables.getFirst(entries, null); + } + default T getOnlyItem(final Class clazz) { final var map = getItemMap(clazz); return Iterables.getOnlyElement(map.values()); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java new file mode 100644 index 0000000000000000000000000000000000000000..c41a9fbdb2d4038835b711978a1920e37e772189 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class Action extends Extension { + + public Action(Class clazz) { + super(clazz); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java new file mode 100644 index 0000000000000000000000000000000000000000..ed5f6295eca366dff2895757402cc618042547c4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Delete extends Action { + + public Delete() { + super(Delete.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java index 1e180c460053ee0bb74cbefa753c86b63f175e2f..d224060d452378cc0f81bb60b6ad5061b4dc2a5d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java @@ -12,25 +12,17 @@ public class Event extends Extension { super(Event.class); } - public Items getItems() { - return this.getExtension(ItemsWrapper.class); - } - - public Purge getPurge() { - return this.getExtension(Purge.class); + public Action getAction() { + return this.getOnlyExtension(Action.class); } @XmlElement(name = "items") - public static class ItemsWrapper extends Extension implements Items { + public static class ItemsWrapper extends Action implements Items { public ItemsWrapper() { super(ItemsWrapper.class); } - public String getNode() { - return this.getAttribute("node"); - } - public Collection getItems() { return this.getExtensions(Item.class); } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java index 64550e0b77f1ca33d57e2b501204995545bfcac6..66329a2aff54b05ca2de2c79abb952f911d18aa1 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java @@ -1,10 +1,9 @@ package im.conversations.android.xmpp.model.pubsub.event; import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; @XmlElement -public class Purge extends Extension { +public class Purge extends Action { public Purge() { super(Purge.class); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java index 139a49522c48cb74aa9b4469768baf43305fb446..bbdf2fc497e441ba6ba2a191b885c88027591477 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java @@ -1,10 +1,9 @@ package im.conversations.android.xmpp.model.pubsub.event; import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; @XmlElement -public class Retract extends Extension { +public class Retract extends Action { public Retract() { super(Retract.class); diff --git a/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..dff10104a81fe596a0ff827ec8c7dc40b5af7b8e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.storage; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query", namespace = Namespace.PRIVATE_XML_STORAGE) +public class PrivateStorage extends Extension { + + public PrivateStorage() { + super(PrivateStorage.class); + } +}