/** * * Copyright 2017 Paul Schaub, 2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.omemo; import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.MessageBuilder; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.Async; import org.jivesoftware.smackx.carbons.CarbonManager; import org.jivesoftware.smackx.carbons.packet.CarbonExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.hints.element.StoreHint; import org.jivesoftware.smackx.mam.MamManager; import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.muc.MultiUserChatManager; import org.jivesoftware.smackx.muc.RoomInfo; import org.jivesoftware.smackx.omemo.element.OmemoBundleElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement; import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl; import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; import org.jivesoftware.smackx.omemo.exceptions.NoOmemoSupportException; import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener; import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback; import org.jivesoftware.smackx.omemo.trust.TrustState; import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.pep.PepEventListener; import org.jivesoftware.smackx.pep.PepManager; import org.jivesoftware.smackx.pubsub.PubSubException; import org.jivesoftware.smackx.pubsub.PubSubManager; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; /** * Manager that allows sending messages encrypted with OMEMO. * This class also provides some methods useful for a client that implements OMEMO. * * @author Paul Schaub */ public final class OmemoManager extends Manager { private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName()); private static final Integer UNKNOWN_DEVICE_ID = -1; private static final WeakHashMap> INSTANCES = new WeakHashMap<>(); private final OmemoService service; private final HashSet omemoMessageListeners = new HashSet<>(); private final HashSet omemoMucMessageListeners = new HashSet<>(); private final PepManager pepManager; private OmemoTrustCallback trustCallback; private BareJid ownJid; private Integer deviceId; /** * Private constructor. * * @param connection connection * @param deviceId deviceId */ private OmemoManager(XMPPConnection connection, Integer deviceId) { super(connection); service = OmemoService.getInstance(); pepManager = PepManager.getInstanceFor(connection); this.deviceId = deviceId; if (connection.isAuthenticated()) { initBareJidAndDeviceId(this); } else { connection.addConnectionListener(new ConnectionListener() { @Override public void authenticated(XMPPConnection connection, boolean resumed) { initBareJidAndDeviceId(OmemoManager.this); } }); } service.registerRatchetForManager(this); // StanzaListeners resumeStanzaAndPEPListeners(); } /** * Return an OmemoManager instance for the given connection and deviceId. * If there was an OmemoManager for the connection and id before, return it. Otherwise create a new OmemoManager * instance and return it. * * @param connection XmppConnection. * @param deviceId MUST NOT be null and MUST be greater than 0. * * @return OmemoManager instance for the given connection and deviceId. */ public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) { if (deviceId == null || deviceId < 1) { throw new IllegalArgumentException("DeviceId MUST NOT be null and MUST be greater than 0."); } TreeMap managersOfConnection = INSTANCES.get(connection); if (managersOfConnection == null) { managersOfConnection = new TreeMap<>(); INSTANCES.put(connection, managersOfConnection); } OmemoManager manager = managersOfConnection.get(deviceId); if (manager == null) { manager = new OmemoManager(connection, deviceId); managersOfConnection.put(deviceId, manager); } return manager; } /** * Returns an OmemoManager instance for the given connection. If there was one manager for the connection before, * return it. If there were multiple managers before, return the one with the lowest deviceId. * If there was no manager before, return a new one. As soon as the connection gets authenticated, the manager * will look for local deviceIDs and select the lowest one as its id. If there are not local deviceIds, the manager * will assign itself a random id. * * @param connection XmppConnection. * * @return OmemoManager instance for the given connection and a determined deviceId. */ public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) { TreeMap managers = INSTANCES.get(connection); if (managers == null) { managers = new TreeMap<>(); INSTANCES.put(connection, managers); } OmemoManager manager; if (managers.size() == 0) { manager = new OmemoManager(connection, UNKNOWN_DEVICE_ID); managers.put(UNKNOWN_DEVICE_ID, manager); } else { manager = managers.get(managers.firstKey()); } return manager; } /** * Set a TrustCallback for this particular OmemoManager. * TrustCallbacks are used to query and modify trust decisions. * * @param callback trustCallback. */ public void setTrustCallback(OmemoTrustCallback callback) { if (trustCallback != null) { throw new IllegalStateException("TrustCallback can only be set once."); } trustCallback = callback; } /** * Return the TrustCallback of this manager. * * @return callback that is used for trust decisions. */ OmemoTrustCallback getTrustCallback() { return trustCallback; } /** * Initializes the OmemoManager. This method must be called before the manager can be used. * * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. * @throws IOException if an I/O error occurred. */ public synchronized void initialize() throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException, PubSubException.NotALeafNodeException, IOException { if (!connection().isAuthenticated()) { throw new SmackException.NotLoggedInException(); } if (getTrustCallback() == null) { throw new IllegalStateException("No TrustCallback set."); } getOmemoService().init(new LoggedInOmemoManager(this)); } /** * Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will * be notified. It will also get notified, if an error occurs. * * @param finishedCallback callback that gets called once the manager is initialized. */ public void initializeAsync(final InitializationFinishedCallback finishedCallback) { Async.go(new Runnable() { @Override public void run() { try { initialize(); finishedCallback.initializationFinished(OmemoManager.this); } catch (Exception e) { finishedCallback.initializationFailed(e); } } }); } /** * Return a set of all OMEMO capable devices of a contact. * Note, that this method does not explicitly refresh the device list of the contact, so it might be outdated. * * @see #requestDeviceListUpdateFor(BareJid) * * @param contact contact we want to get a set of device of. * @return set of known devices of that contact. * * @throws IOException if an I/O error occurred. */ public Set getDevicesOf(BareJid contact) throws IOException { OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact); HashSet devices = new HashSet<>(); for (int deviceId : list.getActiveDevices()) { devices.add(new OmemoDevice(contact, deviceId)); } return devices; } /** * OMEMO encrypt a cleartext message for a single recipient. * Note that this method does NOT set the 'to' attribute of the message. * * @param recipient recipients bareJid * @param message text to encrypt * @return encrypted message * * @throws CryptoFailedException when something crypto related fails * @throws UndecidedOmemoIdentityException When there are undecided devices * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws IOException if an I/O error occurred. */ public OmemoMessage.Sent encrypt(BareJid recipient, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException, IOException { Set recipients = new HashSet<>(); recipients.add(recipient); return encrypt(recipients, message); } /** * OMEMO encrypt a cleartext message for multiple recipients. * * @param recipients recipients barejids * @param message text to encrypt * @return encrypted message. * * @throws CryptoFailedException When something crypto related fails * @throws UndecidedOmemoIdentityException When there are undecided devices. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws IOException if an I/O error occurred. */ public synchronized OmemoMessage.Sent encrypt(Set recipients, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, InterruptedException, SmackException.NotConnectedException, SmackException.NoResponseException, SmackException.NotLoggedInException, IOException { LoggedInOmemoManager guard = new LoggedInOmemoManager(this); Set devices = getDevicesOf(getOwnJid()); for (BareJid recipient : recipients) { devices.addAll(getDevicesOf(recipient)); } return service.createOmemoMessage(guard, devices, message); } /** * Encrypt a message for all recipients in the MultiUserChat. * * @param muc multiUserChat * @param message message to send * @return encrypted message * * @throws UndecidedOmemoIdentityException when there are undecided devices. * @throws CryptoFailedException if the OMEMO cryptography failed. * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws NoOmemoSupportException When the muc doesn't support OMEMO. * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws IOException if an I/O error occurred. */ public synchronized OmemoMessage.Sent encrypt(MultiUserChat muc, String message) throws UndecidedOmemoIdentityException, CryptoFailedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, NoOmemoSupportException, SmackException.NotLoggedInException, IOException { if (!multiUserChatSupportsOmemo(muc)) { throw new NoOmemoSupportException(); } Set recipients = new HashSet<>(); for (EntityFullJid e : muc.getOccupants()) { recipients.add(muc.getOccupant(e).getJid().asBareJid()); } return encrypt(recipients, message); } /** * Manually decrypt an OmemoElement. * This method should only be used for use-cases, where the internal listeners don't pick up on an incoming message. * (for example MAM query results). * * @param sender bareJid of the message sender (must be the jid of the contact who sent the message) * @param omemoElement omemoElement * @return decrypted OmemoMessage * * @throws SmackException.NotLoggedInException if the Manager is not authenticated * @throws CorruptedOmemoKeyException if our or their key is corrupted * @throws NoRawSessionException if the message was not a preKeyMessage, but we had no session with the contact * @throws CryptoFailedException if decryption fails * @throws IOException if an I/O error occurred. */ public OmemoMessage.Received decrypt(BareJid sender, OmemoElement omemoElement) throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, NoRawSessionException, CryptoFailedException, IOException { LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this); return getOmemoService().decryptMessage(managerGuard, sender, omemoElement); } /** * Decrypt messages from a MAM query. * * @param mamQuery The MAM query * @return list of decrypted OmemoMessages * * @throws SmackException.NotLoggedInException if the Manager is not authenticated. * @throws IOException if an I/O error occurred. */ public List decryptMamQueryResult(MamManager.MamQuery mamQuery) throws SmackException.NotLoggedInException, IOException { return new ArrayList<>(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQuery)); } /** * Trust that a fingerprint belongs to an OmemoDevice. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must * be of length 64. * * @param device device * @param fingerprint fingerprint */ public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { if (trustCallback == null) { throw new IllegalStateException("No TrustCallback set."); } trustCallback.setTrust(device, fingerprint, TrustState.trusted); } /** * Distrust the fingerprint/OmemoDevice tuple. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must * be of length 64. * * @param device device * @param fingerprint fingerprint */ public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { if (trustCallback == null) { throw new IllegalStateException("No TrustCallback set."); } trustCallback.setTrust(device, fingerprint, TrustState.untrusted); } /** * Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must * be of length 64. * * @param device device * @param fingerprint fingerprint * @return true if this is a trusted OMEMO identity. */ public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { if (trustCallback == null) { throw new IllegalStateException("No TrustCallback set."); } return trustCallback.getTrust(device, fingerprint) == TrustState.trusted; } /** * Returns true, if the fingerprint/OmemoDevice tuple is decided by the user. * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must * be of length 64. * * @param device device * @param fingerprint fingerprint * @return true if the trust is decided for the identity. */ public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) { if (trustCallback == null) { throw new IllegalStateException("No TrustCallback set."); } return trustCallback.getTrust(device, fingerprint) != TrustState.undecided; } /** * Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward * secrecy. * * @param recipient recipient * * @throws CorruptedOmemoKeyException When the used identityKeys are corrupted * @throws CryptoFailedException When something fails with the crypto * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws NoSuchAlgorithmException if no such algorithm is available. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws IOException if an I/O error occurred. */ public synchronized void sendRatchetUpdateMessage(OmemoDevice recipient) throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException, CryptoFailedException, CannotEstablishOmemoSessionException, IOException { XMPPConnection connection = connection(); MessageBuilder message = connection.getStanzaFactory() .buildMessageStanza() .to(recipient.getJid()); OmemoElement element = getOmemoService().createRatchetUpdateElement(new LoggedInOmemoManager(this), recipient); message.addExtension(element); // Set MAM Storage hint StoreHint.set(message); connection.sendStanza(message.build()); } /** * Returns true, if the contact has any active devices published in a deviceList. * * @param contact contact * @return true if contact has at least one OMEMO capable device. * * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws IOException if an I/O error occurred. */ public synchronized boolean contactSupportsOmemo(BareJid contact) throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, IOException { OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); return !deviceList.getActiveDevices().isEmpty(); } /** * Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite * for OMEMO encryption in MUC). * * @param multiUserChat MUC * @return true if chat supports OMEMO * * @throws XMPPException.XMPPErrorException if there was an XMPP protocol level error * @throws SmackException.NotConnectedException if the connection is not connected * @throws InterruptedException if the thread is interrupted * @throws SmackException.NoResponseException if the server does not respond */ public boolean multiUserChatSupportsOmemo(MultiUserChat multiUserChat) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { EntityBareJid jid = multiUserChat.getRoom(); RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(jid); return roomInfo.isNonanonymous() && roomInfo.isMembersOnly(); } /** * Returns true, if the Server supports PEP. * * @param connection XMPPConnection * @param server domainBareJid of the server to test * @return true if server supports pep * * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. */ public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { return ServiceDiscoveryManager.getInstanceFor(connection) .discoverInfo(server).containsFeature(PubSub.NAMESPACE); } /** * Return the fingerprint of our identity key. * * @return our own OMEMO fingerprint * * @throws SmackException.NotLoggedInException if we don't know our bareJid yet. * @throws CorruptedOmemoKeyException if our identityKey is corrupted. * @throws IOException if an I/O error occurred. */ public synchronized OmemoFingerprint getOwnFingerprint() throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, IOException { if (getOwnJid() == null) { throw new SmackException.NotLoggedInException(); } return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice()); } /** * Get the fingerprint of a contacts device. * * @param device contacts OmemoDevice * @return fingerprint of the given OMEMO device. * * @throws CannotEstablishOmemoSessionException if we have no session yet, and are unable to create one. * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws CorruptedOmemoKeyException if the copy of the fingerprint we have is corrupted. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. */ public synchronized OmemoFingerprint getFingerprint(OmemoDevice device) throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException, CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException { if (getOwnJid() == null) { throw new SmackException.NotLoggedInException(); } if (device.equals(getOwnDevice())) { return getOwnFingerprint(); } return getOmemoService().getOmemoStoreBackend() .getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device); } /** * Return all OmemoFingerprints of active devices of a contact. * TODO: Make more fail-safe * * @param contact contact * @return Map of all active devices of the contact and their fingerprints. * * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted. * @throws CannotEstablishOmemoSessionException if no OMEMO session could be established. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. */ public synchronized HashMap getActiveFingerprints(BareJid contact) throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException { if (getOwnJid() == null) { throw new SmackException.NotLoggedInException(); } HashMap fingerprints = new HashMap<>(); OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact); for (int id : deviceList.getActiveDevices()) { OmemoDevice device = new OmemoDevice(contact, id); OmemoFingerprint fingerprint = getFingerprint(device); if (fingerprint != null) { fingerprints.put(device, fingerprint); } } return fingerprints; } /** * Add an OmemoMessageListener. This listener will be informed about incoming OMEMO messages * (as well as KeyTransportMessages) and OMEMO encrypted message carbons. * * @param listener OmemoMessageListener */ public void addOmemoMessageListener(OmemoMessageListener listener) { omemoMessageListeners.add(listener); } /** * Remove an OmemoMessageListener. * * @param listener OmemoMessageListener */ public void removeOmemoMessageListener(OmemoMessageListener listener) { omemoMessageListeners.remove(listener); } /** * Add an OmemoMucMessageListener. This listener will be informed about incoming OMEMO encrypted MUC messages. * * @param listener OmemoMessageListener. */ public void addOmemoMucMessageListener(OmemoMucMessageListener listener) { omemoMucMessageListeners.add(listener); } /** * Remove an OmemoMucMessageListener. * * @param listener OmemoMucMessageListener */ public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) { omemoMucMessageListeners.remove(listener); } /** * Request a deviceList update from contact contact. * * @param contact contact we want to obtain the deviceList from. * * @throws InterruptedException if the calling thread was interrupted. * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. */ public synchronized void requestDeviceListUpdateFor(BareJid contact) throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, IOException { getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact); } /** * Publish a new device list with just our own deviceId in it. * * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws InterruptedException if the calling thread was interrupted. * @throws XMPPException.XMPPErrorException if there was an XMPP error returned. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws IOException if an I/O error occurred. * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. */ public void purgeDeviceList() throws SmackException.NotLoggedInException, InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, IOException, PubSubException.NotALeafNodeException { getOmemoService().purgeDeviceList(new LoggedInOmemoManager(this)); } public List purgeEverything() throws NotConnectedException, InterruptedException, IOException { List exceptions = new ArrayList<>(5); PubSubManager pm = PubSubManager.getInstanceFor(getConnection(), getOwnJid()); try { requestDeviceListUpdateFor(getOwnJid()); } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException e) { exceptions.add(e); } OmemoCachedDeviceList deviceList = OmemoService.getInstance().getOmemoStoreBackend() .loadCachedDeviceList(getOwnDevice(), getOwnJid()); for (int id : deviceList.getAllDevices()) { try { pm.getLeafNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)).deleteAllItems(); } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { exceptions.add(e); } try { pm.deleteNode(OmemoConstants.PEP_NODE_BUNDLE_FROM_DEVICE_ID(id)); } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { exceptions.add(e); } } try { pm.getLeafNode(OmemoConstants.PEP_NODE_DEVICE_LIST).deleteAllItems(); } catch (SmackException.NoResponseException | PubSubException.NotALeafNodeException | XMPPException.XMPPErrorException | PubSubException.NotAPubSubNodeException e) { exceptions.add(e); } try { pm.deleteNode(OmemoConstants.PEP_NODE_DEVICE_LIST); } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException e) { exceptions.add(e); } return exceptions; } /** * Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and * then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption * of messages that have been sent since the key was changed. * * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged. * @throws InterruptedException XMPP error * @throws XMPPException.XMPPErrorException XMPP error * @throws SmackException.NotConnectedException XMPP error * @throws SmackException.NoResponseException XMPP error * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated. * @throws IOException if an I/O error occurred. * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node. */ public synchronized void rotateSignedPreKey() throws CorruptedOmemoKeyException, SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, IOException, PubSubException.NotALeafNodeException { if (!connection().isAuthenticated()) { throw new SmackException.NotLoggedInException(); } // generate key getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice()); // publish OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice()); OmemoService.publishBundle(connection(), getOwnDevice(), bundle); } /** * Return true, if the given Stanza contains an OMEMO element 'encrypted'. * * @param stanza stanza * @return true if stanza has extension 'encrypted' */ static boolean stanzaContainsOmemoElement(Stanza stanza) { return stanza.hasExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL); } /** * Throw an IllegalStateException if no OmemoService is set. */ private void throwIfNoServiceSet() { if (service == null) { throw new IllegalStateException("No OmemoService set in OmemoManager."); } } /** * Returns a pseudo random number from the interval [1, Integer.MAX_VALUE]. * * @return a random deviceId. */ public static int randomDeviceId() { return new Random().nextInt(Integer.MAX_VALUE - 1) + 1; } /** * Return the BareJid of the user. * * @return our own bare JID. */ public BareJid getOwnJid() { if (ownJid == null && connection().isAuthenticated()) { ownJid = connection().getUser().asBareJid(); } return ownJid; } /** * Return the deviceId of this OmemoManager. * * @return this OmemoManagers deviceId. */ public synchronized Integer getDeviceId() { return deviceId; } /** * Return the OmemoDevice of the user. * * @return our own OmemoDevice */ public synchronized OmemoDevice getOwnDevice() { BareJid jid = getOwnJid(); if (jid == null) { return null; } return new OmemoDevice(jid, getDeviceId()); } /** * Set the deviceId of the manager to nDeviceId. * * @param nDeviceId new deviceId */ synchronized void setDeviceId(int nDeviceId) { // Move this instance inside the HashMaps INSTANCES.get(connection()).remove(getDeviceId()); INSTANCES.get(connection()).put(nDeviceId, this); this.deviceId = nDeviceId; } /** * Notify all registered OmemoMessageListeners about a received OmemoMessage. * * @param stanza original stanza * @param decryptedMessage decrypted OmemoMessage. */ void notifyOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) { for (OmemoMessageListener l : omemoMessageListeners) { l.onOmemoMessageReceived(stanza, decryptedMessage); } } /** * Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC. * * @param muc MultiUserChat the message was received in. * @param stanza Original Stanza. * @param decryptedMessage Decrypted OmemoMessage. */ void notifyOmemoMucMessageReceived(MultiUserChat muc, Stanza stanza, OmemoMessage.Received decryptedMessage) { for (OmemoMucMessageListener l : omemoMucMessageListeners) { l.onOmemoMucMessageReceived(muc, stanza, decryptedMessage); } } /** * Notify all registered OmemoMessageListeners of an incoming OMEMO encrypted Carbon Copy. * Remember: If you want to receive OMEMO encrypted carbon copies, you have to enable carbons using * {@link CarbonManager#enableCarbons()}. * * @param direction direction of the carbon copy * @param carbonCopy carbon copy itself * @param wrappingMessage wrapping message * @param decryptedCarbonCopy decrypted carbon copy OMEMO element */ void notifyOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoMessage.Received decryptedCarbonCopy) { for (OmemoMessageListener l : omemoMessageListeners) { l.onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decryptedCarbonCopy); } } /** * Register stanza listeners needed for OMEMO. * This method is called automatically in the constructor and should only be used to restore the previous state * after {@link #stopStanzaAndPEPListeners()} was called. */ public void resumeStanzaAndPEPListeners() { CarbonManager carbonManager = CarbonManager.getInstanceFor(connection()); // Remove listeners to avoid them getting added twice connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener); carbonManager.removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); // Add listeners pepManager.addPepEventListener(OmemoConstants.PEP_NODE_DEVICE_LIST, OmemoDeviceListElement.class, pepOmemoDeviceListEventListener); connection().addAsyncStanzaListener(this::internalOmemoMessageStanzaListener, OmemoManager::isOmemoMessage); carbonManager.addCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); } /** * Remove active stanza listeners needed for OMEMO. */ public void stopStanzaAndPEPListeners() { pepManager.removePepEventListener(pepOmemoDeviceListEventListener); connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener); CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener); } /** * Build a fresh session with a contacts device. * This might come in handy if a session is broken. * * @param contactsDevice OmemoDevice of a contact. * * @throws InterruptedException if the calling thread was interrupted. * @throws SmackException.NoResponseException if there was no response from the remote entity. * @throws CorruptedOmemoKeyException if our or their identityKey is corrupted. * @throws SmackException.NotConnectedException if the XMPP connection is not connected. * @throws CannotEstablishOmemoSessionException if no new session can be established. * @throws SmackException.NotLoggedInException if the connection is not authenticated. */ public void rebuildSessionWith(OmemoDevice contactsDevice) throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException, SmackException.NotConnectedException, CannotEstablishOmemoSessionException, SmackException.NotLoggedInException { if (!connection().isAuthenticated()) { throw new SmackException.NotLoggedInException(); } getOmemoService().buildFreshSessionWithDevice(connection(), getOwnDevice(), contactsDevice); } /** * Get our connection. * * @return the connection of this manager */ XMPPConnection getConnection() { return connection(); } /** * Return the OMEMO service object. * * @return the OmemoService object related to this OmemoManager. */ OmemoService getOmemoService() { throwIfNoServiceSet(); return service; } /** * StanzaListener that listens for incoming Stanzas which contain OMEMO elements. */ private void internalOmemoMessageStanzaListener(final Stanza packet) { Async.go(new Runnable() { @Override public void run() { try { getOmemoService().onOmemoMessageStanzaReceived(packet, new LoggedInOmemoManager(OmemoManager.this)); } catch (SmackException.NotLoggedInException | IOException e) { LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e); } } }); } /** * CarbonCopyListener that listens for incoming carbon copies which contain OMEMO elements. */ private void internalOmemoCarbonCopyListener(final CarbonExtension.Direction direction, final Message carbonCopy, final Message wrappingMessage) { Async.go(new Runnable() { @Override public void run() { if (isOmemoMessage(carbonCopy)) { try { getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, new LoggedInOmemoManager(OmemoManager.this)); } catch (SmackException.NotLoggedInException | IOException e) { LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e); } } } }); } @SuppressWarnings("UnnecessaryLambda") private final PepEventListener pepOmemoDeviceListEventListener = (from, receivedDeviceList, id, message) -> { // Device List OmemoCachedDeviceList deviceList; try { getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(getOwnDevice(), from, receivedDeviceList); if (!from.asBareJid().equals(getOwnJid())) { return; } deviceList = getOmemoService().cleanUpDeviceList(getOwnDevice()); } catch (IOException e) { LOGGER.log(Level.SEVERE, "IOException while processing OMEMO PEP device updates. Message: " + message, e); return; } final OmemoDeviceListElement_VAxolotl newDeviceList = new OmemoDeviceListElement_VAxolotl(deviceList); if (!newDeviceList.copyDeviceIds().equals(receivedDeviceList.copyDeviceIds())) { LOGGER.log(Level.FINE, "Republish deviceList due to changes:" + " Received: " + Arrays.toString(receivedDeviceList.copyDeviceIds().toArray()) + " Published: " + Arrays.toString(newDeviceList.copyDeviceIds().toArray())); Async.go(new Runnable() { @Override public void run() { try { OmemoService.publishDeviceList(connection(), newDeviceList); } catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException e) { LOGGER.log(Level.WARNING, "Could not publish our deviceList upon an received update.", e); } } }); } }; /** * StanzaFilter that filters messages containing a OMEMO element. */ private static boolean isOmemoMessage(Stanza stanza) { return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza); } /** * Guard class which ensures that the wrapped OmemoManager knows its BareJid. */ public static class LoggedInOmemoManager { private final OmemoManager manager; public LoggedInOmemoManager(OmemoManager manager) throws SmackException.NotLoggedInException { if (manager == null) { throw new IllegalArgumentException("OmemoManager cannot be null."); } if (manager.getOwnJid() == null) { if (manager.getConnection().isAuthenticated()) { manager.ownJid = manager.getConnection().getUser().asBareJid(); } else { throw new SmackException.NotLoggedInException(); } } this.manager = manager; } public OmemoManager get() { return manager; } } /** * Callback which can be used to get notified, when the OmemoManager finished initializing. */ public interface InitializationFinishedCallback { void initializationFinished(OmemoManager manager); void initializationFailed(Exception cause); } /** * Get the bareJid of the user from the authenticated XMPP connection. * If our deviceId is unknown, use the bareJid to look up deviceIds available in the omemoStore. * If there are ids available, choose the smallest one. Otherwise generate a random deviceId. * * @param manager OmemoManager */ private static void initBareJidAndDeviceId(OmemoManager manager) { if (!manager.getConnection().isAuthenticated()) { throw new IllegalStateException("Connection MUST be authenticated."); } if (manager.ownJid == null) { manager.ownJid = manager.getConnection().getUser().asBareJid(); } if (UNKNOWN_DEVICE_ID.equals(manager.deviceId)) { SortedSet storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid); if (storedDeviceIds.size() > 0) { manager.setDeviceId(storedDeviceIds.first()); } else { manager.setDeviceId(randomDeviceId()); } } } }