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