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.crypto;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.security.Security;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.jivesoftware.smack.XMPPConnection;
029import org.jivesoftware.smack.util.Objects;
030import org.jivesoftware.smack.util.stringencoder.Base64;
031import org.jivesoftware.smackx.ox.OpenPgpContact;
032import org.jivesoftware.smackx.ox.OpenPgpMessage;
033import org.jivesoftware.smackx.ox.OpenPgpSelf;
034import org.jivesoftware.smackx.ox.element.CryptElement;
035import org.jivesoftware.smackx.ox.element.OpenPgpElement;
036import org.jivesoftware.smackx.ox.element.SignElement;
037import org.jivesoftware.smackx.ox.element.SigncryptElement;
038import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
039
040import org.bouncycastle.jce.provider.BouncyCastleProvider;
041import org.bouncycastle.openpgp.PGPException;
042import org.bouncycastle.openpgp.PGPPublicKey;
043import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
044import org.bouncycastle.util.io.Streams;
045import org.pgpainless.PGPainless;
046import org.pgpainless.decryption_verification.DecryptionStream;
047import org.pgpainless.decryption_verification.MissingPublicKeyCallback;
048import org.pgpainless.decryption_verification.OpenPgpMetadata;
049import org.pgpainless.encryption_signing.EncryptionStream;
050
051public class PainlessOpenPgpProvider implements OpenPgpProvider {
052
053    private static final Logger LOGGER = Logger.getLogger(PainlessOpenPgpProvider.class.getName());
054
055    static {
056        // Remove any BC providers and add a fresh one.
057        // This is done, since older Android versions ship with a crippled BC provider.
058        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
059        Security.addProvider(new BouncyCastleProvider());
060    }
061
062    private final XMPPConnection connection;
063    private final OpenPgpStore store;
064
065    public PainlessOpenPgpProvider(XMPPConnection connection, OpenPgpStore store) {
066        this.connection = Objects.requireNonNull(connection);
067        this.store = Objects.requireNonNull(store);
068    }
069
070    @Override
071    public OpenPgpStore getStore() {
072        return store;
073    }
074
075    @Override
076    public OpenPgpElementAndMetadata signAndEncrypt(SigncryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
077            throws IOException, PGPException {
078        InputStream plainText = element.toInputStream();
079        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
080
081        ArrayList<PGPPublicKeyRingCollection> recipientKeys = new ArrayList<>();
082        for (OpenPgpContact contact : recipients) {
083            PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
084            if (keys != null) {
085                recipientKeys.add(keys);
086            } else {
087                LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid().toString());
088            }
089        }
090
091        EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
092                .toRecipients(recipientKeys.toArray(new PGPPublicKeyRingCollection[] {}))
093                .andToSelf(self.getTrustedAnnouncedKeys())
094                .usingSecureAlgorithms()
095                .signWith(getStore().getKeyRingProtector(), self.getSigningKeyRing())
096                .noArmor();
097
098        Streams.pipeAll(plainText, cipherStream);
099        plainText.close();
100        cipherStream.flush();
101        cipherStream.close();
102        cipherText.close();
103
104        String base64 = Base64.encodeToString(cipherText.toByteArray());
105        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
106
107        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
108    }
109
110    @Override
111    public OpenPgpElementAndMetadata sign(SignElement element, OpenPgpSelf self)
112            throws IOException, PGPException {
113        InputStream plainText = element.toInputStream();
114        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
115
116        EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
117                .doNotEncrypt()
118                .signWith(getStore().getKeyRingProtector(), self.getSigningKeyRing())
119                .noArmor();
120
121        Streams.pipeAll(plainText, cipherStream);
122        plainText.close();
123        cipherStream.flush();
124        cipherStream.close();
125        cipherText.close();
126
127        String base64 = Base64.encodeToString(cipherText.toByteArray());
128        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
129
130        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
131    }
132
133    @Override
134    public OpenPgpElementAndMetadata encrypt(CryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
135            throws IOException, PGPException {
136        InputStream plainText = element.toInputStream();
137        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
138
139        ArrayList<PGPPublicKeyRingCollection> recipientKeys = new ArrayList<>();
140        for (OpenPgpContact contact : recipients) {
141            PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
142            if (keys != null) {
143                recipientKeys.add(keys);
144            } else {
145                LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid().toString());
146            }
147        }
148
149        EncryptionStream cipherStream = PGPainless.createEncryptor().onOutputStream(cipherText)
150                .toRecipients(recipientKeys.toArray(new PGPPublicKeyRingCollection[] {}))
151                .andToSelf(self.getTrustedAnnouncedKeys())
152                .usingSecureAlgorithms()
153                .doNotSign()
154                .noArmor();
155
156        Streams.pipeAll(plainText, cipherStream);
157        plainText.close();
158        cipherStream.flush();
159        cipherStream.close();
160        cipherText.close();
161
162        String base64 = Base64.encodeToString(cipherText.toByteArray());
163        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
164
165        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
166    }
167
168    @Override
169    public OpenPgpMessage decryptAndOrVerify(OpenPgpElement element, final OpenPgpSelf self, final OpenPgpContact sender) throws IOException, PGPException {
170        ByteArrayOutputStream plainText = new ByteArrayOutputStream();
171        InputStream cipherText = element.toInputStream();
172
173        PGPPublicKeyRingCollection announcedPublicKeys = sender.getAnnouncedPublicKeys();
174        if (announcedPublicKeys == null) {
175            LOGGER.log(Level.INFO, "Received a message from " + sender.getJid() + " but we have no keys yet. Try fetching them.");
176            try {
177                sender.updateKeys(connection);
178                announcedPublicKeys = sender.getAnnouncedPublicKeys();
179            } catch (Exception e) {
180                LOGGER.log(Level.SEVERE, "Fetching keys of " + sender.getJid() + " failed. Abort decryption and discard message.", e);
181                throw new PGPException("Abort decryption due to lack of keys.", e);
182            }
183        }
184
185        MissingPublicKeyCallback missingPublicKeyCallback = new MissingPublicKeyCallback() {
186            @Override
187            public PGPPublicKey onMissingPublicKeyEncountered(Long keyId) {
188                try {
189                    sender.updateKeys(connection);
190                    return sender.getAnyPublicKeys().getPublicKey(keyId);
191                } catch (Exception e) {
192                    LOGGER.log(Level.WARNING, "Cannot fetch missing key " + keyId, e);
193                    return null;
194                }
195            }
196        };
197
198        DecryptionStream cipherStream = PGPainless.createDecryptor().onInputStream(cipherText)
199                .decryptWith(getStore().getKeyRingProtector(), self.getSecretKeys())
200                .verifyWith(announcedPublicKeys)
201                .handleMissingPublicKeysWith(missingPublicKeyCallback)
202                .build();
203
204        Streams.pipeAll(cipherStream, plainText);
205
206        cipherText.close();
207        cipherStream.close();
208        plainText.close();
209
210        OpenPgpMetadata info = cipherStream.getResult();
211
212        OpenPgpMessage.State state;
213        if (info.isSigned()) {
214            if (info.isEncrypted()) {
215                state = OpenPgpMessage.State.signcrypt;
216            } else {
217                state = OpenPgpMessage.State.sign;
218            }
219        } else if (info.isEncrypted()) {
220            state = OpenPgpMessage.State.crypt;
221        } else {
222            throw new PGPException("Received message appears to be neither encrypted, nor signed.");
223        }
224
225        return new OpenPgpMessage(plainText.toByteArray(), state, info);
226    }
227}