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}