From 7d374f10a723c254533571fa860e8b3a53067b00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 26 Dec 2020 19:04:27 +0100 Subject: [PATCH] Allow encryption and decryption using symmetric passphrases in the main API --- .../main/java/org/pgpainless/PGPainless.java | 6 ++ .../DecryptionBuilder.java | 13 ++- .../DecryptionBuilderInterface.java | 10 ++ .../DecryptionStreamFactory.java | 58 ++++++++--- .../encryption_signing/EncryptionBuilder.java | 83 +++++++++++----- .../EncryptionBuilderInterface.java | 12 +++ .../encryption_signing/EncryptionStream.java | 11 ++- .../LegacySymmetricEncryptionTest.java | 67 +++++++++++++ .../SymmetricEncryptionTest.java | 98 ++++++++++++------- .../org/pgpainless/sop/commands/Encrypt.java | 29 +++--- 10 files changed, 303 insertions(+), 84 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java index 2507aef9..3fd90f82 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java @@ -116,7 +116,10 @@ public class PGPainless { * * @throws IOException IO is dangerous. * @throws PGPException PGP is brittle. + * @deprecated use {@link #encryptAndOrSign()} instead and provide a passphrase in + * {@link org.pgpainless.encryption_signing.EncryptionBuilderInterface.ToRecipients#forPassphrases(Passphrase...)}. */ + @Deprecated public static byte[] encryptWithPassword(@Nonnull byte[] data, @Nonnull Passphrase password, @Nonnull SymmetricKeyAlgorithm algorithm) throws IOException, PGPException { return SymmetricEncryptorDecryptor.symmetricallyEncrypt(data, password, algorithm, CompressionAlgorithm.UNCOMPRESSED); @@ -131,7 +134,10 @@ public class PGPainless { * @return decrypted data. * @throws IOException IO is dangerous. * @throws PGPException PGP is brittle. + * @deprecated Use {@link #decryptAndOrVerify()} instead and provide the decryption passphrase in + * {@link org.pgpainless.decryption_verification.DecryptionBuilder.DecryptWith#decryptWith(Passphrase)}. */ + @Deprecated public static byte[] decryptWithPassword(@Nonnull byte[] data, @Nonnull Passphrase password) throws IOException, PGPException { return SymmetricEncryptorDecryptor.symmetricallyDecrypt(data, password); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 1501c5da..f78e03c4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -37,12 +37,14 @@ import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; public class DecryptionBuilder implements DecryptionBuilderInterface { private InputStream inputStream; private PGPSecretKeyRingCollection decryptionKeys; private SecretKeyRingProtector decryptionKeyDecryptor; + private Passphrase decryptionPassphrase; private List detachedSignatures; private Set verificationKeys = new HashSet<>(); private MissingPublicKeyCallback missingPublicKeyCallback = null; @@ -64,6 +66,15 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { return new VerifyImpl(); } + @Override + public Verify decryptWith(@Nonnull Passphrase passphrase) { + if (passphrase.isEmpty()) { + throw new IllegalArgumentException("Passphrase MUST NOT be empty."); + } + DecryptionBuilder.this.decryptionPassphrase = passphrase; + return new VerifyImpl(); + } + @Override public Verify doNotDecrypt() { DecryptionBuilder.this.decryptionKeys = null; @@ -194,7 +205,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override public DecryptionStream build() throws IOException, PGPException { return DecryptionStreamFactory.create(inputStream, - decryptionKeys, decryptionKeyDecryptor, detachedSignatures, verificationKeys, missingPublicKeyCallback); + decryptionKeys, decryptionKeyDecryptor, decryptionPassphrase, detachedSignatures, verificationKeys, missingPublicKeyCallback); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index cc41f5d6..608c5f63 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -31,6 +31,7 @@ import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.util.Passphrase; public interface DecryptionBuilderInterface { @@ -67,6 +68,15 @@ public interface DecryptionBuilderInterface { */ Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings); + /** + * Decrypt the encrypted data using a passphrase. + * Note: The passphrase MUST NOT be empty. + * + * @param passphrase passphrase + * @return api handle + */ + Verify decryptWith(@Nonnull Passphrase passphrase); + Verify doNotDecrypt(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index 784d9bab..130eb537 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -30,12 +30,14 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; @@ -45,15 +47,19 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; public final class DecryptionStreamFactory { @@ -62,6 +68,7 @@ public final class DecryptionStreamFactory { private final PGPSecretKeyRingCollection decryptionKeys; private final SecretKeyRingProtector decryptionKeyDecryptor; + private final Passphrase decryptionPassphrase; private final Set verificationKeys = new HashSet<>(); private final MissingPublicKeyCallback missingPublicKeyCallback; @@ -72,10 +79,12 @@ public final class DecryptionStreamFactory { private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, + @Nullable Passphrase decryptionPassphrase, @Nullable Set verificationKeys, @Nullable MissingPublicKeyCallback missingPublicKeyCallback) { this.decryptionKeys = decryptionKeys; this.decryptionKeyDecryptor = decryptor; + this.decryptionPassphrase = decryptionPassphrase; this.verificationKeys.addAll(verificationKeys != null ? verificationKeys : Collections.emptyList()); this.missingPublicKeyCallback = missingPublicKeyCallback; } @@ -83,13 +92,14 @@ public final class DecryptionStreamFactory { public static DecryptionStream create(@Nonnull InputStream inputStream, @Nullable PGPSecretKeyRingCollection decryptionKeys, @Nullable SecretKeyRingProtector decryptor, + @Nullable Passphrase decryptionPassphrase, @Nullable List detachedSignatures, @Nullable Set verificationKeys, @Nullable MissingPublicKeyCallback missingPublicKeyCallback) throws IOException, PGPException { InputStream pgpInputStream; - DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, verificationKeys, - missingPublicKeyCallback); + DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, + decryptionPassphrase, verificationKeys, missingPublicKeyCallback); if (detachedSignatures != null) { pgpInputStream = inputStream; @@ -171,7 +181,7 @@ public final class DecryptionStreamFactory { private InputStream decrypt(@Nonnull PGPEncryptedDataList encryptedDataList) throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); + Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); if (!encryptedDataIterator.hasNext()) { throw new PGPException("Decryption failed - EncryptedDataList has no items"); } @@ -179,19 +189,38 @@ public final class DecryptionStreamFactory { PGPPrivateKey decryptionKey = null; PGPPublicKeyEncryptedData encryptedSessionKey = null; while (encryptedDataIterator.hasNext()) { - PGPPublicKeyEncryptedData encryptedData = (PGPPublicKeyEncryptedData) encryptedDataIterator.next(); - long keyId = encryptedData.getKeyID(); + PGPEncryptedData encryptedData = encryptedDataIterator.next(); - resultBuilder.addRecipientKeyId(keyId); - LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); + if (encryptedData instanceof PGPPBEEncryptedData) { - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - if (secretKey != null) { - LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); - // Watch out! This assignment is possibly done multiple times. - encryptedSessionKey = encryptedData; - decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); - resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData; + if (decryptionPassphrase != null) { + PBEDataDecryptorFactory passphraseDecryptor = new BcPBEDataDecryptorFactory( + decryptionPassphrase.getChars(), new BcPGPDigestCalculatorProvider()); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( + pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); + resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + resultBuilder.setIntegrityProtected(pbeEncryptedData.isIntegrityProtected()); + return pbeEncryptedData.getDataStream(passphraseDecryptor); + } + + } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; + long keyId = publicKeyEncryptedData.getKeyID(); + + resultBuilder.addRecipientKeyId(keyId); + LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); + + if (decryptionKeys != null) { + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + if (secretKey != null) { + LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); + // Watch out! This assignment is possibly done multiple times. + encryptedSessionKey = publicKeyEncryptedData; + decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); + resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + } + } } } @@ -200,6 +229,7 @@ public final class DecryptionStreamFactory { } PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm .fromId(encryptedSessionKey.getSymmetricAlgorithm(keyDecryptor)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java index a1ea9536..b0f6d022 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java @@ -17,8 +17,10 @@ package org.pgpainless.encryption_signing; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -49,11 +51,13 @@ import org.pgpainless.key.selection.key.util.And; import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.Passphrase; public class EncryptionBuilder implements EncryptionBuilderInterface { private OutputStream outputStream; private final Set encryptionKeys = new HashSet<>(); + private final Set encryptionPassphrases = new HashSet<>(); private boolean detachedSignature = false; private SignatureType signatureType = SignatureType.BINARY_DOCUMENT; private final Set signingKeys = new HashSet<>(); @@ -73,16 +77,20 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKey... keys) { - for (PGPPublicKey k : keys) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); - } else { - throw new IllegalArgumentException("Key " + k.getKeyID() + " is not a valid encryption key."); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKey k : keys) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } else { + throw new IllegalArgumentException("Key " + k.getKeyID() + " is not a valid encryption key."); + } } - } - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -90,16 +98,19 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys) { - for (PGPPublicKeyRing ring : keys) { - for (PGPPublicKey k : ring) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKeyRing ring : keys) { + for (PGPPublicKey k : ring) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } } } - } - - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -107,18 +118,23 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... keys) { - for (PGPPublicKeyRingCollection collection : keys) { - for (PGPPublicKeyRing ring : collection) { - for (PGPPublicKey k : ring) { - if (encryptionKeySelector().accept(null, k)) { - EncryptionBuilder.this.encryptionKeys.add(k); + if (keys.length != 0) { + List encryptionKeys = new ArrayList<>(); + for (PGPPublicKeyRingCollection collection : keys) { + for (PGPPublicKeyRing ring : collection) { + for (PGPPublicKey k : ring) { + if (encryptionKeySelector().accept(null, k)) { + encryptionKeys.add(k); + } } } } - } - if (EncryptionBuilder.this.encryptionKeys.isEmpty()) { - throw new IllegalStateException("No valid encryption keys found!"); + if (encryptionKeys.isEmpty()) { + throw new IllegalStateException("No valid encryption keys found!"); + } + + EncryptionBuilder.this.encryptionKeys.addAll(encryptionKeys); } return new WithAlgorithmsImpl(); @@ -149,6 +165,19 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new WithAlgorithmsImpl(); } + @Override + public WithAlgorithms forPassphrases(Passphrase... passphrases) { + List passphraseList = new ArrayList<>(); + for (Passphrase passphrase : passphrases) { + if (passphrase.isEmpty()) { + throw new IllegalArgumentException("Passphrase must not be empty."); + } + passphraseList.add(passphrase); + } + EncryptionBuilder.this.encryptionPassphrases.addAll(passphraseList); + return new WithAlgorithmsImpl(); + } + @Override public DetachedSign doNotEncrypt() { return new DetachedSignImpl(); @@ -243,6 +272,11 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new DetachedSignImpl(); } + + @Override + public ToRecipients and() { + return new ToRecipientsImpl(); + } } class DetachedSignImpl implements DetachedSign { @@ -379,6 +413,7 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { return new EncryptionStream( EncryptionBuilder.this.outputStream, EncryptionBuilder.this.encryptionKeys, + EncryptionBuilder.this.encryptionPassphrases, EncryptionBuilder.this.detachedSignature, signatureType, privateKeys, diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java index 22645d1d..5d1b4281 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java @@ -36,6 +36,7 @@ import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.selection.keyring.PublicKeyRingSelectionStrategy; import org.pgpainless.key.selection.keyring.SecretKeyRingSelectionStrategy; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.Passphrase; public interface EncryptionBuilderInterface { @@ -85,6 +86,15 @@ public interface EncryptionBuilderInterface { WithAlgorithms toRecipients(@Nonnull PublicKeyRingSelectionStrategy selectionStrategy, @Nonnull MultiMap keys); + /** + * Encrypt to one or more symmetric passphrases. + * Note that the passphrases MUST NOT be empty. + * + * @param passphrases passphrase + * @return api handle + */ + WithAlgorithms forPassphrases(Passphrase... passphrases); + /** * Instruct the {@link EncryptionStream} to not encrypt any data. * @@ -150,6 +160,8 @@ public interface EncryptionBuilderInterface { */ DetachedSign usingSecureAlgorithms(); + ToRecipients and(); + } interface DetachedSign extends SignWith { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 08f35959..39a10e0f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -37,6 +37,7 @@ import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; @@ -47,6 +48,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DetachedSignature; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.util.Passphrase; /** * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. @@ -63,6 +65,7 @@ public final class EncryptionStream extends OutputStream { private final HashAlgorithm hashAlgorithm; private final CompressionAlgorithm compressionAlgorithm; private final Set encryptionKeys; + private final Set encryptionPassphrases; private final boolean detachedSignature; private final SignatureType signatureType; private final Map signingKeys; @@ -86,6 +89,7 @@ public final class EncryptionStream extends OutputStream { EncryptionStream(@Nonnull OutputStream targetOutputStream, @Nonnull Set encryptionKeys, + @Nonnull Set encryptionPassphrases, boolean detachedSignature, SignatureType signatureType, @Nonnull Map signingKeys, @@ -99,6 +103,7 @@ public final class EncryptionStream extends OutputStream { this.hashAlgorithm = hashAlgorithm; this.compressionAlgorithm = compressionAlgorithm; this.encryptionKeys = Collections.unmodifiableSet(encryptionKeys); + this.encryptionPassphrases = Collections.unmodifiableSet(encryptionPassphrases); this.detachedSignature = detachedSignature; this.signatureType = signatureType; this.signingKeys = Collections.unmodifiableMap(signingKeys); @@ -126,7 +131,7 @@ public final class EncryptionStream extends OutputStream { } private void prepareEncryption() throws IOException, PGPException { - if (encryptionKeys.isEmpty()) { + if (encryptionKeys.isEmpty() && encryptionPassphrases.isEmpty()) { return; } @@ -143,6 +148,10 @@ public final class EncryptionStream extends OutputStream { encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key)); } + for (Passphrase passphrase : encryptionPassphrases) { + encryptedDataGenerator.addMethod(new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars())); + } + publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); outermostStream = publicKeyEncryptedStream; } diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java new file mode 100644 index 00000000..c8b44c7b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/LegacySymmetricEncryptionTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.symmetric_encryption; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public class LegacySymmetricEncryptionTest { + + private static final Logger LOGGER = Logger.getLogger(LegacySymmetricEncryptionTest.class.getName()); + + private static final String message = + "I grew up with the understanding that the world " + + "I lived in was one where people enjoyed a sort of freedom " + + "to communicate with each other in privacy, without it " + + "being monitored, without it being measured or analyzed " + + "or sort of judged by these shadowy figures or systems, " + + "any time they mention anything that travels across " + + "public lines.\n" + + "\n" + + "- Edward Snowden -"; + + @SuppressWarnings("deprecation") + @Test + public void testSymmetricEncryptionDecryption() throws IOException, PGPException { + byte[] plain = message.getBytes(); + String password = "choose_a_better_password_please"; + Passphrase passphrase = new Passphrase(password.toCharArray()); + byte[] enc = PGPainless.encryptWithPassword(plain, passphrase, SymmetricKeyAlgorithm.AES_128); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armor = new ArmoredOutputStream(out); + armor.write(enc); + armor.flush(); + armor.close(); + + // Print cipher text for validation with GnuPG. + LOGGER.log(Level.INFO, String.format("Use ciphertext below for manual validation with GnuPG " + + "(passphrase = '%s').\n\n%s", password, new String(out.toByteArray()))); + + byte[] plain2 = PGPainless.decryptWithPassword(enc, passphrase); + assertArrayEquals(plain, plain2); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java index 7df26196..ea4c35bd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Paul Schaub. + * Copyright 2020 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,50 +17,82 @@ package org.pgpainless.symmetric_encryption; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.nio.charset.StandardCharsets; -import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.KeyRingProtectionSettings; +import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; import org.pgpainless.util.Passphrase; +/** + * Test parallel symmetric and public key encryption/decryption. + */ public class SymmetricEncryptionTest { - private static final Logger LOGGER = Logger.getLogger(SymmetricEncryptionTest.class.getName()); - - private static final String message = - "I grew up with the understanding that the world " + - "I lived in was one where people enjoyed a sort of freedom " + - "to communicate with each other in privacy, without it " + - "being monitored, without it being measured or analyzed " + - "or sort of judged by these shadowy figures or systems, " + - "any time they mention anything that travels across " + - "public lines.\n" + - "\n" + - "- Edward Snowden -"; - @Test - public void testSymmetricEncryptionDecryption() throws IOException, PGPException { - byte[] plain = message.getBytes(); - String password = "choose_a_better_password_please"; - Passphrase passphrase = new Passphrase(password.toCharArray()); - byte[] enc = PGPainless.encryptWithPassword(plain, passphrase, SymmetricKeyAlgorithm.AES_128); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = new ArmoredOutputStream(out); - armor.write(enc); - armor.flush(); - armor.close(); + public void test() throws IOException, PGPException { + byte[] plaintext = "This is a secret message".getBytes(StandardCharsets.UTF_8); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream(plaintext); + PGPPublicKeyRing encryptionKey = TestKeys.getCryptiePublicKeyRing(); + Passphrase encryptionPassphrase = Passphrase.fromPassword("greenBeans"); - // Print cipher text for validation with GnuPG. - LOGGER.log(Level.INFO, String.format("Use ciphertext below for manual validation with GnuPG " + - "(passphrase = '%s').\n\n%s", password, new String(out.toByteArray()))); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) + .forPassphrases(encryptionPassphrase) + .and() + .toRecipients(encryptionKey) + .usingSecureAlgorithms() + .doNotSign() + .noArmor(); - byte[] plain2 = PGPainless.decryptWithPassword(enc, passphrase); - assertArrayEquals(plain, plain2); + Streams.pipeAll(plaintextIn, encryptor); + encryptor.close(); + + byte[] ciphertext = ciphertextOut.toByteArray(); + + // Test symmetric decryption + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(encryptionPassphrase) + .doNotVerify() + .build(); + + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, decrypted); + decryptor.close(); + + assertArrayEquals(plaintext, decrypted.toByteArray()); + + // Test public key decryption + PGPSecretKeyRingCollection decryptionKeys = TestKeys.getCryptieSecretKeyRingCollection(); + SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( + KeyRingProtectionSettings.secureDefaultSettings(), + new SolitaryPassphraseProvider(Passphrase.fromPassword(TestKeys.CRYPTIE_PASSWORD))); + decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(protector, decryptionKeys) + .doNotVerify() + .build(); + + decrypted = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, decrypted); + decryptor.close(); + + assertArrayEquals(plaintext, decrypted.toByteArray()); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index 877bcac5..3f4fb812 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -15,6 +15,17 @@ */ package org.pgpainless.sop.commands; +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; @@ -32,17 +43,6 @@ import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; import org.pgpainless.util.Passphrase; import picocli.CommandLine; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Scanner; - -import static org.pgpainless.sop.Print.err_ln; -import static org.pgpainless.sop.Print.print_ln; - @CommandLine.Command(name = "encrypt", description = "Encrypt a message from standard input") public class Encrypt implements Runnable { @@ -107,6 +107,11 @@ public class Encrypt implements Runnable { System.exit(1); } } + Passphrase[] passphraseArray = new Passphrase[withPassword.length]; + for (int i = 0; i < withPassword.length; i++) { + String password = withPassword[i]; + passphraseArray[i] = Passphrase.fromPassword(password); + } Map passphraseMap = new HashMap<>(); Scanner scanner = null; @@ -137,6 +142,8 @@ public class Encrypt implements Runnable { EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign() .onOutputStream(System.out) .toRecipients(publicKeys) + .and() + .forPassphrases(passphraseArray) .usingSecureAlgorithms(); EncryptionBuilderInterface.Armor builder_armor; if (signWith.length != 0) {