001/**
002 *
003 * Copyright 2017 Paul Schaub, 2020 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.omemo;
018
019import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
020
021import java.io.IOException;
022import java.security.NoSuchAlgorithmException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Random;
029import java.util.Set;
030import java.util.SortedSet;
031import java.util.TreeMap;
032import java.util.WeakHashMap;
033import java.util.logging.Level;
034import java.util.logging.Logger;
035
036import org.jivesoftware.smack.ConnectionListener;
037import org.jivesoftware.smack.Manager;
038import org.jivesoftware.smack.SmackException;
039import org.jivesoftware.smack.XMPPConnection;
040import org.jivesoftware.smack.XMPPException;
041import org.jivesoftware.smack.packet.Message;
042import org.jivesoftware.smack.packet.MessageBuilder;
043import org.jivesoftware.smack.packet.Stanza;
044import org.jivesoftware.smack.util.Async;
045
046import org.jivesoftware.smackx.carbons.CarbonManager;
047import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
048import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
049import org.jivesoftware.smackx.hints.element.StoreHint;
050import org.jivesoftware.smackx.mam.MamManager;
051import org.jivesoftware.smackx.muc.MultiUserChat;
052import org.jivesoftware.smackx.muc.MultiUserChatManager;
053import org.jivesoftware.smackx.muc.RoomInfo;
054import org.jivesoftware.smackx.omemo.element.OmemoBundleElement;
055import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement;
056import org.jivesoftware.smackx.omemo.element.OmemoDeviceListElement_VAxolotl;
057import org.jivesoftware.smackx.omemo.element.OmemoElement;
058import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
059import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
060import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
061import org.jivesoftware.smackx.omemo.exceptions.NoOmemoSupportException;
062import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
063import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
064import org.jivesoftware.smackx.omemo.internal.OmemoCachedDeviceList;
065import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
066import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
067import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener;
068import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint;
069import org.jivesoftware.smackx.omemo.trust.OmemoTrustCallback;
070import org.jivesoftware.smackx.omemo.trust.TrustState;
071import org.jivesoftware.smackx.omemo.util.MessageOrOmemoMessage;
072import org.jivesoftware.smackx.omemo.util.OmemoConstants;
073import org.jivesoftware.smackx.pep.PepEventListener;
074import org.jivesoftware.smackx.pep.PepManager;
075import org.jivesoftware.smackx.pubsub.PubSubException;
076import org.jivesoftware.smackx.pubsub.packet.PubSub;
077
078import org.jxmpp.jid.BareJid;
079import org.jxmpp.jid.DomainBareJid;
080import org.jxmpp.jid.EntityBareJid;
081import org.jxmpp.jid.EntityFullJid;
082
083/**
084 * Manager that allows sending messages encrypted with OMEMO.
085 * This class also provides some methods useful for a client that implements OMEMO.
086 *
087 * @author Paul Schaub
088 */
089
090public final class OmemoManager extends Manager {
091    private static final Logger LOGGER = Logger.getLogger(OmemoManager.class.getName());
092
093    private static final Integer UNKNOWN_DEVICE_ID = -1;
094
095    private static final WeakHashMap<XMPPConnection, TreeMap<Integer, OmemoManager>> INSTANCES = new WeakHashMap<>();
096    private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service;
097
098    private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>();
099    private final HashSet<OmemoMucMessageListener> omemoMucMessageListeners = new HashSet<>();
100
101    private final PepManager pepManager;
102
103    private OmemoTrustCallback trustCallback;
104
105    private BareJid ownJid;
106    private Integer deviceId;
107
108    /**
109     * Private constructor.
110     *
111     * @param connection connection
112     * @param deviceId deviceId
113     */
114    private OmemoManager(XMPPConnection connection, Integer deviceId) {
115        super(connection);
116
117        service = OmemoService.getInstance();
118        pepManager = PepManager.getInstanceFor(connection);
119
120        this.deviceId = deviceId;
121
122        if (connection.isAuthenticated()) {
123            initBareJidAndDeviceId(this);
124        } else {
125            connection.addConnectionListener(new ConnectionListener() {
126                @Override
127                public void authenticated(XMPPConnection connection, boolean resumed) {
128                    initBareJidAndDeviceId(OmemoManager.this);
129                }
130            });
131        }
132
133        service.registerRatchetForManager(this);
134
135        // StanzaListeners
136        resumeStanzaAndPEPListeners();
137    }
138
139    /**
140     * Return an OmemoManager instance for the given connection and deviceId.
141     * If there was an OmemoManager for the connection and id before, return it. Otherwise create a new OmemoManager
142     * instance and return it.
143     *
144     * @param connection XmppConnection.
145     * @param deviceId MUST NOT be null and MUST be greater than 0.
146     *
147     * @return OmemoManager instance for the given connection and deviceId.
148     */
149    public static synchronized OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) {
150        if (deviceId == null || deviceId < 1) {
151            throw new IllegalArgumentException("DeviceId MUST NOT be null and MUST be greater than 0.");
152        }
153
154        TreeMap<Integer, OmemoManager> managersOfConnection = INSTANCES.get(connection);
155        if (managersOfConnection == null) {
156            managersOfConnection = new TreeMap<>();
157            INSTANCES.put(connection, managersOfConnection);
158        }
159
160        OmemoManager manager = managersOfConnection.get(deviceId);
161        if (manager == null) {
162            manager = new OmemoManager(connection, deviceId);
163            managersOfConnection.put(deviceId, manager);
164        }
165
166        return manager;
167    }
168
169    /**
170     * Returns an OmemoManager instance for the given connection. If there was one manager for the connection before,
171     * return it. If there were multiple managers before, return the one with the lowest deviceId.
172     * If there was no manager before, return a new one. As soon as the connection gets authenticated, the manager
173     * will look for local deviceIDs and select the lowest one as its id. If there are not local deviceIds, the manager
174     * will assign itself a random id.
175     *
176     * @param connection XmppConnection.
177     *
178     * @return OmemoManager instance for the given connection and a determined deviceId.
179     */
180    public static synchronized OmemoManager getInstanceFor(XMPPConnection connection) {
181        TreeMap<Integer, OmemoManager> managers = INSTANCES.get(connection);
182        if (managers == null) {
183            managers = new TreeMap<>();
184            INSTANCES.put(connection, managers);
185        }
186
187        OmemoManager manager;
188        if (managers.size() == 0) {
189
190            manager = new OmemoManager(connection, UNKNOWN_DEVICE_ID);
191            managers.put(UNKNOWN_DEVICE_ID, manager);
192
193        } else {
194            manager = managers.get(managers.firstKey());
195        }
196
197        return manager;
198    }
199
200    /**
201     * Set a TrustCallback for this particular OmemoManager.
202     * TrustCallbacks are used to query and modify trust decisions.
203     *
204     * @param callback trustCallback.
205     */
206    public void setTrustCallback(OmemoTrustCallback callback) {
207        if (trustCallback != null) {
208            throw new IllegalStateException("TrustCallback can only be set once.");
209        }
210        trustCallback = callback;
211    }
212
213    /**
214     * Return the TrustCallback of this manager.
215     *
216     * @return callback that is used for trust decisions.
217     */
218    OmemoTrustCallback getTrustCallback() {
219        return trustCallback;
220    }
221
222    /**
223     * Initializes the OmemoManager. This method must be called before the manager can be used.
224     *
225     * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted.
226     * @throws InterruptedException if the calling thread was interrupted.
227     * @throws SmackException.NoResponseException if there was no response from the remote entity.
228     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
229     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
230     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
231     * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
232     * @throws IOException if an I/O error occurred.
233     */
234    public synchronized void initialize()
235            throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException,
236            SmackException.NoResponseException, SmackException.NotConnectedException, XMPPException.XMPPErrorException,
237            PubSubException.NotALeafNodeException, IOException {
238        if (!connection().isAuthenticated()) {
239            throw new SmackException.NotLoggedInException();
240        }
241
242        if (getTrustCallback() == null) {
243            throw new IllegalStateException("No TrustCallback set.");
244        }
245
246        getOmemoService().init(new LoggedInOmemoManager(this));
247    }
248
249    /**
250     * Initialize the manager without blocking. Once the manager is successfully initialized, the finishedCallback will
251     * be notified. It will also get notified, if an error occurs.
252     *
253     * @param finishedCallback callback that gets called once the manager is initialized.
254     */
255    public void initializeAsync(final InitializationFinishedCallback finishedCallback) {
256        Async.go(new Runnable() {
257            @Override
258            public void run() {
259                try {
260                    initialize();
261                    finishedCallback.initializationFinished(OmemoManager.this);
262                } catch (Exception e) {
263                    finishedCallback.initializationFailed(e);
264                }
265            }
266        });
267    }
268
269    /**
270     * Return a set of all OMEMO capable devices of a contact.
271     * Note, that this method does not explicitly refresh the device list of the contact, so it might be outdated.
272     *
273     * @see #requestDeviceListUpdateFor(BareJid)
274     *
275     * @param contact contact we want to get a set of device of.
276     * @return set of known devices of that contact.
277     *
278     * @throws IOException if an I/O error occurred.
279     */
280    public Set<OmemoDevice> getDevicesOf(BareJid contact) throws IOException {
281        OmemoCachedDeviceList list = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(), contact);
282        HashSet<OmemoDevice> devices = new HashSet<>();
283
284        for (int deviceId : list.getActiveDevices()) {
285            devices.add(new OmemoDevice(contact, deviceId));
286        }
287
288        return devices;
289    }
290
291    /**
292     * OMEMO encrypt a cleartext message for a single recipient.
293     * Note that this method does NOT set the 'to' attribute of the message.
294     *
295     * @param recipient recipients bareJid
296     * @param message text to encrypt
297     * @return encrypted message
298     *
299     * @throws CryptoFailedException                when something crypto related fails
300     * @throws UndecidedOmemoIdentityException      When there are undecided devices
301     * @throws InterruptedException if the calling thread was interrupted.
302     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
303     * @throws SmackException.NoResponseException if there was no response from the remote entity.
304     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
305     * @throws IOException if an I/O error occurred.
306     */
307    public OmemoMessage.Sent encrypt(BareJid recipient, String message)
308            throws CryptoFailedException, UndecidedOmemoIdentityException,
309            InterruptedException, SmackException.NotConnectedException,
310            SmackException.NoResponseException, SmackException.NotLoggedInException, IOException {
311        Set<BareJid> recipients = new HashSet<>();
312        recipients.add(recipient);
313        return encrypt(recipients, message);
314    }
315
316    /**
317     * OMEMO encrypt a cleartext message for multiple recipients.
318     *
319     * @param recipients recipients barejids
320     * @param message text to encrypt
321     * @return encrypted message.
322     *
323     * @throws CryptoFailedException    When something crypto related fails
324     * @throws UndecidedOmemoIdentityException  When there are undecided devices.
325     * @throws InterruptedException if the calling thread was interrupted.
326     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
327     * @throws SmackException.NoResponseException if there was no response from the remote entity.
328     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
329     * @throws IOException if an I/O error occurred.
330     */
331    public synchronized OmemoMessage.Sent encrypt(Set<BareJid> recipients, String message)
332            throws CryptoFailedException, UndecidedOmemoIdentityException,
333            InterruptedException, SmackException.NotConnectedException,
334            SmackException.NoResponseException, SmackException.NotLoggedInException, IOException {
335        LoggedInOmemoManager guard = new LoggedInOmemoManager(this);
336        Set<OmemoDevice> devices = getDevicesOf(getOwnJid());
337        for (BareJid recipient : recipients) {
338            devices.addAll(getDevicesOf(recipient));
339        }
340        return service.createOmemoMessage(guard, devices, message);
341    }
342
343    /**
344     * Encrypt a message for all recipients in the MultiUserChat.
345     *
346     * @param muc multiUserChat
347     * @param message message to send
348     * @return encrypted message
349     *
350     * @throws UndecidedOmemoIdentityException when there are undecided devices.
351     * @throws CryptoFailedException if the OMEMO cryptography failed.
352     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
353     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
354     * @throws InterruptedException if the calling thread was interrupted.
355     * @throws SmackException.NoResponseException if there was no response from the remote entity.
356     * @throws NoOmemoSupportException When the muc doesn't support OMEMO.
357     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
358     * @throws IOException if an I/O error occurred.
359     */
360    public synchronized OmemoMessage.Sent encrypt(MultiUserChat muc, String message)
361            throws UndecidedOmemoIdentityException, CryptoFailedException,
362            XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
363            SmackException.NoResponseException, NoOmemoSupportException,
364            SmackException.NotLoggedInException, IOException {
365        if (!multiUserChatSupportsOmemo(muc)) {
366            throw new NoOmemoSupportException();
367        }
368
369        Set<BareJid> recipients = new HashSet<>();
370
371        for (EntityFullJid e : muc.getOccupants()) {
372            recipients.add(muc.getOccupant(e).getJid().asBareJid());
373        }
374        return encrypt(recipients, message);
375    }
376
377    /**
378     * Manually decrypt an OmemoElement.
379     * This method should only be used for use-cases, where the internal listeners don't pick up on an incoming message.
380     * (for example MAM query results).
381     *
382     * @param sender bareJid of the message sender (must be the jid of the contact who sent the message)
383     * @param omemoElement omemoElement
384     * @return decrypted OmemoMessage
385     *
386     * @throws SmackException.NotLoggedInException if the Manager is not authenticated
387     * @throws CorruptedOmemoKeyException if our or their key is corrupted
388     * @throws NoRawSessionException if the message was not a preKeyMessage, but we had no session with the contact
389     * @throws CryptoFailedException if decryption fails
390     * @throws IOException if an I/O error occurred.
391     */
392    public OmemoMessage.Received decrypt(BareJid sender, OmemoElement omemoElement)
393            throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, NoRawSessionException,
394            CryptoFailedException, IOException {
395        LoggedInOmemoManager managerGuard = new LoggedInOmemoManager(this);
396        return getOmemoService().decryptMessage(managerGuard, sender, omemoElement);
397    }
398
399    /**
400     * Decrypt messages from a MAM query.
401     *
402     * @param mamQuery The MAM query
403     * @return list of decrypted OmemoMessages
404     *
405     * @throws SmackException.NotLoggedInException if the Manager is not authenticated.
406     * @throws IOException if an I/O error occurred.
407     */
408    public List<MessageOrOmemoMessage> decryptMamQueryResult(MamManager.MamQuery mamQuery)
409            throws SmackException.NotLoggedInException, IOException {
410        return new ArrayList<>(getOmemoService().decryptMamQueryResult(new LoggedInOmemoManager(this), mamQuery));
411    }
412
413    /**
414     * Trust that a fingerprint belongs to an OmemoDevice.
415     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
416     * be of length 64.
417     *
418     * @param device device
419     * @param fingerprint fingerprint
420     */
421    public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
422        if (trustCallback == null) {
423            throw new IllegalStateException("No TrustCallback set.");
424        }
425
426        trustCallback.setTrust(device, fingerprint, TrustState.trusted);
427    }
428
429    /**
430     * Distrust the fingerprint/OmemoDevice tuple.
431     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
432     * be of length 64.
433     *
434     * @param device device
435     * @param fingerprint fingerprint
436     */
437    public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
438        if (trustCallback == null) {
439            throw new IllegalStateException("No TrustCallback set.");
440        }
441
442        trustCallback.setTrust(device, fingerprint, TrustState.untrusted);
443    }
444
445    /**
446     * Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false.
447     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
448     * be of length 64.
449     *
450     * @param device device
451     * @param fingerprint fingerprint
452     * @return <code>true</code> if this is a trusted OMEMO identity.
453     */
454    public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
455        if (trustCallback == null) {
456            throw new IllegalStateException("No TrustCallback set.");
457        }
458
459        return trustCallback.getTrust(device, fingerprint) == TrustState.trusted;
460    }
461
462    /**
463     * Returns true, if the fingerprint/OmemoDevice tuple is decided by the user.
464     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
465     * be of length 64.
466     *
467     * @param device device
468     * @param fingerprint fingerprint
469     * @return <code>true</code> if the trust is decided for the identity.
470     */
471    public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
472        if (trustCallback == null) {
473            throw new IllegalStateException("No TrustCallback set.");
474        }
475
476        return trustCallback.getTrust(device, fingerprint) != TrustState.undecided;
477    }
478
479    /**
480     * Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward
481     * secrecy.
482     *
483     * @param recipient recipient
484     *
485     * @throws CorruptedOmemoKeyException           When the used identityKeys are corrupted
486     * @throws CryptoFailedException                When something fails with the crypto
487     * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
488     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
489     * @throws InterruptedException if the calling thread was interrupted.
490     * @throws SmackException.NoResponseException if there was no response from the remote entity.
491     * @throws NoSuchAlgorithmException if no such algorithm is available.
492     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
493     * @throws IOException if an I/O error occurred.
494     */
495    public synchronized void sendRatchetUpdateMessage(OmemoDevice recipient)
496            throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, InterruptedException,
497            SmackException.NoResponseException, NoSuchAlgorithmException, SmackException.NotConnectedException,
498            CryptoFailedException, CannotEstablishOmemoSessionException, IOException {
499        XMPPConnection connection = connection();
500        MessageBuilder message = connection.getStanzaFactory()
501                .buildMessageStanza()
502                .to(recipient.getJid());
503
504        OmemoElement element = getOmemoService().createRatchetUpdateElement(new LoggedInOmemoManager(this), recipient);
505        message.addExtension(element);
506
507        // Set MAM Storage hint
508        StoreHint.set(message);
509        connection.sendStanza(message.build());
510    }
511
512    /**
513     * Returns true, if the contact has any active devices published in a deviceList.
514     *
515     * @param contact contact
516     * @return true if contact has at least one OMEMO capable device.
517     *
518     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
519     * @throws InterruptedException if the calling thread was interrupted.
520     * @throws SmackException.NoResponseException if there was no response from the remote entity.
521     * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
522     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
523     * @throws IOException if an I/O error occurred.
524     */
525    public synchronized boolean contactSupportsOmemo(BareJid contact)
526            throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
527            SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
528        OmemoCachedDeviceList deviceList = getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact);
529        return !deviceList.getActiveDevices().isEmpty();
530    }
531
532    /**
533     * Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite
534     * for OMEMO encryption in MUC).
535     *
536     * @param multiUserChat MUC
537     * @return true if chat supports OMEMO
538     *
539     * @throws XMPPException.XMPPErrorException     if there was an XMPP protocol level error
540     * @throws SmackException.NotConnectedException if the connection is not connected
541     * @throws InterruptedException                 if the thread is interrupted
542     * @throws SmackException.NoResponseException   if the server does not respond
543     */
544    public boolean multiUserChatSupportsOmemo(MultiUserChat multiUserChat)
545            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
546            SmackException.NoResponseException {
547        EntityBareJid jid = multiUserChat.getRoom();
548        RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(jid);
549        return roomInfo.isNonanonymous() && roomInfo.isMembersOnly();
550    }
551
552    /**
553     * Returns true, if the Server supports PEP.
554     *
555     * @param connection XMPPConnection
556     * @param server domainBareJid of the server to test
557     * @return true if server supports pep
558     *
559     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
560     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
561     * @throws InterruptedException if the calling thread was interrupted.
562     * @throws SmackException.NoResponseException if there was no response from the remote entity.
563     */
564    public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server)
565            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
566            SmackException.NoResponseException {
567        return ServiceDiscoveryManager.getInstanceFor(connection)
568                .discoverInfo(server).containsFeature(PubSub.NAMESPACE);
569    }
570
571    /**
572     * Return the fingerprint of our identity key.
573     *
574     * @return our own OMEMO fingerprint
575     *
576     * @throws SmackException.NotLoggedInException if we don't know our bareJid yet.
577     * @throws CorruptedOmemoKeyException if our identityKey is corrupted.
578     * @throws IOException if an I/O error occurred.
579     */
580    public synchronized OmemoFingerprint getOwnFingerprint()
581            throws SmackException.NotLoggedInException, CorruptedOmemoKeyException, IOException {
582        if (getOwnJid() == null) {
583            throw new SmackException.NotLoggedInException();
584        }
585
586        return getOmemoService().getOmemoStoreBackend().getFingerprint(getOwnDevice());
587    }
588
589    /**
590     * Get the fingerprint of a contacts device.
591     *
592     * @param device contacts OmemoDevice
593     * @return fingerprint of the given OMEMO device.
594     *
595     * @throws CannotEstablishOmemoSessionException if we have no session yet, and are unable to create one.
596     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
597     * @throws CorruptedOmemoKeyException if the copy of the fingerprint we have is corrupted.
598     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
599     * @throws InterruptedException if the calling thread was interrupted.
600     * @throws SmackException.NoResponseException if there was no response from the remote entity.
601     * @throws IOException if an I/O error occurred.
602     */
603    public synchronized OmemoFingerprint getFingerprint(OmemoDevice device)
604            throws CannotEstablishOmemoSessionException, SmackException.NotLoggedInException,
605            CorruptedOmemoKeyException, SmackException.NotConnectedException, InterruptedException,
606            SmackException.NoResponseException, IOException {
607        if (getOwnJid() == null) {
608            throw new SmackException.NotLoggedInException();
609        }
610
611        if (device.equals(getOwnDevice())) {
612            return getOwnFingerprint();
613        }
614
615        return getOmemoService().getOmemoStoreBackend()
616                .getFingerprintAndMaybeBuildSession(new LoggedInOmemoManager(this), device);
617    }
618
619    /**
620     * Return all OmemoFingerprints of active devices of a contact.
621     * TODO: Make more fail-safe
622     *
623     * @param contact contact
624     * @return Map of all active devices of the contact and their fingerprints.
625     *
626     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
627     * @throws CorruptedOmemoKeyException if the OMEMO key is corrupted.
628     * @throws CannotEstablishOmemoSessionException if no OMEMO session could be established.
629     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
630     * @throws InterruptedException if the calling thread was interrupted.
631     * @throws SmackException.NoResponseException if there was no response from the remote entity.
632     * @throws IOException if an I/O error occurred.
633     */
634    public synchronized HashMap<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact)
635            throws SmackException.NotLoggedInException, CorruptedOmemoKeyException,
636            CannotEstablishOmemoSessionException, SmackException.NotConnectedException, InterruptedException,
637            SmackException.NoResponseException, IOException {
638        if (getOwnJid() == null) {
639            throw new SmackException.NotLoggedInException();
640        }
641
642        HashMap<OmemoDevice, OmemoFingerprint> fingerprints = new HashMap<>();
643        OmemoCachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(getOwnDevice(),
644                contact);
645
646        for (int id : deviceList.getActiveDevices()) {
647            OmemoDevice device = new OmemoDevice(contact, id);
648            OmemoFingerprint fingerprint = getFingerprint(device);
649
650            if (fingerprint != null) {
651                fingerprints.put(device, fingerprint);
652            }
653        }
654
655        return fingerprints;
656    }
657
658    /**
659     * Add an OmemoMessageListener. This listener will be informed about incoming OMEMO messages
660     * (as well as KeyTransportMessages) and OMEMO encrypted message carbons.
661     *
662     * @param listener OmemoMessageListener
663     */
664    public void addOmemoMessageListener(OmemoMessageListener listener) {
665        omemoMessageListeners.add(listener);
666    }
667
668    /**
669     * Remove an OmemoMessageListener.
670     *
671     * @param listener OmemoMessageListener
672     */
673    public void removeOmemoMessageListener(OmemoMessageListener listener) {
674        omemoMessageListeners.remove(listener);
675    }
676
677    /**
678     * Add an OmemoMucMessageListener. This listener will be informed about incoming OMEMO encrypted MUC messages.
679     *
680     * @param listener OmemoMessageListener.
681     */
682    public void addOmemoMucMessageListener(OmemoMucMessageListener listener) {
683        omemoMucMessageListeners.add(listener);
684    }
685
686    /**
687     * Remove an OmemoMucMessageListener.
688     *
689     * @param listener OmemoMucMessageListener
690     */
691    public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) {
692        omemoMucMessageListeners.remove(listener);
693    }
694
695    /**
696     * Request a deviceList update from contact contact.
697     *
698     * @param contact contact we want to obtain the deviceList from.
699     *
700     * @throws InterruptedException if the calling thread was interrupted.
701     * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
702     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
703     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
704     * @throws SmackException.NoResponseException if there was no response from the remote entity.
705     * @throws IOException if an I/O error occurred.
706     */
707    public synchronized void requestDeviceListUpdateFor(BareJid contact)
708            throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
709            SmackException.NotConnectedException, SmackException.NoResponseException, IOException {
710        getOmemoService().refreshDeviceList(connection(), getOwnDevice(), contact);
711    }
712
713    /**
714     * Publish a new device list with just our own deviceId in it.
715     *
716     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
717     * @throws InterruptedException if the calling thread was interrupted.
718     * @throws XMPPException.XMPPErrorException if there was an XMPP error returned.
719     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
720     * @throws SmackException.NoResponseException if there was no response from the remote entity.
721     * @throws IOException if an I/O error occurred.
722     * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
723     */
724    public void purgeDeviceList()
725            throws SmackException.NotLoggedInException, InterruptedException, XMPPException.XMPPErrorException,
726            SmackException.NotConnectedException, SmackException.NoResponseException, IOException, PubSubException.NotALeafNodeException {
727        getOmemoService().purgeDeviceList(new LoggedInOmemoManager(this));
728    }
729
730    /**
731     * Rotate the signedPreKey published in our OmemoBundle and republish it. This should be done every now and
732     * then (7-14 days). The old signedPreKey should be kept for some more time (a month or so) to enable decryption
733     * of messages that have been sent since the key was changed.
734     *
735     * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged.
736     * @throws InterruptedException XMPP error
737     * @throws XMPPException.XMPPErrorException XMPP error
738     * @throws SmackException.NotConnectedException XMPP error
739     * @throws SmackException.NoResponseException XMPP error
740     * @throws SmackException.NotLoggedInException if the XMPP connection is not authenticated.
741     * @throws IOException if an I/O error occurred.
742     * @throws PubSubException.NotALeafNodeException if a PubSub leaf node operation was attempted on a non-leaf node.
743     */
744    public synchronized void rotateSignedPreKey()
745            throws CorruptedOmemoKeyException, SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
746            SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException,
747            IOException, PubSubException.NotALeafNodeException {
748        if (!connection().isAuthenticated()) {
749            throw new SmackException.NotLoggedInException();
750        }
751
752        // generate key
753        getOmemoService().getOmemoStoreBackend().changeSignedPreKey(getOwnDevice());
754
755        // publish
756        OmemoBundleElement bundle = getOmemoService().getOmemoStoreBackend().packOmemoBundle(getOwnDevice());
757        OmemoService.publishBundle(connection(), getOwnDevice(), bundle);
758    }
759
760    /**
761     * Return true, if the given Stanza contains an OMEMO element 'encrypted'.
762     *
763     * @param stanza stanza
764     * @return true if stanza has extension 'encrypted'
765     */
766    static boolean stanzaContainsOmemoElement(Stanza stanza) {
767        return stanza.hasExtension(OmemoElement.NAME_ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL);
768    }
769
770    /**
771     * Throw an IllegalStateException if no OmemoService is set.
772     */
773    private void throwIfNoServiceSet() {
774        if (service == null) {
775            throw new IllegalStateException("No OmemoService set in OmemoManager.");
776        }
777    }
778
779    /**
780     * Returns a pseudo random number from the interval [1, Integer.MAX_VALUE].
781     *
782     * @return a random deviceId.
783     */
784    public static int randomDeviceId() {
785        return new Random().nextInt(Integer.MAX_VALUE - 1) + 1;
786    }
787
788    /**
789     * Return the BareJid of the user.
790     *
791     * @return our own bare JID.
792     */
793    public BareJid getOwnJid() {
794        if (ownJid == null && connection().isAuthenticated()) {
795            ownJid = connection().getUser().asBareJid();
796        }
797
798        return ownJid;
799    }
800
801    /**
802     * Return the deviceId of this OmemoManager.
803     *
804     * @return this OmemoManagers deviceId.
805     */
806    public synchronized Integer getDeviceId() {
807        return deviceId;
808    }
809
810    /**
811     * Return the OmemoDevice of the user.
812     *
813     * @return our own OmemoDevice
814     */
815    public synchronized OmemoDevice getOwnDevice() {
816        BareJid jid = getOwnJid();
817        if (jid == null) {
818            return null;
819        }
820        return new OmemoDevice(jid, getDeviceId());
821    }
822
823    /**
824     * Set the deviceId of the manager to nDeviceId.
825     *
826     * @param nDeviceId new deviceId
827     */
828    synchronized void setDeviceId(int nDeviceId) {
829        // Move this instance inside the HashMaps
830        INSTANCES.get(connection()).remove(getDeviceId());
831        INSTANCES.get(connection()).put(nDeviceId, this);
832
833        this.deviceId = nDeviceId;
834    }
835
836    /**
837     * Notify all registered OmemoMessageListeners about a received OmemoMessage.
838     *
839     * @param stanza original stanza
840     * @param decryptedMessage decrypted OmemoMessage.
841     */
842    void notifyOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) {
843        for (OmemoMessageListener l : omemoMessageListeners) {
844            l.onOmemoMessageReceived(stanza, decryptedMessage);
845        }
846    }
847
848    /**
849     * Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC.
850     *
851     * @param muc               MultiUserChat the message was received in.
852     * @param stanza            Original Stanza.
853     * @param decryptedMessage  Decrypted OmemoMessage.
854     */
855    void notifyOmemoMucMessageReceived(MultiUserChat muc,
856                                       Stanza stanza,
857                                       OmemoMessage.Received decryptedMessage) {
858        for (OmemoMucMessageListener l : omemoMucMessageListeners) {
859            l.onOmemoMucMessageReceived(muc, stanza, decryptedMessage);
860        }
861    }
862
863    /**
864     * Notify all registered OmemoMessageListeners of an incoming OMEMO encrypted Carbon Copy.
865     * Remember: If you want to receive OMEMO encrypted carbon copies, you have to enable carbons using
866     * {@link CarbonManager#enableCarbons()}.
867     *
868     * @param direction             direction of the carbon copy
869     * @param carbonCopy            carbon copy itself
870     * @param wrappingMessage       wrapping message
871     * @param decryptedCarbonCopy   decrypted carbon copy OMEMO element
872     */
873    void notifyOmemoCarbonCopyReceived(CarbonExtension.Direction direction,
874                                       Message carbonCopy,
875                                       Message wrappingMessage,
876                                       OmemoMessage.Received decryptedCarbonCopy) {
877        for (OmemoMessageListener l : omemoMessageListeners) {
878            l.onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage, decryptedCarbonCopy);
879        }
880    }
881
882    /**
883     * Register stanza listeners needed for OMEMO.
884     * This method is called automatically in the constructor and should only be used to restore the previous state
885     * after {@link #stopStanzaAndPEPListeners()} was called.
886     */
887    public void resumeStanzaAndPEPListeners() {
888        CarbonManager carbonManager = CarbonManager.getInstanceFor(connection());
889
890        // Remove listeners to avoid them getting added twice
891        connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener);
892        carbonManager.removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener);
893
894        // Add listeners
895        pepManager.addPepEventListener(OmemoConstants.PEP_NODE_DEVICE_LIST, OmemoDeviceListElement.class, pepOmemoDeviceListEventListener);
896        connection().addAsyncStanzaListener(this::internalOmemoMessageStanzaListener, OmemoManager::isOmemoMessage);
897        carbonManager.addCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener);
898    }
899
900    /**
901     * Remove active stanza listeners needed for OMEMO.
902     */
903    public void stopStanzaAndPEPListeners() {
904        pepManager.removePepEventListener(pepOmemoDeviceListEventListener);
905        connection().removeAsyncStanzaListener(this::internalOmemoMessageStanzaListener);
906        CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(this::internalOmemoCarbonCopyListener);
907    }
908
909    /**
910     * Build a fresh session with a contacts device.
911     * This might come in handy if a session is broken.
912     *
913     * @param contactsDevice OmemoDevice of a contact.
914     *
915     * @throws InterruptedException if the calling thread was interrupted.
916     * @throws SmackException.NoResponseException if there was no response from the remote entity.
917     * @throws CorruptedOmemoKeyException if our or their identityKey is corrupted.
918     * @throws SmackException.NotConnectedException if the XMPP connection is not connected.
919     * @throws CannotEstablishOmemoSessionException if no new session can be established.
920     * @throws SmackException.NotLoggedInException if the connection is not authenticated.
921     */
922    public void rebuildSessionWith(OmemoDevice contactsDevice)
923            throws InterruptedException, SmackException.NoResponseException, CorruptedOmemoKeyException,
924            SmackException.NotConnectedException, CannotEstablishOmemoSessionException,
925            SmackException.NotLoggedInException {
926        if (!connection().isAuthenticated()) {
927            throw new SmackException.NotLoggedInException();
928        }
929        getOmemoService().buildFreshSessionWithDevice(connection(), getOwnDevice(), contactsDevice);
930    }
931
932    /**
933     * Get our connection.
934     *
935     * @return the connection of this manager
936     */
937    XMPPConnection getConnection() {
938        return connection();
939    }
940
941    /**
942     * Return the OMEMO service object.
943     *
944     * @return the OmemoService object related to this OmemoManager.
945     */
946    OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> getOmemoService() {
947        throwIfNoServiceSet();
948        return service;
949    }
950
951    /**
952     * StanzaListener that listens for incoming Stanzas which contain OMEMO elements.
953     */
954    private void internalOmemoMessageStanzaListener(final Stanza packet) {
955        Async.go(new Runnable() {
956            @Override
957            public void run() {
958                try {
959                    getOmemoService().onOmemoMessageStanzaReceived(packet,
960                            new LoggedInOmemoManager(OmemoManager.this));
961                } catch (SmackException.NotLoggedInException | IOException e) {
962                    LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e);
963                }
964            }
965        });
966    }
967
968    /**
969     * CarbonCopyListener that listens for incoming carbon copies which contain OMEMO elements.
970     */
971    private void internalOmemoCarbonCopyListener(final CarbonExtension.Direction direction,
972                    final Message carbonCopy,
973                    final Message wrappingMessage) {
974        Async.go(new Runnable() {
975            @Override
976            public void run() {
977                if (isOmemoMessage(carbonCopy)) {
978                    try {
979                        getOmemoService().onOmemoCarbonCopyReceived(direction, carbonCopy, wrappingMessage,
980                                new LoggedInOmemoManager(OmemoManager.this));
981                    } catch (SmackException.NotLoggedInException | IOException e) {
982                        LOGGER.log(Level.SEVERE, "Exception while processing OMEMO stanza", e);
983                    }
984                }
985            }
986        });
987    }
988
989    @SuppressWarnings("UnnecessaryLambda")
990    private final PepEventListener<OmemoDeviceListElement> pepOmemoDeviceListEventListener =
991                    (from, receivedDeviceList, id, message) -> {
992        // Device List <list>
993        OmemoCachedDeviceList deviceList;
994        try {
995            getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(getOwnDevice(), from,
996                            receivedDeviceList);
997
998            if (!from.asBareJid().equals(getOwnJid())) {
999                return;
1000            }
1001
1002            deviceList = getOmemoService().cleanUpDeviceList(getOwnDevice());
1003        } catch (IOException e) {
1004            LOGGER.log(Level.SEVERE,
1005                            "IOException while processing OMEMO PEP device updates. Message: " + message,
1006                                e);
1007            return;
1008        }
1009        final OmemoDeviceListElement_VAxolotl newDeviceList = new OmemoDeviceListElement_VAxolotl(deviceList);
1010
1011        if (!newDeviceList.copyDeviceIds().equals(receivedDeviceList.copyDeviceIds())) {
1012            LOGGER.log(Level.FINE, "Republish deviceList due to changes:" +
1013                            " Received: " + Arrays.toString(receivedDeviceList.copyDeviceIds().toArray()) +
1014                            " Published: " + Arrays.toString(newDeviceList.copyDeviceIds().toArray()));
1015            Async.go(new Runnable() {
1016                @Override
1017                public void run() {
1018                    try {
1019                        OmemoService.publishDeviceList(connection(), newDeviceList);
1020                    } catch (InterruptedException | XMPPException.XMPPErrorException |
1021                                    SmackException.NotConnectedException | SmackException.NoResponseException | PubSubException.NotALeafNodeException e) {
1022                        LOGGER.log(Level.WARNING, "Could not publish our deviceList upon an received update.", e);
1023                    }
1024                }
1025            });
1026        }
1027    };
1028
1029    /**
1030     * StanzaFilter that filters messages containing a OMEMO element.
1031     */
1032    private static boolean isOmemoMessage(Stanza stanza) {
1033        return stanza instanceof Message && OmemoManager.stanzaContainsOmemoElement(stanza);
1034    }
1035
1036    /**
1037     * Guard class which ensures that the wrapped OmemoManager knows its BareJid.
1038     */
1039    public static class LoggedInOmemoManager {
1040
1041        private final OmemoManager manager;
1042
1043        public LoggedInOmemoManager(OmemoManager manager)
1044                throws SmackException.NotLoggedInException {
1045
1046            if (manager == null) {
1047                throw new IllegalArgumentException("OmemoManager cannot be null.");
1048            }
1049
1050            if (manager.getOwnJid() == null) {
1051                if (manager.getConnection().isAuthenticated()) {
1052                    manager.ownJid = manager.getConnection().getUser().asBareJid();
1053                } else {
1054                    throw new SmackException.NotLoggedInException();
1055                }
1056            }
1057
1058            this.manager = manager;
1059        }
1060
1061        public OmemoManager get() {
1062            return manager;
1063        }
1064    }
1065
1066    /**
1067     * Callback which can be used to get notified, when the OmemoManager finished initializing.
1068     */
1069    public interface InitializationFinishedCallback {
1070
1071        void initializationFinished(OmemoManager manager);
1072
1073        void initializationFailed(Exception cause);
1074    }
1075
1076    /**
1077     * Get the bareJid of the user from the authenticated XMPP connection.
1078     * If our deviceId is unknown, use the bareJid to look up deviceIds available in the omemoStore.
1079     * If there are ids available, choose the smallest one. Otherwise generate a random deviceId.
1080     *
1081     * @param manager OmemoManager
1082     */
1083    private static void initBareJidAndDeviceId(OmemoManager manager) {
1084        if (!manager.getConnection().isAuthenticated()) {
1085            throw new IllegalStateException("Connection MUST be authenticated.");
1086        }
1087
1088        if (manager.ownJid == null) {
1089            manager.ownJid = manager.getConnection().getUser().asBareJid();
1090        }
1091
1092        if (UNKNOWN_DEVICE_ID.equals(manager.deviceId)) {
1093            SortedSet<Integer> storedDeviceIds = manager.getOmemoService().getOmemoStoreBackend().localDeviceIdsOf(manager.ownJid);
1094            if (storedDeviceIds.size() > 0) {
1095                manager.setDeviceId(storedDeviceIds.first());
1096            } else {
1097                manager.setDeviceId(randomDeviceId());
1098            }
1099        }
1100    }
1101}