001/**
002 *
003 * Copyright 2017 Paul Schaub
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.omemo;
018
019import static org.jivesoftware.smackx.omemo.util.OmemoConstants.BODY_OMEMO_HINT;
020import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO;
021import static org.jivesoftware.smackx.omemo.util.OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL;
022import static org.jivesoftware.smackx.omemo.util.OmemoConstants.PEP_NODE_DEVICE_LIST_NOTIFY;
023
024import java.security.NoSuchAlgorithmException;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Random;
030import java.util.Set;
031import java.util.WeakHashMap;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import org.jivesoftware.smack.AbstractConnectionListener;
036import org.jivesoftware.smack.AbstractXMPPConnection;
037import org.jivesoftware.smack.Manager;
038import org.jivesoftware.smack.SmackException;
039import org.jivesoftware.smack.XMPPConnection;
040import org.jivesoftware.smack.XMPPException;
041import org.jivesoftware.smack.packet.ExtensionElement;
042import org.jivesoftware.smack.packet.Message;
043import org.jivesoftware.smack.packet.Stanza;
044import org.jivesoftware.smack.util.Async;
045
046import org.jivesoftware.smackx.carbons.CarbonManager;
047import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
048import org.jivesoftware.smackx.eme.element.ExplicitMessageEncryptionElement;
049import org.jivesoftware.smackx.hints.element.StoreHint;
050import org.jivesoftware.smackx.mam.MamManager;
051import org.jivesoftware.smackx.muc.MultiUserChat;
052import org.jivesoftware.smackx.muc.MultiUserChatManager;
053import org.jivesoftware.smackx.muc.RoomInfo;
054import org.jivesoftware.smackx.omemo.element.OmemoDeviceListVAxolotlElement;
055import org.jivesoftware.smackx.omemo.element.OmemoElement;
056import org.jivesoftware.smackx.omemo.element.OmemoVAxolotlElement;
057import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException;
058import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException;
059import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException;
060import org.jivesoftware.smackx.omemo.exceptions.NoOmemoSupportException;
061import org.jivesoftware.smackx.omemo.exceptions.NoRawSessionException;
062import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException;
063import org.jivesoftware.smackx.omemo.internal.CachedDeviceList;
064import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag;
065import org.jivesoftware.smackx.omemo.internal.ClearTextMessage;
066import org.jivesoftware.smackx.omemo.internal.OmemoDevice;
067import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation;
068import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener;
069import org.jivesoftware.smackx.omemo.listener.OmemoMucMessageListener;
070import org.jivesoftware.smackx.pep.PEPListener;
071import org.jivesoftware.smackx.pep.PEPManager;
072import org.jivesoftware.smackx.pubsub.EventElement;
073import org.jivesoftware.smackx.pubsub.ItemsExtension;
074import org.jivesoftware.smackx.pubsub.PayloadItem;
075import org.jivesoftware.smackx.pubsub.PubSubException;
076import org.jivesoftware.smackx.pubsub.packet.PubSub;
077
078import org.jxmpp.jid.BareJid;
079import org.jxmpp.jid.DomainBareJid;
080import org.jxmpp.jid.EntityBareJid;
081import org.jxmpp.jid.EntityFullJid;
082import org.jxmpp.jid.impl.JidCreate;
083import org.jxmpp.stringprep.XmppStringprepException;
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 WeakHashMap<XMPPConnection, WeakHashMap<Integer,OmemoManager>> INSTANCES = new WeakHashMap<>();
096    private final OmemoService<?, ?, ?, ?, ?, ?, ?, ?, ?> service;
097
098    private final HashSet<OmemoMessageListener> omemoMessageListeners = new HashSet<>();
099    private final HashSet<OmemoMucMessageListener> omemoMucMessageListeners = new HashSet<>();
100
101    private OmemoService<?,?,?,?,?,?,?,?,?>.OmemoStanzaListener omemoStanzaListener;
102    private OmemoService<?,?,?,?,?,?,?,?,?>.OmemoCarbonCopyListener omemoCarbonCopyListener;
103
104    private int deviceId;
105
106    /**
107     * Private constructor to prevent multiple instances on a single connection (which probably would be bad!).
108     *
109     * @param connection connection
110     */
111    private OmemoManager(XMPPConnection connection, int deviceId) {
112        super(connection);
113
114        this.deviceId = deviceId;
115
116        connection.addConnectionListener(new AbstractConnectionListener() {
117            @Override
118            public void authenticated(XMPPConnection connection, boolean resumed) {
119                if (resumed) {
120                    return;
121                }
122                Async.go(new Runnable() {
123                    @Override
124                    public void run() {
125                        try {
126                            initialize();
127                        } catch (InterruptedException | CorruptedOmemoKeyException | PubSubException.NotALeafNodeException | SmackException.NotLoggedInException | SmackException.NoResponseException | SmackException.NotConnectedException | XMPPException.XMPPErrorException e) {
128                            LOGGER.log(Level.SEVERE, "connectionListener.authenticated() failed to initialize OmemoManager: "
129                                    + e.getMessage());
130                        }
131                    }
132                });
133            }
134        });
135
136        PEPManager.getInstanceFor(connection).addPEPListener(deviceListUpdateListener);
137        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(PEP_NODE_DEVICE_LIST_NOTIFY);
138
139        service = OmemoService.getInstance();
140    }
141
142    /**
143     * Get an instance of the OmemoManager for the given connection and deviceId.
144     *
145     * @param connection Connection
146     * @param deviceId deviceId of the Manager. If the deviceId is null, a random id will be generated.
147     * @return an OmemoManager
148     */
149    public synchronized static OmemoManager getInstanceFor(XMPPConnection connection, Integer deviceId) {
150        WeakHashMap<Integer,OmemoManager> managersOfConnection = INSTANCES.get(connection);
151        if (managersOfConnection == null) {
152            managersOfConnection = new WeakHashMap<>();
153            INSTANCES.put(connection, managersOfConnection);
154        }
155
156        if (deviceId == null || deviceId < 1) {
157            deviceId = randomDeviceId();
158        }
159
160        OmemoManager manager = managersOfConnection.get(deviceId);
161        if (manager == null) {
162            manager = new OmemoManager(connection, deviceId);
163            managersOfConnection.put(deviceId, manager);
164        }
165        return manager;
166    }
167
168    /**
169     * Get an instance of the OmemoManager for the given connection.
170     * This method creates the OmemoManager for the stored defaultDeviceId of the connections user.
171     * If there is no such id is stored, it uses a fresh deviceId and sets that as defaultDeviceId instead.
172     *
173     * @param connection connection
174     * @return OmemoManager
175     */
176    public synchronized static OmemoManager getInstanceFor(XMPPConnection connection) {
177        BareJid user;
178        if (connection.getUser() != null) {
179            user = connection.getUser().asBareJid();
180        } else {
181            // This might be dangerous
182            try {
183                user = JidCreate.bareFrom(((AbstractXMPPConnection) connection).getConfiguration().getUsername());
184            } catch (XmppStringprepException e) {
185                throw new AssertionError("Username is not a valid Jid. " +
186                        "Use OmemoManager.gerInstanceFor(Connection, deviceId) instead.");
187            }
188        }
189
190        int defaultDeviceId = OmemoService.getInstance().getOmemoStoreBackend().getDefaultDeviceId(user);
191        if (defaultDeviceId < 1) {
192            defaultDeviceId = randomDeviceId();
193            OmemoService.getInstance().getOmemoStoreBackend().setDefaultDeviceId(user, defaultDeviceId);
194        }
195
196        return getInstanceFor(connection, defaultDeviceId);
197    }
198
199    /**
200     * Initializes the OmemoManager. This method is called automatically once the client logs into the server successfully.
201     *
202     * @throws CorruptedOmemoKeyException
203     * @throws InterruptedException
204     * @throws SmackException.NoResponseException
205     * @throws SmackException.NotConnectedException
206     * @throws XMPPException.XMPPErrorException
207     * @throws SmackException.NotLoggedInException
208     * @throws PubSubException.NotALeafNodeException
209     */
210    public void initialize() throws CorruptedOmemoKeyException, InterruptedException, SmackException.NoResponseException,
211            SmackException.NotConnectedException, XMPPException.XMPPErrorException, SmackException.NotLoggedInException,
212            PubSubException.NotALeafNodeException {
213        getOmemoService().initialize(this);
214    }
215
216    /**
217     * OMEMO encrypt a cleartext message for a single recipient.
218     *
219     * @param to recipients barejid
220     * @param message text to encrypt
221     * @return encrypted message
222     * @throws CryptoFailedException                when something crypto related fails
223     * @throws UndecidedOmemoIdentityException      When there are undecided devices
224     * @throws NoSuchAlgorithmException
225     * @throws InterruptedException
226     * @throws CannotEstablishOmemoSessionException when we could not create session withs all of the recipients devices.
227     * @throws SmackException.NotConnectedException
228     * @throws SmackException.NoResponseException
229     */
230    public Message encrypt(BareJid to, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
231        Message m = new Message();
232        m.setBody(message);
233        OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, to, m);
234        return finishMessage(encrypted);
235    }
236
237    /**
238     * OMEMO encrypt a cleartext message for multiple recipients.
239     *
240     * @param recipients recipients barejids
241     * @param message text to encrypt
242     * @return encrypted message.
243     * @throws CryptoFailedException    When something crypto related fails
244     * @throws UndecidedOmemoIdentityException  When there are undecided devices.
245     * @throws NoSuchAlgorithmException
246     * @throws InterruptedException
247     * @throws CannotEstablishOmemoSessionException When there is one recipient, for whom we failed to create a session
248     *                                              with every one of their devices.
249     * @throws SmackException.NotConnectedException
250     * @throws SmackException.NoResponseException
251     */
252    public Message encrypt(ArrayList<BareJid> recipients, String message) throws CryptoFailedException, UndecidedOmemoIdentityException, NoSuchAlgorithmException, InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
253        Message m = new Message();
254        m.setBody(message);
255        OmemoVAxolotlElement encrypted = getOmemoService().processSendingMessage(this, recipients, m);
256        return finishMessage(encrypted);
257    }
258
259    /**
260     * Encrypt a message for all recipients in the MultiUserChat.
261     *
262     * @param muc multiUserChat
263     * @param message message to send
264     * @return encrypted message
265     * @throws UndecidedOmemoIdentityException when there are undecided devices.
266     * @throws NoSuchAlgorithmException
267     * @throws CryptoFailedException
268     * @throws XMPPException.XMPPErrorException
269     * @throws SmackException.NotConnectedException
270     * @throws InterruptedException
271     * @throws SmackException.NoResponseException
272     * @throws NoOmemoSupportException When the muc doesn't support OMEMO.
273     * @throws CannotEstablishOmemoSessionException when there is a user for whom we could not create a session
274     *                                              with any of their devices.
275     */
276    public Message encrypt(MultiUserChat muc, String message) throws UndecidedOmemoIdentityException, NoSuchAlgorithmException, CryptoFailedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, NoOmemoSupportException, CannotEstablishOmemoSessionException {
277        if (!multiUserChatSupportsOmemo(muc.getRoom())) {
278            throw new NoOmemoSupportException();
279        }
280        Message m = new Message();
281        m.setBody(message);
282        ArrayList<BareJid> recipients = new ArrayList<>();
283        for (EntityFullJid e : muc.getOccupants()) {
284            recipients.add(muc.getOccupant(e).getJid().asBareJid());
285        }
286        return encrypt(recipients, message);
287    }
288
289    /**
290     * Encrypt a message for all users we could build a session with successfully in a previous attempt.
291     * This method can come in handy as a fallback when encrypting a message fails due to devices we cannot
292     * build a session with.
293     *
294     * @param exception CannotEstablishSessionException from a previous encrypt(user(s), message) call.
295     * @param message message we want to send.
296     * @return encrypted message
297     * @throws CryptoFailedException
298     * @throws UndecidedOmemoIdentityException when there are undecided identities.
299     */
300    public Message encryptForExistingSessions(CannotEstablishOmemoSessionException exception, String message) throws CryptoFailedException, UndecidedOmemoIdentityException {
301        Message m = new Message();
302        m.setBody(message);
303        OmemoVAxolotlElement encrypted = getOmemoService().encryptOmemoMessage(this, exception.getSuccesses(), m);
304        return finishMessage(encrypted);
305    }
306
307    /**
308     * Decrypt an OMEMO message. This method comes handy when dealing with messages that were not automatically
309     * decrypted by smack-omemo, eg. MAM query messages.
310     * @param sender sender of the message
311     * @param omemoMessage message
312     * @return decrypted message
313     * @throws InterruptedException                 Exception
314     * @throws SmackException.NoResponseException   Exception
315     * @throws SmackException.NotConnectedException Exception
316     * @throws CryptoFailedException                When decryption fails
317     * @throws XMPPException.XMPPErrorException     Exception
318     * @throws CorruptedOmemoKeyException           When the used keys are invalid
319     * @throws NoRawSessionException                When there is no double ratchet session found for this message
320     */
321    public ClearTextMessage decrypt(BareJid sender, Message omemoMessage) throws InterruptedException, SmackException.NoResponseException, SmackException.NotConnectedException, CryptoFailedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException, NoRawSessionException {
322        return getOmemoService().processLocalMessage(this, sender, omemoMessage);
323    }
324
325    /**
326     * Return a list of all OMEMO messages that were found in the MAM query result, that could be successfully decrypted.
327     * Normal cleartext messages are also added to this list.
328     *
329     * @param mamQueryResult mamQueryResult
330     * @return list of decrypted OmemoMessages
331     * @throws InterruptedException                 Exception
332     * @throws XMPPException.XMPPErrorException     Exception
333     * @throws SmackException.NotConnectedException Exception
334     * @throws SmackException.NoResponseException   Exception
335     */
336    public List<ClearTextMessage> decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException {
337        List<ClearTextMessage> l = new ArrayList<>();
338        l.addAll(getOmemoService().decryptMamQueryResult(this, mamQueryResult));
339        return l;
340    }
341
342    /**
343     * Trust that a fingerprint belongs to an OmemoDevice.
344     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
345     * be of length 64.
346     * @param device device
347     * @param fingerprint fingerprint
348     */
349    public void trustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
350        getOmemoService().getOmemoStoreBackend().trustOmemoIdentity(this, device, fingerprint);
351    }
352
353    /**
354     * Distrust the fingerprint/OmemoDevice tuple.
355     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
356     * be of length 64.
357     * @param device device
358     * @param fingerprint fingerprint
359     */
360    public void distrustOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
361        getOmemoService().getOmemoStoreBackend().distrustOmemoIdentity(this, device, fingerprint);
362    }
363
364    /**
365     * Returns true, if the fingerprint/OmemoDevice tuple is trusted, otherwise false.
366     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
367     * be of length 64.
368     * @param device device
369     * @param fingerprint fingerprint
370     * @return
371     */
372    public boolean isTrustedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
373        return getOmemoService().getOmemoStoreBackend().isTrustedOmemoIdentity(this, device, fingerprint);
374    }
375
376    /**
377     * Returns true, if the fingerprint/OmemoDevice tuple is decided by the user.
378     * The fingerprint must be the lowercase, hexadecimal fingerprint of the identityKey of the device and must
379     * be of length 64.
380     * @param device device
381     * @param fingerprint fingerprint
382     * @return
383     */
384    public boolean isDecidedOmemoIdentity(OmemoDevice device, OmemoFingerprint fingerprint) {
385        return getOmemoService().getOmemoStoreBackend().isDecidedOmemoIdentity(this, device, fingerprint);
386    }
387
388    /**
389     * Clear all other devices except this one from our device list and republish the list.
390     *
391     * @throws InterruptedException
392     * @throws SmackException
393     * @throws XMPPException.XMPPErrorException
394     * @throws CorruptedOmemoKeyException
395     */
396    public void purgeDevices() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException {
397        getOmemoService().publishDeviceIdIfNeeded(this,true);
398        getOmemoService().publishBundle(this);
399    }
400
401    /**
402     * Generate fresh identity keys and bundle and publish it to the server.
403     * @throws SmackException
404     * @throws InterruptedException
405     * @throws XMPPException.XMPPErrorException
406     * @throws CorruptedOmemoKeyException
407     */
408    public void regenerate() throws SmackException, InterruptedException, XMPPException.XMPPErrorException, CorruptedOmemoKeyException {
409        // create a new identity and publish new keys to the server
410        getOmemoService().regenerate(this, null);
411        getOmemoService().publishDeviceIdIfNeeded(this,false);
412        getOmemoService().publishBundle(this);
413    }
414
415    /**
416     * Send a ratchet update message. This can be used to advance the ratchet of a session in order to maintain forward
417     * secrecy.
418     *
419     * @param recipient recipient
420     * @throws UndecidedOmemoIdentityException      When the trust of session with the recipient is not decided yet
421     * @throws CorruptedOmemoKeyException           When the used identityKeys are corrupted
422     * @throws CryptoFailedException                When something fails with the crypto
423     * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
424     */
425    public void sendRatchetUpdateMessage(OmemoDevice recipient)
426            throws CorruptedOmemoKeyException, UndecidedOmemoIdentityException, CryptoFailedException,
427            CannotEstablishOmemoSessionException {
428        getOmemoService().sendOmemoRatchetUpdateMessage(this, recipient, false);
429    }
430
431    /**
432     * Create a new KeyTransportElement. This message will contain the AES-Key and IV that can be used eg. for encrypted
433     * Jingle file transfer.
434     *
435     * @param aesKey    AES key to transport
436     * @param iv        Initialization vector
437     * @param to        list of recipient devices
438     * @return          KeyTransportMessage
439     * @throws UndecidedOmemoIdentityException      When the trust of session with the recipient is not decided yet
440     * @throws CorruptedOmemoKeyException           When the used identityKeys are corrupted
441     * @throws CryptoFailedException                When something fails with the crypto
442     * @throws CannotEstablishOmemoSessionException When we can't establish a session with the recipient
443     */
444    public OmemoVAxolotlElement createKeyTransportElement(byte[] aesKey, byte[] iv, OmemoDevice ... to)
445            throws UndecidedOmemoIdentityException, CorruptedOmemoKeyException, CryptoFailedException,
446            CannotEstablishOmemoSessionException {
447        return getOmemoService().prepareOmemoKeyTransportElement(this, aesKey, iv, to);
448    }
449
450    /**
451     * Create a new Message from a encrypted OmemoMessageElement.
452     * Add ourselves as the sender and the encrypted element.
453     * Also tell the server to store the message despite a possible missing body.
454     * The body will be set to a hint message that we are using OMEMO.
455     *
456     * @param encrypted OmemoMessageElement
457     * @return Message containing the OMEMO element and some additional information
458     */
459    Message finishMessage(OmemoVAxolotlElement encrypted) {
460        if (encrypted == null) {
461            return null;
462        }
463
464        Message chatMessage = new Message();
465        chatMessage.setFrom(connection().getUser().asBareJid());
466        chatMessage.addExtension(encrypted);
467
468        if (OmemoConfiguration.getAddOmemoHintBody()) {
469            chatMessage.setBody(BODY_OMEMO_HINT);
470        }
471
472        if (OmemoConfiguration.getAddMAMStorageProcessingHint()) {
473            StoreHint.set(chatMessage);
474        }
475
476        if (OmemoConfiguration.getAddEmeEncryptionHint()) {
477            chatMessage.addExtension(new ExplicitMessageEncryptionElement(OMEMO_NAMESPACE_V_AXOLOTL, OMEMO));
478        }
479
480        return chatMessage;
481    }
482
483    /**
484     * Returns true, if the contact has any active devices published in a deviceList.
485     *
486     * @param contact contact
487     * @return true if contact has at least one OMEMO capable device.
488     * @throws SmackException.NotConnectedException
489     * @throws InterruptedException
490     * @throws SmackException.NoResponseException
491     */
492    public boolean contactSupportsOmemo(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
493        getOmemoService().refreshDeviceList(this, contact);
494        return !getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact)
495                .getActiveDevices().isEmpty();
496    }
497
498    /**
499     * Returns true, if the MUC with the EntityBareJid multiUserChat is non-anonymous and members only (prerequisite
500     * for OMEMO encryption in MUC).
501     *
502     * @param multiUserChat EntityBareJid of the MUC
503     * @return true if chat supports OMEMO
504     * @throws XMPPException.XMPPErrorException     if
505     * @throws SmackException.NotConnectedException something
506     * @throws InterruptedException                 goes
507     * @throws SmackException.NoResponseException   wrong
508     */
509    public boolean multiUserChatSupportsOmemo(EntityBareJid multiUserChat) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
510        RoomInfo roomInfo = MultiUserChatManager.getInstanceFor(connection()).getRoomInfo(multiUserChat);
511        return roomInfo.isNonanonymous() && roomInfo.isMembersOnly();
512    }
513
514    /**
515     * Returns true, if the Server supports PEP.
516     *
517     * @param connection XMPPConnection
518     * @param server domainBareJid of the server to test
519     * @return true if server supports pep
520     * @throws XMPPException.XMPPErrorException
521     * @throws SmackException.NotConnectedException
522     * @throws InterruptedException
523     * @throws SmackException.NoResponseException
524     */
525    public static boolean serverSupportsOmemo(XMPPConnection connection, DomainBareJid server) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
526        return ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(server).containsFeature(PubSub.NAMESPACE);
527    }
528
529    /**
530     * Return the fingerprint of our identity key.
531     *
532     * @return fingerprint
533     */
534    public OmemoFingerprint getOurFingerprint() {
535        return getOmemoService().getOmemoStoreBackend().getFingerprint(this);
536    }
537
538    public OmemoFingerprint getFingerprint(OmemoDevice device) throws CannotEstablishOmemoSessionException {
539        if (device.equals(getOwnDevice())) {
540            return getOurFingerprint();
541        }
542
543        return getOmemoService().getOmemoStoreBackend().getFingerprint(this, device);
544    }
545
546    /**
547     * Return all fingerprints of active devices of a contact.
548     * @param contact contact
549     * @return HashMap of deviceIds and corresponding fingerprints.
550     */
551    public HashMap<OmemoDevice, OmemoFingerprint> getActiveFingerprints(BareJid contact) {
552        HashMap<OmemoDevice, OmemoFingerprint> fingerprints = new HashMap<>();
553        CachedDeviceList deviceList = getOmemoService().getOmemoStoreBackend().loadCachedDeviceList(this, contact);
554        for (int id : deviceList.getActiveDevices()) {
555            OmemoDevice device = new OmemoDevice(contact, id);
556            OmemoFingerprint fingerprint = null;
557            try {
558                fingerprint = getFingerprint(device);
559            } catch (CannotEstablishOmemoSessionException e) {
560                LOGGER.log(Level.WARNING, "Could not build session with device " + id
561                        + " of user " + contact + ": " + e.getMessage());
562            }
563
564            if (fingerprint != null) {
565                fingerprints.put(device, fingerprint);
566            }
567        }
568        return fingerprints;
569    }
570
571    public void addOmemoMessageListener(OmemoMessageListener listener) {
572        omemoMessageListeners.add(listener);
573    }
574
575    public void removeOmemoMessageListener(OmemoMessageListener listener) {
576        omemoMessageListeners.remove(listener);
577    }
578
579    public void addOmemoMucMessageListener(OmemoMucMessageListener listener) {
580        omemoMucMessageListeners.add(listener);
581    }
582
583    public void removeOmemoMucMessageListener(OmemoMucMessageListener listener) {
584        omemoMucMessageListeners.remove(listener);
585    }
586
587    /**
588     * Build OMEMO sessions with devices of contact.
589     *
590     * @param contact contact we want to build session with.
591     * @throws InterruptedException
592     * @throws CannotEstablishOmemoSessionException
593     * @throws SmackException.NotConnectedException
594     * @throws SmackException.NoResponseException
595     */
596    public void buildSessionsWith(BareJid contact) throws InterruptedException, CannotEstablishOmemoSessionException, SmackException.NotConnectedException, SmackException.NoResponseException {
597        getOmemoService().buildOrCreateOmemoSessionsFromBundles(this, contact);
598    }
599
600    /**
601     * Request a deviceList update from contact contact.
602     *
603     * @param contact contact we want to obtain the deviceList from.
604     * @throws SmackException.NotConnectedException
605     * @throws InterruptedException
606     * @throws SmackException.NoResponseException
607     */
608    public void requestDeviceListUpdateFor(BareJid contact) throws SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
609        getOmemoService().refreshDeviceList(this, contact);
610    }
611
612    /**
613     * Rotate the signedPreKey published in our OmemoBundle. This should be done every now and then (7-14 days).
614     * The old signedPreKey should be kept for some more time (a month or so) to enable decryption of messages
615     * that have been sent since the key was changed.
616     *
617     * @throws CorruptedOmemoKeyException When the IdentityKeyPair is damaged.
618     * @throws InterruptedException XMPP error
619     * @throws XMPPException.XMPPErrorException XMPP error
620     * @throws SmackException.NotConnectedException XMPP error
621     * @throws SmackException.NoResponseException XMPP error
622     * @throws PubSubException.NotALeafNodeException if the bundle node on the server is a CollectionNode
623     */
624    public void rotateSignedPreKey() throws CorruptedOmemoKeyException, InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException, PubSubException.NotALeafNodeException {
625        // generate key
626        getOmemoService().getOmemoStoreBackend().changeSignedPreKey(this);
627        // publish
628        getOmemoService().publishDeviceIdIfNeeded(this, false);
629        getOmemoService().publishBundle(this);
630    }
631
632    /**
633     * Return true, if the given Stanza contains an OMEMO element 'encrypted'.
634     * @param stanza stanza
635     * @return true if stanza has extension 'encrypted'
636     */
637    public static boolean stanzaContainsOmemoElement(Stanza stanza) {
638        return stanza.hasExtension(OmemoElement.ENCRYPTED, OMEMO_NAMESPACE_V_AXOLOTL);
639    }
640
641    /**
642     * Throw an IllegalStateException if no OmemoService is set.
643     */
644    private void throwIfNoServiceSet() {
645        if (service == null) {
646            throw new IllegalStateException("No OmemoService set in OmemoManager.");
647        }
648    }
649
650    public static int randomDeviceId() {
651        int i = new Random().nextInt(Integer.MAX_VALUE);
652
653        if (i == 0) {
654            return randomDeviceId();
655        }
656
657        return Math.abs(i);
658    }
659
660    /**
661     * Return the BareJid of the user.
662     *
663     * @return bareJid
664     */
665    public BareJid getOwnJid() {
666        EntityFullJid fullJid = connection().getUser();
667        if (fullJid == null) return null;
668        return fullJid.asBareJid();
669    }
670
671    /**
672     * Return the deviceId of this OmemoManager.
673     *
674     * @return deviceId
675     */
676    public int getDeviceId() {
677        return deviceId;
678    }
679
680    /**
681     * Return the OmemoDevice of the user.
682     *
683     * @return omemoDevice
684     */
685    public OmemoDevice getOwnDevice() {
686        return new OmemoDevice(getOwnJid(), getDeviceId());
687    }
688
689    void setDeviceId(int nDeviceId) {
690        INSTANCES.get(connection()).remove(getDeviceId());
691        INSTANCES.get(connection()).put(nDeviceId, this);
692        this.deviceId = nDeviceId;
693    }
694
695    /**
696     * Notify all registered OmemoMessageListeners about a received OmemoMessage.
697     *
698     * @param decryptedBody      decrypted Body element of the message
699     * @param encryptedMessage   unmodified message as it was received
700     * @param wrappingMessage    message that wrapped the incoming message
701     * @param messageInformation information about the messages encryption (used identityKey, carbon...)
702     */
703    void notifyOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation messageInformation) {
704        for (OmemoMessageListener l : omemoMessageListeners) {
705            l.onOmemoMessageReceived(decryptedBody, encryptedMessage, wrappingMessage, messageInformation);
706        }
707    }
708
709    void notifyOmemoKeyTransportMessageReceived(CipherAndAuthTag cipherAndAuthTag, Message transportingMessage,
710                                                Message wrappingMessage, OmemoMessageInformation information) {
711        for (OmemoMessageListener l : omemoMessageListeners) {
712            l.onOmemoKeyTransportReceived(cipherAndAuthTag, transportingMessage, wrappingMessage, information);
713        }
714    }
715
716    /**
717     * Notify all registered OmemoMucMessageListeners of an incoming OmemoMessageElement in a MUC.
718     *
719     * @param muc              MultiUserChat the message was received in
720     * @param from             BareJid of the user that sent the message
721     * @param decryptedBody    decrypted body
722     * @param message          original message with encrypted content
723     * @param wrappingMessage  wrapping message (in case of carbon copy)
724     * @param omemoInformation information about the encryption of the message
725     */
726    void notifyOmemoMucMessageReceived(MultiUserChat muc, BareJid from, String decryptedBody, Message message,
727                                               Message wrappingMessage, OmemoMessageInformation omemoInformation) {
728        for (OmemoMucMessageListener l : omemoMucMessageListeners) {
729            l.onOmemoMucMessageReceived(muc, from, decryptedBody, message,
730                    wrappingMessage, omemoInformation);
731        }
732    }
733
734    void notifyOmemoMucKeyTransportMessageReceived(MultiUserChat muc, BareJid from, CipherAndAuthTag cipherAndAuthTag,
735                                                   Message transportingMessage, Message wrappingMessage,
736                                                   OmemoMessageInformation messageInformation) {
737        for (OmemoMucMessageListener l : omemoMucMessageListeners) {
738            l.onOmemoKeyTransportReceived(muc, from, cipherAndAuthTag,
739                    transportingMessage, wrappingMessage, messageInformation);
740        }
741    }
742
743    /**
744     * Remove all active stanza listeners of this manager from the connection.
745     * This is somewhat the counterpart of initialize().
746     */
747    public void shutdown() {
748        PEPManager.getInstanceFor(connection()).removePEPListener(deviceListUpdateListener);
749        connection().removeAsyncStanzaListener(omemoStanzaListener);
750        CarbonManager.getInstanceFor(connection()).removeCarbonCopyReceivedListener(omemoCarbonCopyListener);
751    }
752
753    /**
754     * Get our connection.
755     *
756     * @return the connection of this manager
757     */
758    XMPPConnection getConnection() {
759        return connection();
760    }
761
762    /**
763     * Return the OMEMO service object.
764     *
765     * @return omemoService
766     */
767    OmemoService<?,?,?,?,?,?,?,?,?> getOmemoService() {
768        throwIfNoServiceSet();
769        return service;
770    }
771
772    PEPListener deviceListUpdateListener = new PEPListener() {
773        @Override
774        public void eventReceived(EntityBareJid from, EventElement event, Message message) {
775            for (ExtensionElement items : event.getExtensions()) {
776                if (!(items instanceof ItemsExtension)) {
777                    continue;
778                }
779
780                for (ExtensionElement item : ((ItemsExtension) items).getItems()) {
781                    if (!(item instanceof PayloadItem<?>)) {
782                        continue;
783                    }
784
785                    PayloadItem<?> payloadItem = (PayloadItem<?>) item;
786
787                    if (!(payloadItem.getPayload() instanceof  OmemoDeviceListVAxolotlElement)) {
788                        continue;
789                    }
790
791                    // Device List <list>
792                    OmemoDeviceListVAxolotlElement omemoDeviceListElement = (OmemoDeviceListVAxolotlElement) payloadItem.getPayload();
793                    int ourDeviceId = getDeviceId();
794                    getOmemoService().getOmemoStoreBackend().mergeCachedDeviceList(OmemoManager.this, from, omemoDeviceListElement);
795
796                    if (from == null) {
797                        // Unknown sender, no more work to do.
798                        // TODO: This DOES happen for some reason. Figure out when...
799                        continue;
800                    }
801
802                    if (!from.equals(getOwnJid())) {
803                        // Not our deviceList, so nothing more to do
804                        continue;
805                    }
806
807                    if (omemoDeviceListElement.getDeviceIds().contains(ourDeviceId)) {
808                        // We are on the list. Nothing more to do
809                        continue;
810                    }
811
812                    // Our deviceList and we are not on it! We don't want to miss all the action!!!
813                    LOGGER.log(Level.INFO, "Our deviceId was not on the list!");
814                    Set<Integer> deviceListIds = omemoDeviceListElement.copyDeviceIds();
815                    // enroll at the deviceList
816                    deviceListIds.add(ourDeviceId);
817                    final OmemoDeviceListVAxolotlElement newOmemoDeviceListElement = new OmemoDeviceListVAxolotlElement(deviceListIds);
818
819                    // PEPListener is a synchronous listener. Avoid any deadlocks by using an async task to update the device list.
820                    Async.go(new Runnable() {
821                        @Override
822                        public void run() {
823                            try {
824                                OmemoService.publishDeviceIds(OmemoManager.this, newOmemoDeviceListElement);
825                            }
826                            catch (SmackException | InterruptedException | XMPPException.XMPPErrorException e) {
827                                // TODO: It might be dangerous NOT to retry publishing our deviceId
828                                LOGGER.log(Level.SEVERE,
829                                                "Could not publish our device list after an update without our id was received: "
830                                                                + e.getMessage());
831                            }
832                        }
833                    });
834                }
835            }
836        }
837    };
838
839
840
841    OmemoService<?,?,?,?,?,?,?,?,?>.OmemoStanzaListener getOmemoStanzaListener() {
842        if (omemoStanzaListener == null) {
843            omemoStanzaListener = getOmemoService().createStanzaListener(this);
844        }
845        return omemoStanzaListener;
846    }
847
848    OmemoService<?,?,?,?,?,?,?,?,?>.OmemoCarbonCopyListener getOmemoCarbonCopyListener() {
849        if (omemoCarbonCopyListener == null) {
850            omemoCarbonCopyListener = getOmemoService().createOmemoCarbonCopyListener(this);
851        }
852        return omemoCarbonCopyListener;
853    }
854}