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