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.ByteArrayOutputStream;
020import java.io.IOException;
021import java.security.SecureRandom;
022import java.util.Set;
023
024import org.jivesoftware.smack.util.stringencoder.Base64;
025import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
026import org.jivesoftware.smackx.ox.element.SecretkeyElement;
027import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
028import org.jivesoftware.smackx.ox.exception.MissingOpenPgpKeyException;
029
030import org.bouncycastle.openpgp.PGPException;
031import org.bouncycastle.openpgp.PGPSecretKeyRing;
032import org.jxmpp.jid.BareJid;
033import org.pgpainless.PGPainless;
034import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
035import org.pgpainless.key.OpenPgpV4Fingerprint;
036import org.pgpainless.util.Passphrase;
037
038/**
039 * Helper class which provides some functions needed for backup/restore of the users secret key to/from their private
040 * PubSub node.
041 */
042public class SecretKeyBackupHelper {
043
044    /**
045     * Generate a secure backup code.
046     * This code can be used to encrypt a secret key backup and follows the form described in XEP-0373 §5.3.
047     *
048     * @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
049     *     XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
050     *
051     * @return backup code
052     */
053    public static String generateBackupPassword() {
054        final String alphabet = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ";
055        final int len = alphabet.length();
056        SecureRandom random = new SecureRandom();
057        StringBuilder code = new StringBuilder(29);
058
059        // 6 blocks
060        for (int i = 0; i < 6; i++) {
061
062            // of 4 chars
063            for (int j = 0; j < 4; j++) {
064                char c = alphabet.charAt(random.nextInt(len));
065                code.append(c);
066            }
067
068            // dash after every block except the last one
069            if (i != 5) {
070                code.append('-');
071            }
072        }
073        return code.toString();
074    }
075
076    /**
077     * Create a {@link SecretkeyElement} which contains the secret keys listed in {@code fingerprints} and is encrypted
078     * symmetrically using the {@code backupCode}.
079     *
080     * @param provider {@link OpenPgpProvider} for symmetric encryption.
081     * @param owner owner of the secret keys (usually our jid).
082     * @param fingerprints set of {@link OpenPgpV4Fingerprint}s of the keys which are going to be backed up.
083     * @param backupCode passphrase for symmetric encryption.
084     * @return {@link SecretkeyElement}
085     *
086     * @throws PGPException PGP is brittle
087     * @throws IOException IO is dangerous
088     * @throws MissingOpenPgpKeyException in case one of the keys whose fingerprint is in {@code fingerprints} is
089     * not accessible.
090     */
091    public static SecretkeyElement createSecretkeyElement(OpenPgpProvider provider,
092                                                    BareJid owner,
093                                                    Set<OpenPgpV4Fingerprint> fingerprints,
094                                                    String backupCode)
095            throws PGPException, IOException, MissingOpenPgpKeyException {
096        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
097
098        for (OpenPgpV4Fingerprint fingerprint : fingerprints) {
099
100                PGPSecretKeyRing key = provider.getStore().getSecretKeyRing(owner, fingerprint);
101                if (key == null) {
102                    throw new MissingOpenPgpKeyException(owner, fingerprint);
103                }
104
105                byte[] bytes = key.getEncoded();
106                buffer.write(bytes);
107        }
108        return createSecretkeyElement(buffer.toByteArray(), backupCode);
109    }
110
111    /**
112     * Create a {@link SecretkeyElement} which contains the secret keys which are serialized in {@code keys} and is
113     * symmetrically encrypted using the {@code backupCode}.
114     *
115     * @see <a href="https://xmpp.org/extensions/xep-0373.html#backup-encryption">
116     *     XEP-0373 §5.4 Encrypting the Secret Key Backup</a>
117     *
118     * @param keys serialized OpenPGP secret keys in transferable key format
119     * @param backupCode passphrase for symmetric encryption
120     * @return {@link SecretkeyElement}
121     *
122     * @throws PGPException PGP is brittle
123     * @throws IOException IO is dangerous
124     */
125    public static SecretkeyElement createSecretkeyElement(byte[] keys,
126                                                          String backupCode)
127            throws PGPException, IOException {
128        byte[] encrypted = PGPainless.encryptWithPassword(keys, new Passphrase(backupCode.toCharArray()),
129                SymmetricKeyAlgorithm.AES_256);
130        return new SecretkeyElement(Base64.encode(encrypted));
131    }
132
133    /**
134     * Decrypt a secret key backup and return the {@link PGPSecretKeyRing} contained in it.
135     * TODO: Return a PGPSecretKeyRingCollection instead?
136     *
137     * @param backup encrypted {@link SecretkeyElement} containing the backup
138     * @param backupCode passphrase for decrypting the {@link SecretkeyElement}.
139     * @return the
140     * @throws InvalidBackupCodeException in case the provided backup code is invalid.
141     * @throws IOException IO is dangerous.
142     * @throws PGPException PGP is brittle.
143     */
144    public static PGPSecretKeyRing restoreSecretKeyBackup(SecretkeyElement backup, String backupCode)
145            throws InvalidBackupCodeException, IOException, PGPException {
146        byte[] encrypted = Base64.decode(backup.getB64Data());
147
148        byte[] decrypted;
149        try {
150            decrypted = PGPainless.decryptWithPassword(encrypted, new Passphrase(backupCode.toCharArray()));
151        } catch (IOException | PGPException e) {
152            throw new InvalidBackupCodeException("Could not decrypt secret key backup. Possibly wrong passphrase?", e);
153        }
154
155        return PGPainless.readKeyRing().secretKeyRing(decrypted);
156    }
157}