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.util.Collection;
023import java.util.logging.Level;
024import java.util.logging.Logger;
025
026import org.jivesoftware.smack.SmackException.NoResponseException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.XMPPConnection;
029import org.jivesoftware.smack.XMPPException.XMPPErrorException;
030import org.jivesoftware.smack.util.Objects;
031import org.jivesoftware.smack.util.stringencoder.Base64;
032import org.jivesoftware.smackx.ox.OpenPgpContact;
033import org.jivesoftware.smackx.ox.OpenPgpMessage;
034import org.jivesoftware.smackx.ox.OpenPgpSelf;
035import org.jivesoftware.smackx.ox.element.CryptElement;
036import org.jivesoftware.smackx.ox.element.OpenPgpElement;
037import org.jivesoftware.smackx.ox.element.SignElement;
038import org.jivesoftware.smackx.ox.element.SigncryptElement;
039import org.jivesoftware.smackx.ox.store.definition.OpenPgpStore;
040import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
041import org.jivesoftware.smackx.pubsub.PubSubException.NotAPubSubNodeException;
042
043import org.bouncycastle.openpgp.PGPException;
044import org.bouncycastle.openpgp.PGPPublicKeyRing;
045import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
046import org.bouncycastle.util.io.Streams;
047import org.pgpainless.PGPainless;
048import org.pgpainless.algorithm.DocumentSignatureType;
049import org.pgpainless.decryption_verification.ConsumerOptions;
050import org.pgpainless.decryption_verification.DecryptionStream;
051import org.pgpainless.decryption_verification.MissingPublicKeyCallback;
052import org.pgpainless.decryption_verification.OpenPgpMetadata;
053import org.pgpainless.encryption_signing.EncryptionOptions;
054import org.pgpainless.encryption_signing.EncryptionStream;
055import org.pgpainless.encryption_signing.ProducerOptions;
056import org.pgpainless.encryption_signing.SigningOptions;
057
058public class PainlessOpenPgpProvider implements OpenPgpProvider {
059
060    private static final Logger LOGGER = Logger.getLogger(PainlessOpenPgpProvider.class.getName());
061
062    private final OpenPgpStore store;
063
064    public PainlessOpenPgpProvider(OpenPgpStore store) {
065        this.store = Objects.requireNonNull(store);
066    }
067
068    @Override
069    public OpenPgpStore getStore() {
070        return store;
071    }
072
073    @Override
074    public OpenPgpElementAndMetadata signAndEncrypt(SigncryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
075            throws IOException, PGPException {
076        InputStream plainText = element.toInputStream();
077        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
078
079        EncryptionOptions encOpts = EncryptionOptions.encryptCommunications();
080        for (OpenPgpContact contact : recipients) {
081            PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
082            if (keys == null) {
083                LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid());
084            }
085            encOpts.addRecipients(keys);
086        }
087
088        encOpts.addRecipients(self.getTrustedAnnouncedKeys());
089
090        SigningOptions signOpts = new SigningOptions();
091        signOpts.addInlineSignature(getStore().getKeyRingProtector(), self.getSigningKeyRing(),
092                DocumentSignatureType.BINARY_DOCUMENT);
093
094        EncryptionStream cipherStream = PGPainless.encryptAndOrSign()
095                .onOutputStream(cipherText)
096                .withOptions(ProducerOptions
097                        .signAndEncrypt(encOpts, signOpts)
098                        .setAsciiArmor(false));
099
100        Streams.pipeAll(plainText, cipherStream);
101        plainText.close();
102        cipherStream.flush();
103        cipherStream.close();
104        cipherText.close();
105
106        String base64 = Base64.encodeToString(cipherText.toByteArray());
107        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
108
109        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
110    }
111
112    @Override
113    public OpenPgpElementAndMetadata sign(SignElement element, OpenPgpSelf self)
114            throws IOException, PGPException {
115        InputStream plainText = element.toInputStream();
116        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
117
118        EncryptionStream cipherStream = PGPainless.encryptAndOrSign()
119                .onOutputStream(cipherText)
120                .withOptions(ProducerOptions.sign(new SigningOptions()
121                        .addInlineSignature(getStore().getKeyRingProtector(), self.getSigningKeyRing(),
122                                "xmpp:" + self.getJid().toString(), DocumentSignatureType.BINARY_DOCUMENT)
123                ).setAsciiArmor(false));
124
125        Streams.pipeAll(plainText, cipherStream);
126        plainText.close();
127        cipherStream.flush();
128        cipherStream.close();
129        cipherText.close();
130
131        String base64 = Base64.encodeToString(cipherText.toByteArray());
132        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
133
134        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
135    }
136
137    @Override
138    public OpenPgpElementAndMetadata encrypt(CryptElement element, OpenPgpSelf self, Collection<OpenPgpContact> recipients)
139            throws IOException, PGPException {
140        InputStream plainText = element.toInputStream();
141        ByteArrayOutputStream cipherText = new ByteArrayOutputStream();
142
143        EncryptionOptions encOpts = EncryptionOptions.encryptCommunications();
144        for (OpenPgpContact contact : recipients) {
145            PGPPublicKeyRingCollection keys = contact.getTrustedAnnouncedKeys();
146            if (keys == null) {
147                LOGGER.log(Level.WARNING, "There are no suitable keys for contact " + contact.getJid());
148            }
149            encOpts.addRecipients(keys);
150        }
151
152        encOpts.addRecipients(self.getTrustedAnnouncedKeys());
153
154        EncryptionStream cipherStream = PGPainless.encryptAndOrSign()
155                .onOutputStream(cipherText)
156                .withOptions(ProducerOptions
157                        .encrypt(encOpts)
158                        .setAsciiArmor(false)
159                );
160
161        Streams.pipeAll(plainText, cipherStream);
162        plainText.close();
163        cipherStream.flush();
164        cipherStream.close();
165        cipherText.close();
166
167        String base64 = Base64.encodeToString(cipherText.toByteArray());
168        OpenPgpElement openPgpElement = new OpenPgpElement(base64);
169
170        return new OpenPgpElementAndMetadata(openPgpElement, cipherStream.getResult());
171    }
172
173    @Override
174    public OpenPgpMessage decryptAndOrVerify(XMPPConnection connection, OpenPgpElement element, final OpenPgpSelf self, final OpenPgpContact sender) throws IOException, PGPException {
175        ByteArrayOutputStream plainText = new ByteArrayOutputStream();
176        InputStream cipherText = element.toInputStream();
177
178        PGPPublicKeyRingCollection announcedPublicKeys = sender.getAnnouncedPublicKeys();
179        if (announcedPublicKeys == null) {
180            try {
181                sender.updateKeys(connection);
182                announcedPublicKeys = sender.getAnnouncedPublicKeys();
183            } catch (InterruptedException | NotALeafNodeException | NotAPubSubNodeException | NotConnectedException
184                    | NoResponseException | XMPPErrorException e) {
185                throw new PGPException("Abort decryption due to lack of keys", e);
186            }
187        }
188
189        MissingPublicKeyCallback missingPublicKeyCallback = new MissingPublicKeyCallback() {
190
191            @Override
192            public PGPPublicKeyRing onMissingPublicKeyEncountered(Long keyId) {
193                try {
194                    sender.updateKeys(connection);
195                    PGPPublicKeyRingCollection anyKeys = sender.getAnyPublicKeys();
196                    for (PGPPublicKeyRing ring : anyKeys) {
197                        if (ring.getPublicKey(keyId) != null) {
198                            return ring;
199                        }
200                    }
201                    return null;
202                } catch (InterruptedException | NotALeafNodeException | NotAPubSubNodeException | NotConnectedException
203                        | NoResponseException | XMPPErrorException | IOException | PGPException e) {
204                    LOGGER.log(Level.WARNING, "Cannot fetch missing key " + keyId, e);
205                    return null;
206                }
207            }
208        };
209
210        DecryptionStream cipherStream = PGPainless.decryptAndOrVerify()
211                .onInputStream(cipherText)
212                .withOptions(new ConsumerOptions()
213                        .addDecryptionKeys(self.getSecretKeys(), getStore().getKeyRingProtector())
214                        .addVerificationCerts(announcedPublicKeys)
215                        .setMissingCertificateCallback(missingPublicKeyCallback));
216
217        Streams.pipeAll(cipherStream, plainText);
218
219        cipherText.close();
220        cipherStream.close();
221        plainText.close();
222
223        OpenPgpMetadata info = cipherStream.getMetadata().toLegacyMetadata();
224
225        OpenPgpMessage.State state;
226        if (info.isSigned()) {
227            if (info.isEncrypted()) {
228                state = OpenPgpMessage.State.signcrypt;
229            } else {
230                state = OpenPgpMessage.State.sign;
231            }
232        } else if (info.isEncrypted()) {
233            state = OpenPgpMessage.State.crypt;
234        } else {
235            throw new PGPException("Received message appears to be neither encrypted, nor signed.");
236        }
237
238        return new OpenPgpMessage(plainText.toByteArray(), state, info);
239    }
240}