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