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