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}