001/**
002 *
003 * Copyright 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.util;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.Set;
024
025import org.jivesoftware.smack.util.StringUtils;
026import org.jivesoftware.smack.util.stringencoder.Base64;
027
028import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
029import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
030import org.jivesoftware.smackx.ox.element.SecretkeyElement;
031import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
032import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
033
034import org.bouncycastle.openpgp.PGPException;
035import org.bouncycastle.openpgp.PGPSecretKeyRing;
036import org.bouncycastle.util.io.Streams;
037import org.jxmpp.jid.BareJid;
038import org.pgpainless.PGPainless;
039import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
040import org.pgpainless.decryption_verification.ConsumerOptions;
041import org.pgpainless.decryption_verification.DecryptionStream;
042import org.pgpainless.encryption_signing.EncryptionOptions;
043import org.pgpainless.encryption_signing.EncryptionStream;
044import org.pgpainless.encryption_signing.ProducerOptions;
045import org.pgpainless.exception.MissingDecryptionMethodException;
046import org.pgpainless.key.OpenPgpV4Fingerprint;
047import org.pgpainless.util.Passphrase;
048
049/**
050 * Helper class which provides some functions needed for backup/restore of the users secret key to/from their private
051 * PubSub node.
052 */
053public class SecretKeyBackupHelper {
054
055    /**
056     * Generate a secure backup code.
057     * This code can be used to encrypt a secret key backup and follows the form described in XEP-0373 §5.3.
058     *
059     * @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
060     *     XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
061     *
062     * @return backup code
063     */
064    public static OpenPgpSecretKeyBackupPassphrase generateBackupPassword() {
065        return new OpenPgpSecretKeyBackupPassphrase(StringUtils.secureOfflineAttackSafeRandomString());
066    }
067
068    /**
069     * Create a {@link SecretkeyElement} which contains the secret keys listed in {@code fingerprints} and is encrypted
070     * symmetrically using the {@code backupCode}.
071     *
072     * @param provider {@link OpenPgpProvider} for symmetric encryption.
073     * @param owner owner of the secret keys (usually our jid).
074     * @param fingerprints set of {@link OpenPgpV4Fingerprint}s of the keys which are going to be backed up.
075     * @param backupCode passphrase for symmetric encryption.
076     * @return {@link SecretkeyElement}
077     *
078     * @throws PGPException PGP is brittle
079     * @throws IOException IO is dangerous
080     * @throws MissingOpenPgpKeyException in case one of the keys whose fingerprint is in {@code fingerprints} is
081     * not accessible.
082     */
083    public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider,
084                                                          BareJid owner,
085                                                          Set<OpenPgpV4Fingerprint> fingerprints,
086                                                          OpenPgpSecretKeyBackupPassphrase backupCode)
087            throws PGPException, IOException, MissingOpenPgpKeyException {
088        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
089
090        for (OpenPgpV4Fingerprint fingerprint : fingerprints) {
091
092            PGPSecretKeyRing key = provider.getStore().getSecretKeyRing(owner, fingerprint);
093            if (key == null) {
094                throw new MissingOpenPgpKeyException(owner, fingerprint);
095            }
096
097            byte[] bytes = key.getEncoded();
098            buffer.write(bytes);
099        }
100        return createSecretkeyElement(buffer.toByteArray(), backupCode);
101    }
102
103    /**
104     * Create a {@link SecretkeyElement} which contains the secret keys which are serialized in {@code keys} and is
105     * symmetrically encrypted using the {@code backupCode}.
106     *
107     * @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
108     *     XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
109     *
110     * @param keys serialized OpenPGP secret keys in transferable key format
111     * @param backupCode passphrase for symmetric encryption
112     * @return {@link SecretkeyElement}
113     *
114     * @throws PGPException PGP is brittle
115     * @throws IOException IO is dangerous
116     */
117    public static SecretkeyElement createSecretkeyElement(byte[] keys,
118                                                          OpenPgpSecretKeyBackupPassphrase backupCode)
119            throws PGPException, IOException {
120        InputStream keyStream = new ByteArrayInputStream(keys);
121        ByteArrayOutputStream cryptOut = new ByteArrayOutputStream();
122        EncryptionOptions encOpts = new EncryptionOptions()
123                .addPassphrase(Passphrase.fromPassword(backupCode.toString()));
124        encOpts.overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_256);
125
126        EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
127                .onOutputStream(cryptOut)
128                .withOptions(ProducerOptions.encrypt(encOpts)
129                        .setAsciiArmor(false));
130
131        Streams.pipeAll(keyStream, encryptionStream);
132        encryptionStream.close();
133
134        return new SecretkeyElement(Base64.encode(cryptOut.toByteArray()));
135    }
136
137    /**
138     * Decrypt a secret key backup and return the {@link PGPSecretKeyRing} contained in it.
139     * TODO: Return a PGPSecretKeyRingCollection instead?
140     *
141     * @param backup encrypted {@link SecretkeyElement} containing the backup
142     * @param backupCode passphrase for decrypting the {@link SecretkeyElement}.
143     * @return the TODO javadoc me please
144     * @throws InvalidBackupCodeException in case the provided backup code is invalid.
145     * @throws IOException IO is dangerous.
146     * @throws PGPException PGP is brittle.
147     */
148    public static PGPSecretKeyRing restoreSecretKeyBackup(SecretkeyElement backup, OpenPgpSecretKeyBackupPassphrase backupCode)
149            throws InvalidBackupCodeException, IOException, PGPException {
150        byte[] encrypted = Base64.decode(backup.getB64Data());
151        InputStream encryptedIn = new ByteArrayInputStream(encrypted);
152        ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
153
154        try {
155            DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
156                    .onInputStream(encryptedIn)
157                    .withOptions(new ConsumerOptions()
158                            .addDecryptionPassphrase(Passphrase.fromPassword(backupCode.toString())));
159
160            Streams.pipeAll(decryptionStream, plaintextOut);
161            decryptionStream.close();
162        } catch (MissingDecryptionMethodException e) {
163            throw new InvalidBackupCodeException("Could not decrypt secret key backup. Possibly wrong passphrase?", e);
164        }
165
166        byte[] decrypted = plaintextOut.toByteArray();
167        return PGPainless.readKeyRing().secretKeyRing(decrypted);
168    }
169}