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