001/**
002 *
003 * Copyright 2017 Florian Schmaus, 2018 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.ox;
018
019import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS;
020import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS_NOTIFY;
021import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.publishPublicKey;
022
023import java.io.ByteArrayOutputStream;
024import java.io.IOException;
025import java.security.InvalidAlgorithmParameterException;
026import java.security.NoSuchAlgorithmException;
027import java.security.NoSuchProviderException;
028import java.util.Date;
029import java.util.HashSet;
030import java.util.Map;
031import java.util.Set;
032import java.util.WeakHashMap;
033import java.util.logging.Level;
034import java.util.logging.Logger;
035
036import org.jivesoftware.smack.Manager;
037import org.jivesoftware.smack.SmackException;
038import org.jivesoftware.smack.XMPPConnection;
039import org.jivesoftware.smack.XMPPException;
040import org.jivesoftware.smack.chat2.Chat;
041import org.jivesoftware.smack.chat2.ChatManager;
042import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
043import org.jivesoftware.smack.packet.Message;
044import org.jivesoftware.smack.util.Async;
045import org.jivesoftware.smack.util.stringencoder.Base64;
046import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
047import org.jivesoftware.smackx.ox.callback.backup.AskForBackupCodeCallback;
048import org.jivesoftware.smackx.ox.callback.backup.DisplayBackupCodeCallback;
049import org.jivesoftware.smackx.ox.callback.backup.SecretKeyBackupSelectionCallback;
050import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
051import org.jivesoftware.smackx.ox.element.CryptElement;
052import org.jivesoftware.smackx.ox.element.OpenPgpContentElement;
053import org.jivesoftware.smackx.ox.element.OpenPgpElement;
054import org.jivesoftware.smackx.ox.element.PubkeyElement;
055import org.jivesoftware.smackx.ox.element.PublicKeysListElement;
056import org.jivesoftware.smackx.ox.element.SecretkeyElement;
057import org.jivesoftware.smackx.ox.element.SignElement;
058import org.jivesoftware.smackx.ox.element.SigncryptElement;
059import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
060import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
061import org.jivesoftware.smackx.ox.exception.MissingUserIdOnKeyException;
062import org.jivesoftware.smackx.ox.exception.NoBackupFoundException;
063import org.jivesoftware.smackx.ox.listener.CryptElementReceivedListener;
064import org.jivesoftware.smackx.ox.listener.SignElementReceivedListener;
065import org.jivesoftware.smackx.ox.listener.SigncryptElementReceivedListener;
066import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
067import org.jivesoftware.smackx.ox.store.definition.OpenPgpTrustStore;
068import org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil;
069import org.jivesoftware.smackx.ox.util.SecretKeyBackupHelper;
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.LeafNode;
075import org.jivesoftware.smackx.pubsub.PayloadItem;
076import org.jivesoftware.smackx.pubsub.PubSubException;
077import org.jivesoftware.smackx.pubsub.PubSubFeature;
078
079import org.bouncycastle.openpgp.PGPException;
080import org.bouncycastle.openpgp.PGPPublicKey;
081import org.bouncycastle.openpgp.PGPPublicKeyRing;
082import org.bouncycastle.openpgp.PGPSecretKey;
083import org.bouncycastle.openpgp.PGPSecretKeyRing;
084import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
085import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
086import org.jxmpp.jid.BareJid;
087import org.jxmpp.jid.EntityBareJid;
088import org.pgpainless.key.OpenPgpV4Fingerprint;
089import org.pgpainless.key.collection.PGPKeyRing;
090import org.pgpainless.key.protection.SecretKeyRingProtector;
091import org.pgpainless.util.BCUtil;
092import org.xmlpull.v1.XmlPullParserException;
093
094/**
095 * Entry point for Smacks API for OpenPGP for XMPP.
096 *
097 * <h2>Setup</h2>
098 *
099 * In order to use OpenPGP for XMPP in Smack, just follow the following procedure.<br>
100 * <br>
101 * First, acquire an instance of the {@link OpenPgpManager} for your {@link XMPPConnection} using
102 * {@link #getInstanceFor(XMPPConnection)}.
103 *
104 * <pre>
105 * {@code
106 * OpenPgpManager openPgpManager = OpenPgpManager.getInstanceFor(connection);
107 * }
108 * </pre>
109 *
110 * You also need an {@link OpenPgpProvider}, as well as an {@link OpenPgpStore}.
111 * The provider must be registered using {@link #setOpenPgpProvider(OpenPgpProvider)}.
112 *
113 * <pre>
114 * {@code
115 * OpenPgpStore store = new FileBasedOpenPgpStore(storePath);
116 * OpenPgpProvider provider = new PainlessOpenPgpProvider(connection, store);
117 * openPgpManager.setOpenPgpProvider(provider);
118 * }
119 * </pre>
120 *
121 * It is also advised to register a custom {@link SecretKeyRingProtector} using
122 * {@link OpenPgpStore#setKeyRingProtector(SecretKeyRingProtector)} in order to be able to handle password protected
123 * secret keys.<br>
124 * <br>
125 * Speaking of keys, you can now check, if you have any keys available in your {@link OpenPgpStore} by doing
126 * {@link #hasSecretKeysAvailable()}.<br>
127 * <br>
128 * If you do, you can now announce support for OX and publish those keys using {@link #announceSupportAndPublish()}.<br>
129 * <br>
130 * Otherwise, you can either generate fresh keys using {@link #generateAndImportKeyPair(BareJid)},
131 * or try to restore a secret key backup from your private PubSub node by doing
132 * {@link #restoreSecretKeyServerBackup(AskForBackupCodeCallback)}.<br>
133 * <br>
134 * In any case you should still do an {@link #announceSupportAndPublish()} afterwards.
135 * <br>
136 * <br>
137 * Contacts are represented by {@link OpenPgpContact}s in the context of OpenPGP for XMPP. You can get those by using
138 * {@link #getOpenPgpContact(EntityBareJid)}. The main function of {@link OpenPgpContact}s is to bundle information
139 * about the OpenPGP capabilities of a contact in one spot. The pendant to the {@link OpenPgpContact} is the
140 * {@link OpenPgpSelf}, which encapsulates your own OpenPGP identity. Both classes can be used to acquire information
141 * about the OpenPGP keys of a user.
142 *
143 * <h2>Elements</h2>
144 *
145 * OpenPGP for XMPP defines multiple different element classes which contain the users messages.
146 * The outermost element is the {@link OpenPgpElement}, which contains an OpenPGP encrypted content element.
147 *
148 * The content can be either a {@link SignElement}, {@link CryptElement} or {@link SigncryptElement}, depending on the use-case.
149 * Those content elements contain the actual payload. If an {@link OpenPgpElement} is decrypted, it will be returned in
150 * form of an {@link OpenPgpMessage}, which represents the decrypted message + metadata.
151 *
152 * @see <a href="https://xmpp.org/extensions/xep-0373.html">
153 *     XEP-0373: OpenPGP for XMPP</a>
154 */
155public final class OpenPgpManager extends Manager {
156
157    private static final Logger LOGGER = Logger.getLogger(OpenPgpManager.class.getName());
158
159    /**
160     * Map of instances.
161     */
162    private static final Map<XMPPConnection, OpenPgpManager> INSTANCES = new WeakHashMap<>();
163
164    /**
165     * {@link OpenPgpProvider} responsible for processing keys, encrypting and decrypting messages and so on.
166     */
167    private OpenPgpProvider provider;
168
169    private final PepManager pepManager;
170
171    private final Set<SigncryptElementReceivedListener> signcryptElementReceivedListeners = new HashSet<>();
172    private final Set<SignElementReceivedListener> signElementReceivedListeners = new HashSet<>();
173    private final Set<CryptElementReceivedListener> cryptElementReceivedListeners = new HashSet<>();
174
175    /**
176     * Private constructor to avoid instantiation without putting the object into {@code INSTANCES}.
177     *
178     * @param connection xmpp connection.
179     */
180    private OpenPgpManager(XMPPConnection connection) {
181        super(connection);
182        ChatManager.getInstanceFor(connection).addIncomingListener(incomingOpenPgpMessageListener);
183        pepManager = PepManager.getInstanceFor(connection);
184    }
185
186    /**
187     * Get the instance of the {@link OpenPgpManager} which belongs to the {@code connection}.
188     *
189     * @param connection xmpp connection.
190     * @return instance of the manager.
191     */
192    public static OpenPgpManager getInstanceFor(XMPPConnection connection) {
193        OpenPgpManager manager = INSTANCES.get(connection);
194        if (manager == null) {
195            manager = new OpenPgpManager(connection);
196            INSTANCES.put(connection, manager);
197        }
198        return manager;
199    }
200
201    /**
202     * Return our own {@link BareJid}.
203     *
204     * @return our bareJid
205     *
206     * @throws SmackException.NotLoggedInException in case our connection is not logged in, which means our BareJid is unknown.
207     */
208    public BareJid getJidOrThrow() throws SmackException.NotLoggedInException {
209        throwIfNotAuthenticated();
210        return connection().getUser().asEntityBareJidOrThrow();
211    }
212
213    /**
214     * Set the {@link OpenPgpProvider} which will be used to process incoming OpenPGP elements,
215     * as well as to execute cryptographic operations.
216     *
217     * @param provider OpenPgpProvider.
218     */
219    public void setOpenPgpProvider(OpenPgpProvider provider) {
220        this.provider = provider;
221    }
222
223    public OpenPgpProvider getOpenPgpProvider() {
224        return provider;
225    }
226
227    /**
228     * Get our OpenPGP self.
229     *
230     * @return self
231     * @throws SmackException.NotLoggedInException if we are not logged in
232     */
233    public OpenPgpSelf getOpenPgpSelf() throws SmackException.NotLoggedInException {
234        throwIfNoProviderSet();
235        return new OpenPgpSelf(getJidOrThrow(), provider.getStore());
236    }
237
238    /**
239     * Generate a fresh OpenPGP key pair, given we don't have one already.
240     * Publish the public key to the Public Key Node and update the Public Key Metadata Node with our keys fingerprint.
241     * Lastly register a {@link PepListener} which listens for updates to Public Key Metadata Nodes.
242     *
243     * @throws NoSuchAlgorithmException if we are missing an algorithm to generate a fresh key pair.
244     * @throws NoSuchProviderException if we are missing a suitable {@link java.security.Provider}.
245     * @throws InterruptedException if the thread gets interrupted.
246     * @throws PubSubException.NotALeafNodeException if one of the PubSub nodes is not a {@link LeafNode}.
247     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
248     * @throws SmackException.NotConnectedException if we are not connected.
249     * @throws SmackException.NoResponseException if the server doesn't respond.
250     * @throws IOException IO is dangerous.
251     * @throws InvalidAlgorithmParameterException if illegal algorithm parameters are used for key generation.
252     * @throws SmackException.NotLoggedInException if we are not logged in.
253     * @throws PGPException if something goes wrong during key loading/generating
254     */
255    public void announceSupportAndPublish()
256            throws NoSuchAlgorithmException, NoSuchProviderException, InterruptedException,
257            PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
258            SmackException.NotConnectedException, SmackException.NoResponseException, IOException,
259            InvalidAlgorithmParameterException, SmackException.NotLoggedInException, PGPException {
260        throwIfNoProviderSet();
261        throwIfNotAuthenticated();
262
263        OpenPgpV4Fingerprint primaryFingerprint = getOurFingerprint();
264
265        if (primaryFingerprint == null) {
266            primaryFingerprint = generateAndImportKeyPair(getJidOrThrow());
267        }
268
269        // Create <pubkey/> element
270        PubkeyElement pubkeyElement;
271        try {
272            pubkeyElement = createPubkeyElement(getJidOrThrow(), primaryFingerprint, new Date());
273        } catch (MissingOpenPgpKeyException e) {
274            throw new AssertionError("Cannot publish our public key, since it is missing (MUST NOT happen!)");
275        }
276
277        // publish it
278        publishPublicKey(pepManager, pubkeyElement, primaryFingerprint);
279
280        // Subscribe to public key changes
281        PepManager.getInstanceFor(connection()).addPepListener(metadataListener);
282        ServiceDiscoveryManager.getInstanceFor(connection())
283                .addFeature(PEP_NODE_PUBLIC_KEYS_NOTIFY);
284    }
285
286    /**
287     * Generate a fresh OpenPGP key pair and import it.
288     *
289     * @param ourJid our {@link BareJid}.
290     * @return {@link OpenPgpV4Fingerprint} of the generated key.
291     * @throws NoSuchAlgorithmException if the JVM doesn't support one of the used algorithms.
292     * @throws InvalidAlgorithmParameterException if the used algorithm parameters are invalid.
293     * @throws NoSuchProviderException if we are missing a cryptographic provider.
294     * @throws PGPException PGP is brittle.
295     * @throws IOException IO is dangerous.
296     */
297    public OpenPgpV4Fingerprint generateAndImportKeyPair(BareJid ourJid)
298            throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException,
299            PGPException, IOException {
300
301        throwIfNoProviderSet();
302        OpenPgpStore store = provider.getStore();
303        PGPKeyRing keys = store.generateKeyRing(ourJid);
304        try {
305            store.importSecretKey(ourJid, keys.getSecretKeys());
306            store.importPublicKey(ourJid, keys.getPublicKeys());
307        } catch (MissingUserIdOnKeyException e) {
308            // This should never throw, since we set our jid literally one line above this comment.
309            throw new AssertionError(e);
310        }
311
312        OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(keys.getSecretKeys());
313
314        store.setTrust(ourJid, fingerprint, OpenPgpTrustStore.Trust.trusted);
315
316        return fingerprint;
317    }
318
319    /**
320     * Return the upper-case hex encoded OpenPGP v4 fingerprint of our key pair.
321     *
322     * @return fingerprint.
323     * @throws SmackException.NotLoggedInException in case we are not logged in.
324     * @throws IOException IO is dangerous.
325     * @throws PGPException PGP is brittle.
326     */
327    public OpenPgpV4Fingerprint getOurFingerprint()
328            throws SmackException.NotLoggedInException, IOException, PGPException {
329        return getOpenPgpSelf().getSigningKeyFingerprint();
330    }
331
332    /**
333     * Return an OpenPGP capable contact.
334     * This object can be used as an entry point to OpenPGP related API.
335     *
336     * @param jid {@link BareJid} of the contact.
337     * @return {@link OpenPgpContact}.
338     */
339    public OpenPgpContact getOpenPgpContact(EntityBareJid jid) {
340        throwIfNoProviderSet();
341        return provider.getStore().getOpenPgpContact(jid);
342    }
343
344    /**
345     * Return true, if we have a secret key available, otherwise false.
346     *
347     * @return true if secret key available
348     *
349     * @throws SmackException.NotLoggedInException If we are not logged in (we need to know our jid in order to look up
350     * our keys in the key store.
351     * @throws PGPException in case the keys in the store are damaged somehow.
352     * @throws IOException IO is dangerous.
353     */
354    public boolean hasSecretKeysAvailable() throws SmackException.NotLoggedInException, PGPException, IOException {
355        throwIfNoProviderSet();
356        return getOpenPgpSelf().hasSecretKeyAvailable();
357    }
358
359    /**
360     * Determine, if we can sync secret keys using private PEP nodes as described in the XEP.
361     * Requirements on the server side are support for PEP and support for the whitelist access model of PubSub.
362     *
363     * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
364     *
365     * @param connection XMPP connection
366     * @return true, if the server supports secret key backups, otherwise false.
367     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
368     * @throws SmackException.NotConnectedException if we are not connected.
369     * @throws InterruptedException if the thread is interrupted.
370     * @throws SmackException.NoResponseException if the server doesn't respond.
371     */
372    public static boolean serverSupportsSecretKeyBackups(XMPPConnection connection)
373            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
374            SmackException.NoResponseException {
375        return ServiceDiscoveryManager.getInstanceFor(connection)
376                .serverSupportsFeature(PubSubFeature.access_whitelist.toString());
377    }
378
379    /**
380     * Remove the metadata listener. This method is mainly used in tests.
381     */
382    public void stopMetadataListener() {
383        PepManager.getInstanceFor(connection()).removePepListener(metadataListener);
384    }
385
386    /**
387     * Upload the encrypted secret key to a private PEP node.
388     *
389     * @see <a href="https://xmpp.org/extensions/xep-0373.html#synchro-pep">XEP-0373 §5</a>
390     *
391     * @param displayCodeCallback callback, which will receive the backup password used to encrypt the secret key.
392     * @param selectKeyCallback callback, which will receive the users choice of which keys will be backed up.
393     * @throws InterruptedException if the thread is interrupted.
394     * @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
395     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
396     * @throws SmackException.NotConnectedException if we are not connected.
397     * @throws SmackException.NoResponseException if the server doesn't respond.
398     * @throws SmackException.NotLoggedInException if we are not logged in.
399     * @throws IOException IO is dangerous.
400     * @throws SmackException.FeatureNotSupportedException if the server doesn't support the PubSub whitelist access model.
401     * @throws PGPException PGP is brittle
402     * @throws MissingOpenPgpKeyException in case we have no OpenPGP key pair to back up.
403     */
404    public void backupSecretKeyToServer(DisplayBackupCodeCallback displayCodeCallback,
405                                        SecretKeyBackupSelectionCallback selectKeyCallback)
406            throws InterruptedException, PubSubException.NotALeafNodeException,
407            XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException,
408            SmackException.NotLoggedInException, IOException,
409            SmackException.FeatureNotSupportedException, PGPException, MissingOpenPgpKeyException {
410        throwIfNoProviderSet();
411        throwIfNotAuthenticated();
412
413        BareJid ownJid = connection().getUser().asBareJid();
414
415        String backupCode = SecretKeyBackupHelper.generateBackupPassword();
416
417        PGPSecretKeyRingCollection secretKeyRings = provider.getStore().getSecretKeysOf(ownJid);
418
419        Set<OpenPgpV4Fingerprint> availableKeyPairs = new HashSet<>();
420        for (PGPSecretKeyRing ring : secretKeyRings) {
421            availableKeyPairs.add(new OpenPgpV4Fingerprint(ring));
422        }
423
424        Set<OpenPgpV4Fingerprint> selectedKeyPairs = selectKeyCallback.selectKeysToBackup(availableKeyPairs);
425
426        SecretkeyElement secretKey = SecretKeyBackupHelper.createSecretkeyElement(provider, ownJid, selectedKeyPairs, backupCode);
427
428        OpenPgpPubSubUtil.depositSecretKey(connection(), secretKey);
429        displayCodeCallback.displayBackupCode(backupCode);
430    }
431
432    /**
433     * Delete the private {@link LeafNode} containing our secret key backup.
434     *
435     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
436     * @throws SmackException.NotConnectedException if we are not connected.
437     * @throws InterruptedException if the thread gets interrupted.
438     * @throws SmackException.NoResponseException if the server doesn't respond.
439     * @throws SmackException.NotLoggedInException if we are not logged in.
440     */
441    public void deleteSecretKeyServerBackup()
442            throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
443            SmackException.NoResponseException, SmackException.NotLoggedInException {
444        throwIfNotAuthenticated();
445        OpenPgpPubSubUtil.deleteSecretKeyNode(pepManager);
446    }
447
448    /**
449     * Fetch a secret key backup from the server and try to restore a selected secret key from it.
450     *
451     * @param codeCallback callback for prompting the user to provide the secret backup code.
452     * @return fingerprint of the restored secret key
453     *
454     * @throws InterruptedException if the thread gets interrupted.
455     * @throws PubSubException.NotALeafNodeException if the private node is not a {@link LeafNode}.
456     * @throws XMPPException.XMPPErrorException in case of an XMPP protocol error.
457     * @throws SmackException.NotConnectedException if we are not connected.
458     * @throws SmackException.NoResponseException if the server doesn't respond.
459     * @throws InvalidBackupCodeException if the user-provided backup code is invalid.
460     * @throws SmackException.NotLoggedInException if we are not logged in
461     * @throws IOException IO is dangerous
462     * @throws MissingUserIdOnKeyException if the key that is to be imported is missing a user-id with our jid
463     * @throws NoBackupFoundException if no secret key backup has been found
464     * @throws PGPException in case the restored secret key is damaged.
465     */
466    public OpenPgpV4Fingerprint restoreSecretKeyServerBackup(AskForBackupCodeCallback codeCallback)
467            throws InterruptedException, PubSubException.NotALeafNodeException, XMPPException.XMPPErrorException,
468            SmackException.NotConnectedException, SmackException.NoResponseException,
469            InvalidBackupCodeException, SmackException.NotLoggedInException, IOException, MissingUserIdOnKeyException,
470            NoBackupFoundException, PGPException {
471        throwIfNoProviderSet();
472        throwIfNotAuthenticated();
473        SecretkeyElement backup = OpenPgpPubSubUtil.fetchSecretKey(pepManager);
474        if (backup == null) {
475            throw new NoBackupFoundException();
476        }
477
478        String backupCode = codeCallback.askForBackupCode();
479
480        PGPSecretKeyRing secretKeys = SecretKeyBackupHelper.restoreSecretKeyBackup(backup, backupCode);
481        provider.getStore().importSecretKey(getJidOrThrow(), secretKeys);
482        provider.getStore().importPublicKey(getJidOrThrow(), BCUtil.publicKeyRingFromSecretKeyRing(secretKeys));
483
484        ByteArrayOutputStream buffer = new ByteArrayOutputStream(2048);
485        for (PGPSecretKey sk : secretKeys) {
486            PGPPublicKey pk = sk.getPublicKey();
487            if (pk != null) pk.encode(buffer);
488        }
489        PGPPublicKeyRing publicKeys = new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator());
490        provider.getStore().importPublicKey(getJidOrThrow(), publicKeys);
491
492        return new OpenPgpV4Fingerprint(secretKeys);
493    }
494
495    /*
496    Private stuff.
497     */
498
499    /**
500     * {@link PepListener} that listens for changes to the OX public keys metadata node.
501     *
502     * @see <a href="https://xmpp.org/extensions/xep-0373.html#pubsub-notifications">XEP-0373 §4.4</a>
503     */
504    private final PepListener metadataListener = new PepListener() {
505        @Override
506        public void eventReceived(final EntityBareJid from, final EventElement event, final Message message) {
507            if (PEP_NODE_PUBLIC_KEYS.equals(event.getEvent().getNode())) {
508                final BareJid contact = from.asBareJid();
509                LOGGER.log(Level.INFO, "Received OpenPGP metadata update from " + contact);
510                Async.go(new Runnable() {
511                    @Override
512                    public void run() {
513                        ItemsExtension items = (ItemsExtension) event.getExtensions().get(0);
514                        PayloadItem<?> payload = (PayloadItem) items.getItems().get(0);
515                        PublicKeysListElement listElement = (PublicKeysListElement) payload.getPayload();
516
517                        processPublicKeysListElement(from, listElement);
518                    }
519                }, "ProcessOXMetadata");
520            }
521        }
522    };
523
524    private void processPublicKeysListElement(BareJid contact, PublicKeysListElement listElement) {
525        OpenPgpContact openPgpContact = getOpenPgpContact(contact.asEntityBareJidIfPossible());
526        try {
527            openPgpContact.updateKeys(connection(), listElement);
528        } catch (Exception e) {
529            LOGGER.log(Level.WARNING, "Could not update contacts keys", e);
530        }
531    }
532
533    /**
534     * Decrypt and or verify an {@link OpenPgpElement} and return the decrypted {@link OpenPgpMessage}.
535     *
536     * @param element {@link OpenPgpElement} containing the message.
537     * @param sender {@link OpenPgpContact} who sent the message.
538     *
539     * @return decrypted and/or verified message
540     *
541     * @throws SmackException.NotLoggedInException in case we aren't logged in (we need to know our jid)
542     * @throws IOException IO error (reading keys, streams etc)
543     * @throws PGPException in case of an PGP error
544     */
545    public OpenPgpMessage decryptOpenPgpElement(OpenPgpElement element, OpenPgpContact sender)
546            throws SmackException.NotLoggedInException, IOException, PGPException {
547        return provider.decryptAndOrVerify(element, getOpenPgpSelf(), sender);
548    }
549
550    private final IncomingChatMessageListener incomingOpenPgpMessageListener =
551            new IncomingChatMessageListener() {
552                @Override
553                public void newIncomingMessage(final EntityBareJid from, final Message message, Chat chat) {
554                    Async.go(new Runnable() {
555                        @Override
556                        public void run() {
557                            OpenPgpElement element = message.getExtension(OpenPgpElement.ELEMENT, OpenPgpElement.NAMESPACE);
558                            if (element == null) {
559                                // Message does not contain an OpenPgpElement -> discard
560                                return;
561                            }
562
563                            OpenPgpContact contact = getOpenPgpContact(from);
564
565                            OpenPgpMessage decrypted = null;
566                            OpenPgpContentElement contentElement = null;
567                            try {
568                                decrypted = decryptOpenPgpElement(element, contact);
569                                contentElement = decrypted.getOpenPgpContentElement();
570                            } catch (PGPException e) {
571                                LOGGER.log(Level.WARNING, "Could not decrypt incoming OpenPGP encrypted message", e);
572                            } catch (XmlPullParserException | IOException e) {
573                                LOGGER.log(Level.WARNING, "Invalid XML content of incoming OpenPGP encrypted message", e);
574                            } catch (SmackException.NotLoggedInException e) {
575                                LOGGER.log(Level.WARNING, "Cannot determine our JID, since we are not logged in.", e);
576                            }
577
578                            if (contentElement instanceof SigncryptElement) {
579                                for (SigncryptElementReceivedListener l : signcryptElementReceivedListeners) {
580                                    l.signcryptElementReceived(contact, message, (SigncryptElement) contentElement, decrypted.getMetadata());
581                                }
582                                return;
583                            }
584
585                            if (contentElement instanceof SignElement) {
586                                for (SignElementReceivedListener l : signElementReceivedListeners) {
587                                    l.signElementReceived(contact, message, (SignElement) contentElement, decrypted.getMetadata());
588                                }
589                                return;
590                            }
591
592                            if (contentElement instanceof CryptElement) {
593                                for (CryptElementReceivedListener l : cryptElementReceivedListeners) {
594                                    l.cryptElementReceived(contact, message, (CryptElement) contentElement, decrypted.getMetadata());
595                                }
596                                return;
597                            }
598
599                            else {
600                                throw new AssertionError("Invalid element received: " + contentElement.getClass().getName());
601                            }
602                        }
603                    });
604                }
605            };
606
607    /**
608     * Create a {@link PubkeyElement} which contains the OpenPGP public key of {@code owner} which belongs to
609     * the {@link OpenPgpV4Fingerprint} {@code fingerprint}.
610     *
611     * @param owner owner of the public key
612     * @param fingerprint fingerprint of the key
613     * @param date date of creation of the element
614     * @return {@link PubkeyElement} containing the key
615     *
616     * @throws MissingOpenPgpKeyException if the public key notated by the fingerprint cannot be found
617     */
618    private PubkeyElement createPubkeyElement(BareJid owner,
619                                              OpenPgpV4Fingerprint fingerprint,
620                                              Date date)
621            throws MissingOpenPgpKeyException, IOException, PGPException {
622        PGPPublicKeyRing ring = provider.getStore().getPublicKeyRing(owner, fingerprint);
623        if (ring != null) {
624            byte[] keyBytes = ring.getEncoded(true);
625            return createPubkeyElement(keyBytes, date);
626        }
627        throw new MissingOpenPgpKeyException(owner, fingerprint);
628    }
629
630    /**
631     * Create a {@link PubkeyElement} which contains the given {@code data} base64 encoded.
632     *
633     * @param bytes byte representation of an OpenPGP public key
634     * @param date date of creation of the element
635     * @return {@link PubkeyElement} containing the key
636     */
637    private static PubkeyElement createPubkeyElement(byte[] bytes, Date date) {
638        return new PubkeyElement(new PubkeyElement.PubkeyDataElement(Base64.encode(bytes)), date);
639    }
640
641    /**
642     * Register a {@link SigncryptElementReceivedListener} on the {@link OpenPgpManager}.
643     * That listener will get informed whenever a {@link SigncryptElement} has been received and successfully decrypted.
644     *
645     * Note: This method is not intended for clients to listen for incoming {@link SigncryptElement}s.
646     * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
647     * OpenPGP for XMPP: Instant Messaging.
648     *
649     * @param listener listener that gets registered
650     */
651    public void registerSigncryptReceivedListener(SigncryptElementReceivedListener listener) {
652        signcryptElementReceivedListeners.add(listener);
653    }
654
655    /**
656     * Unregister a prior registered {@link SigncryptElementReceivedListener}. That listener will no longer get
657     * informed about incoming decrypted {@link SigncryptElement}s.
658     *
659     * @param listener listener that gets unregistered
660     */
661    void unregisterSigncryptElementReceivedListener(SigncryptElementReceivedListener listener) {
662        signcryptElementReceivedListeners.remove(listener);
663    }
664
665    /**
666     * Register a {@link SignElementReceivedListener} on the {@link OpenPgpManager}.
667     * That listener will get informed whenever a {@link SignElement} has been received and successfully verified.
668     *
669     * Note: This method is not intended for clients to listen for incoming {@link SignElement}s.
670     * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
671     * OpenPGP for XMPP: Instant Messaging.
672     *
673     * @param listener listener that gets registered
674     */
675    void registerSignElementReceivedListener(SignElementReceivedListener listener) {
676        signElementReceivedListeners.add(listener);
677    }
678
679    /**
680     * Unregister a prior registered {@link SignElementReceivedListener}. That listener will no longer get
681     * informed about incoming decrypted {@link SignElement}s.
682     *
683     * @param listener listener that gets unregistered
684     */
685    void unregisterSignElementReceivedListener(SignElementReceivedListener listener) {
686        signElementReceivedListeners.remove(listener);
687    }
688
689    /**
690     * Register a {@link CryptElementReceivedListener} on the {@link OpenPgpManager}.
691     * That listener will get informed whenever a {@link CryptElement} has been received and successfully decrypted.
692     *
693     * Note: This method is not intended for clients to listen for incoming {@link CryptElement}s.
694     * Instead its purpose is to allow easy extension of XEP-0373 for custom OpenPGP profiles such as
695     * OpenPGP for XMPP: Instant Messaging.
696     *
697     * @param listener listener that gets registered
698     */
699    void registerCryptElementReceivedListener(CryptElementReceivedListener listener) {
700        cryptElementReceivedListeners.add(listener);
701    }
702
703    /**
704     * Unregister a prior registered {@link CryptElementReceivedListener}. That listener will no longer get
705     * informed about incoming decrypted {@link CryptElement}s.
706     *
707     * @param listener listener that gets unregistered
708     */
709    void unregisterCryptElementReceivedListener(CryptElementReceivedListener listener) {
710        cryptElementReceivedListeners.remove(listener);
711    }
712
713    /**
714     * Throw an {@link IllegalStateException} if no {@link OpenPgpProvider} is set.
715     * The OpenPgpProvider is used to process information related to RFC-4880.
716     */
717    private void throwIfNoProviderSet() {
718        if (provider == null) {
719            throw new IllegalStateException("No OpenPgpProvider set!");
720        }
721    }
722
723    /**
724     * Throw a {@link org.jivesoftware.smack.SmackException.NotLoggedInException} if the {@link XMPPConnection} of this
725     * manager is not authenticated at this point.
726     *
727     * @throws SmackException.NotLoggedInException if we are not authenticated
728     */
729    private void throwIfNotAuthenticated() throws SmackException.NotLoggedInException {
730        if (!connection().isAuthenticated()) {
731            throw new SmackException.NotLoggedInException();
732        }
733    }
734}