OmemoManager.java
- /**
- *
- * 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<XMPPConnection, TreeMap<Integer, OmemoManager>> INSTANCES = new WeakHashMap<>();
- private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service;
- private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>();
- private final HashSet<OmemoMucMessageListener> 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<Integer, OmemoManager> 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<Integer, OmemoManager> 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<OmemoDevice> getDevicesOf(BareJid contact) throws IOException {
- OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact);
- HashSet<OmemoDevice> 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<BareJid> 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<BareJid> recipients, String message)
- throws CryptoFailedException, UndecidedOmemoIdentityException,
- InterruptedException, SmackException.NotConnectedException,
- SmackException.NoResponseException, SmackException.NotLoggedInException, IOException {
- LoggedInOmemoManager guard = new LoggedInOmemoManager(this);
- Set<OmemoDevice> 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<BareJid> 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<MessageOrOmemoMessage> 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 <code>true</code> 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 <code>true</code> 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<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact)
- throws SmackException.NotLoggedInException, CorruptedOmemoKeyException,
- CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException,
- SmackException.NoResponseException, IOException {
- if (getOwnJid() == null) {
- throw new SmackException.NotLoggedInException();
- }
- HashMap<OmemoDevice, OmemoFingerprint> 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<Exception> purgeEverything() throws NotConnectedException, InterruptedException, IOException {
- List<Exception> 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<OmemoDeviceListElement> pepOmemoDeviceListEventListener =
- (from, receivedDeviceList, id, message) -> {
- // Device List <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<Integer> storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid);
- if (storedDeviceIds.size() > 0) {
- manager.setDeviceId(storedDeviceIds.first());
- } else {
- manager.setDeviceId(randomDeviceId());
- }
- }
- }
- }