diff --git a/build.gradle b/build.gradle index 53caf778..7d38069d 100644 --- a/build.gradle +++ b/build.gradle @@ -28,13 +28,15 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.1_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] + if (!it.name.equals('pgpainless-sop')) { + // animalsniffer + apply plugin: 'ru.vyarus.animalsniffer' + dependencies { + signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.1_r2@signature" + } + animalsniffer { + sourceSets = [sourceSets.main] + } } // checkstyle 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..cd739017 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 @@ -34,20 +34,23 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.pgpainless.implementation.ImplementationFactory; 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; - private final KeyFingerPrintCalculator keyFingerPrintCalculator = new BcKeyFingerprintCalculator(); + private final KeyFingerPrintCalculator keyFingerPrintCalculator = + ImplementationFactory.getInstance().getKeyFingerprintCalculator(); @Override public DecryptWith onInputStream(@Nonnull InputStream inputStream) { @@ -64,6 +67,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 +206,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..92b0527c 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 @@ -15,8 +15,6 @@ */ package org.pgpainless.decryption_verification; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -28,14 +26,18 @@ import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; 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,15 @@ 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.BcPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; public final class DecryptionStreamFactory { @@ -62,20 +64,23 @@ 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; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final PGPContentVerifierBuilderProvider verifierBuilderProvider = new BcPGPContentVerifierBuilderProvider(); - private final KeyFingerPrintCalculator keyFingerprintCalculator = new BcKeyFingerprintCalculator(); + private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); + private static final KeyFingerPrintCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); private final Map verifiableOnePassSignatures = new HashMap<>(); 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 +88,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; @@ -98,13 +104,13 @@ public final class DecryptionStreamFactory { if (signingKey == null) { continue; } - signature.init(new BcPGPContentVerifierBuilderProvider(), signingKey); + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); factory.resultBuilder.addDetachedSignature( new DetachedSignature(signature, new OpenPgpV4Fingerprint(signingKey))); } } else { PGPObjectFactory objectFactory = new PGPObjectFactory( - PGPUtil.getDecoderStream(inputStream), new BcKeyFingerprintCalculator()); + PGPUtil.getDecoderStream(inputStream), keyFingerprintCalculator); pgpInputStream = factory.processPGPPackets(objectFactory); } return new DecryptionStream(pgpInputStream, factory.resultBuilder); @@ -171,7 +177,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,27 +185,53 @@ 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 = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(decryptionPassphrase); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( + pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); + resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + resultBuilder.setIntegrityProtected(pbeEncryptedData.isIntegrityProtected()); + + try { + return pbeEncryptedData.getDataStream(passphraseDecryptor); + } catch (PGPException e) { + LOGGER.log(LEVEL, "Probable passphrase mismatch, skip PBE encrypted data block", e); + } + } + + } 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)); + } + } } } if (decryptionKey == null) { - throw new PGPException("Decryption failed - No suitable decryption key found"); + throw new PGPException("Decryption failed - No suitable decryption key or passphrase found"); } - PublicKeyDataDecryptorFactory keyDecryptor = new BcPublicKeyDataDecryptorFactory(decryptionKey); + PublicKeyDataDecryptorFactory keyDecryptor = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(decryptionKey); + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm .fromId(encryptedSessionKey.getSymmetricAlgorithm(keyDecryptor)); @@ -240,9 +272,10 @@ public final class DecryptionStreamFactory { } signature.init(verifierBuilderProvider, verificationKey); - OnePassSignature onePassSignature = new OnePassSignature(signature, new OpenPgpV4Fingerprint(verificationKey)); + OpenPgpV4Fingerprint fingerprint = new OpenPgpV4Fingerprint(verificationKey); + OnePassSignature onePassSignature = new OnePassSignature(signature, fingerprint); resultBuilder.addOnePassSignature(onePassSignature); - verifiableOnePassSignatures.put(new OpenPgpV4Fingerprint(verificationKey), onePassSignature); + verifiableOnePassSignatures.put(fingerprint, onePassSignature); } private PGPPublicKey findSignatureVerificationKey(long keyId) { 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..e3c90040 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,16 +37,21 @@ 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.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DetachedSignature; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.util.Passphrase; /** * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. @@ -63,6 +68,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 +92,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 +106,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,21 +134,35 @@ public final class EncryptionStream extends OutputStream { } private void prepareEncryption() throws IOException, PGPException { - if (encryptionKeys.isEmpty()) { + if (encryptionKeys.isEmpty() && encryptionPassphrases.isEmpty()) { return; } LOGGER.log(LEVEL, "At least one encryption key is available -> encrypt using " + symmetricKeyAlgorithm); - BcPGPDataEncryptorBuilder dataEncryptorBuilder = - new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); + PGPDataEncryptorBuilder dataEncryptorBuilder = + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + + // Simplify once https://github.com/bcgit/bc-java/pull/859 is merged + if (dataEncryptorBuilder instanceof BcPGPDataEncryptorBuilder) { + ((BcPGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true); + } else if (dataEncryptorBuilder instanceof JcePGPDataEncryptorBuilder) { + ((JcePGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true); + } - dataEncryptorBuilder.setWithIntegrityPacket(true); PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); for (PGPPublicKey key : encryptionKeys) { LOGGER.log(LEVEL, "Encrypt for key " + Long.toHexString(key.getKeyID())); - encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key)); + PublicKeyKeyEncryptionMethodGenerator keyEncryption = + ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); + encryptedDataGenerator.addMethod(keyEncryption); + } + + for (Passphrase passphrase : encryptionPassphrases) { + PBEKeyEncryptionMethodGenerator passphraseEncryption = + ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); + encryptedDataGenerator.addMethod(passphraseEncryption); } publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); @@ -156,8 +178,10 @@ public final class EncryptionStream extends OutputStream { for (OpenPgpV4Fingerprint fingerprint : signingKeys.keySet()) { PGPPrivateKey privateKey = signingKeys.get(fingerprint); LOGGER.log(LEVEL, "Sign using key " + fingerprint); - BcPGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( - privateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getAlgorithmId()); + PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + privateKey.getPublicKeyPacket().getAlgorithm(), + hashAlgorithm.getAlgorithmId()); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); signatureGenerator.init(signatureType.getCode(), privateKey); diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java new file mode 100644 index 00000000..7b75f422 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java @@ -0,0 +1,160 @@ +/* + * 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. + * 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.implementation; + +import java.security.KeyPair; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public class BcImplementationFactory extends ImplementationFactory { + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) + throws PGPException { + return new BcPBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm(), + getPGPDigestCalculator(secretKey.getS2K().getHashAlgorithm()), + (int) secretKey.getS2K().getIterationCount()) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + PGPDigestCalculator digestCalculator, + Passphrase passphrase) { + return new BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) + .build(passphrase.getChars()); + } + + @Override + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) { + return new BcPBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) + .build(passphrase.getChars()); + } + + @Override + public BcPGPDigestCalculatorProvider getPGPDigestCalculatorProvider() { + return new BcPGPDigestCalculatorProvider(); + } + + @Override + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return new BcPGPContentVerifierBuilderProvider(); + } + + @Override + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); + } + + @Override + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return new BcKeyFingerprintCalculator(); + } + + @Override + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) { + return new BcPBEDataDecryptorFactory(passphrase.getChars(), getPGPDigestCalculatorProvider()); + } + + @Override + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return new BcPublicKeyDataDecryptorFactory(privateKey); + } + + @Override + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return new BcPublicKeyKeyEncryptionMethodGenerator(key); + } + + @Override + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()); + } + + @Override + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) + throws PGPException { + return new BcPGPKeyPair(algorithm.getAlgorithmId(), jceToBcKeyPair(algorithm, keyPair, creationDate), creationDate); + } + + @Override + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException { + return new BcPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + } + + @Override + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return new BcPBESecretKeyEncryptorBuilder( + encryptionAlgorithm.getAlgorithmId(), + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .build(passphrase.getChars()); + } + + // TODO: Find a better conversion method that does not depend on JcaPGPKeyPair. + private AsymmetricCipherKeyPair jceToBcKeyPair(PublicKeyAlgorithm algorithm, + KeyPair keyPair, + Date creationDate) throws PGPException { + BcPGPKeyConverter converter = new BcPGPKeyConverter(); + + PGPKeyPair pair = new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + AsymmetricKeyParameter publicKey = converter.getPublicKey(pair.getPublicKey()); + AsymmetricKeyParameter privateKey = converter.getPrivateKey(pair.getPrivateKey()); + + return new AsymmetricCipherKeyPair(publicKey, privateKey); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java new file mode 100644 index 00000000..6a716b6c --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java @@ -0,0 +1,141 @@ +/* + * 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. + * 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.implementation; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.Passphrase; + +public abstract class ImplementationFactory { + + private static ImplementationFactory FACTORY_IMPLEMENTATION = new BcImplementationFactory(); + + public static void setFactoryImplementation(ImplementationFactory implementation) { + FACTORY_IMPLEMENTATION = implementation; + } + + public static ImplementationFactory getInstance() { + return FACTORY_IMPLEMENTATION; + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, + Passphrase passphrase) + throws PGPException { + return getPBESecretKeyEncryptor(symmetricKeyAlgorithm, + getPGPDigestCalculator(HashAlgorithm.SHA1), passphrase); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(secretKey, passphrase); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(symmetricKeyAlgorithm, digestCalculator, passphrase); + } + + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyDecryptor(passphrase); + } + + public PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { + return getPGPDigestCalculator(algorithm.getAlgorithmId()); + } + + public PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { + return getPGPDigestCalculatorProvider().get(algorithm); + } + + public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException { + return FACTORY_IMPLEMENTATION.getPGPDigestCalculatorProvider(); + } + + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return FACTORY_IMPLEMENTATION.getPGPContentVerifierBuilderProvider(); + } + + public PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { + return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); + } + + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return FACTORY_IMPLEMENTATION.getPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); + } + + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return FACTORY_IMPLEMENTATION.getKeyFingerprintCalculator(); + } + + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBEDataDecryptorFactory(passphrase); + } + + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return FACTORY_IMPLEMENTATION.getPublicKeyDataDecryptorFactory(privateKey); + } + + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return FACTORY_IMPLEMENTATION.getPublicKeyKeyEncryptionMethodGenerator(key); + } + + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return FACTORY_IMPLEMENTATION.getPBEKeyEncryptionMethodGenerator(passphrase); + } + + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { + return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); + } + + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return FACTORY_IMPLEMENTATION.getPGPDataEncryptorBuilder(symmetricKeyAlgorithm); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { + return FACTORY_IMPLEMENTATION.getPGPKeyPair(algorithm, keyPair, creationDate); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException { + return FACTORY_IMPLEMENTATION.getPGPKeyPair(algorithm, keyPair, creationDate); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return FACTORY_IMPLEMENTATION.getPBESecretKeyEncryptor(encryptionAlgorithm, hashAlgorithm, s2kCount, passphrase); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java new file mode 100644 index 00000000..59a2ccd4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java @@ -0,0 +1,162 @@ +/* + * 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. + * 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.implementation; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Date; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.provider.ProviderFactory; +import org.pgpainless.util.Passphrase; + +public class JceImplementationFactory extends ImplementationFactory { + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(PGPSecretKey secretKey, Passphrase passphrase) + throws PGPException { + return new JcePBESecretKeyEncryptorBuilder(secretKey.getKeyEncryptionAlgorithm()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { + return new JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { + return new JcePBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() + throws PGPException { + return new JcaPGPDigestCalculatorProviderBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(); + } + + public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { + return new JcaPGPContentVerifierBuilderProvider() + .setProvider(ProviderFactory.getProvider()); + } + + public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { + return new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + .setProvider(ProviderFactory.getProvider()); + } + + public KeyFingerPrintCalculator getKeyFingerprintCalculator() { + return new JcaKeyFingerprintCalculator() + .setProvider(ProviderFactory.getProvider()); + } + + public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) + throws PGPException { + return new JcePBEDataDecryptorFactoryBuilder(getPGPDigestCalculatorProvider()) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { + return new JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(privateKey); + } + + public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { + return new JcePublicKeyKeyEncryptionMethodGenerator(key) + .setProvider(ProviderFactory.getProvider()); + } + + public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { + return new JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) + .setProvider(ProviderFactory.getProvider()); + } + + public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { + return new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) + .setProvider(ProviderFactory.getProvider()); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { + return new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); + } + + public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, AsymmetricCipherKeyPair keyPair, Date creationDate) throws PGPException, NoSuchAlgorithmException, IOException, InvalidKeySpecException { + return new JcaPGPKeyPair(algorithm.getAlgorithmId(), bcToJceKeyPair(keyPair), creationDate); + } + + public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { + return new JcePBESecretKeyEncryptorBuilder( + encryptionAlgorithm.getAlgorithmId(), + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()); + } + + private KeyPair bcToJceKeyPair(AsymmetricCipherKeyPair keyPair) + throws NoSuchAlgorithmException, InvalidKeySpecException, IOException { + byte[] pkcs8Encoded = PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.getPrivate()).getEncoded(); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(pkcs8Encoded); + byte[] spkiEncoded = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(keyPair.getPublic()).getEncoded(); + X509EncodedKeySpec spkiKeySpec = new X509EncodedKeySpec(spkiEncoded); + KeyFactory keyFac = KeyFactory.getInstance("RSA"); + return new KeyPair(keyFac.generatePublic(spkiKeySpec), keyFac.generatePrivate(pkcs8KeySpec)); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java new file mode 100644 index 00000000..b3e8787e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + * 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. + */ +/** + * Implementation factory classes to be able to switch out the underlying crypto engine implementation. + */ +package org.pgpainless.implementation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index afe3c1c5..f446383f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Set; import javax.annotation.Nonnull; -import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyRingGenerator; @@ -44,14 +43,11 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; @@ -331,33 +327,28 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { } private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) { - return new JcaPGPContentSignerBuilder( - certKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) - .setProvider(ProviderFactory.getProvider()); + return ImplementationFactory.getInstance().getPGPContentSignerBuilder( + certKey.getPublicKey().getAlgorithm(), + HashAlgorithm.SHA512.getAlgorithmId()); } private PBESecretKeyEncryptor buildSecretKeyEncryptor() { PBESecretKeyEncryptor encryptor = passphrase == null || passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted - new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256, digestCalculator) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + SymmetricKeyAlgorithm.AES_256, digestCalculator, passphrase); return encryptor; } private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException { PBESecretKeyDecryptor decryptor = passphrase == null || passphrase.isEmpty() ? null : - new JcePBESecretKeyDecryptorBuilder() - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); return decryptor; } private PGPDigestCalculator buildDigestCalculator() throws PGPException { - return new JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.getProvider()) - .build() - .get(HashAlgorithm.SHA1.getAlgorithmId()); + return ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1); } } } @@ -373,8 +364,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { KeyPair keyPair = certKeyGenerator.generateKeyPair(); // Form PGP key pair - PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(type.getAlgorithm().getAlgorithmId(), - keyPair, new Date()); + PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance().getPGPKeyPair(type.getAlgorithm(), keyPair, new Date()); return pgpKeyPair; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 52eccb0e..b9a94686 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -45,12 +45,10 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; @@ -202,10 +200,13 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { PBESecretKeyDecryptor ringDecryptor = keyRingProtector.getDecryptor(primaryKey.getKeyID()); PBESecretKeyEncryptor subKeyEncryptor = subKeyProtector.getEncryptor(secretSubKey.getKeyID()); - PGPDigestCalculator digestCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); - PGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( - primaryKey.getAlgorithm(), HashAlgorithm.SHA256.getAlgorithmId()); + PGPDigestCalculator digestCalculator = + ImplementationFactory.getInstance().getPGPDigestCalculator(defaultDigestHashAlgorithm); + PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + primaryKey.getAlgorithm(), + HashAlgorithm.SHA256.getAlgorithmId() // TODO: Why SHA256? + ); PGPPrivateKey privateSubKey = unlockSecretKey(secretSubKey, subKeyProtector); PGPKeyPair subKeyPair = new PGPKeyPair(secretSubKey.getPublicKey(), privateSubKey); @@ -222,12 +223,11 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { private PGPSecretKey generateSubKey(@Nonnull KeySpec keySpec, @Nonnull Passphrase subKeyPassphrase) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - PGPDigestCalculator checksumCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); + PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance() + .getPGPDigestCalculator(defaultDigestHashAlgorithm); PBESecretKeyEncryptor subKeyEncryptor = subKeyPassphrase.isEmpty() ? null : - new BcPBESecretKeyEncryptorBuilder(SymmetricKeyAlgorithm.AES_256.getAlgorithmId()) - .build(subKeyPassphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.AES_256, subKeyPassphrase); PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); PGPSecretKey secretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(), @@ -530,8 +530,8 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { // TODO: Move to utility class private PGPSecretKey lockPrivateKey(PGPPrivateKey privateKey, PGPPublicKey publicKey, SecretKeyRingProtector protector) throws PGPException { - PGPDigestCalculator checksumCalculator = new BcPGPDigestCalculatorProvider() - .get(defaultDigestHashAlgorithm.getAlgorithmId()); + PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance() + .getPGPDigestCalculator(defaultDigestHashAlgorithm); PBESecretKeyEncryptor encryptor = protector.getEncryptor(publicKey.getKeyID()); PGPSecretKey secretKey = new PGPSecretKey(privateKey, publicKey, checksumCalculator, publicKey.isMasterKey(), encryptor); return secretKey; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index 7b0297c7..6f3a1c2c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -27,7 +27,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.pgpainless.implementation.ImplementationFactory; public class KeyRingReader { @@ -90,27 +90,27 @@ public class KeyRingReader { public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { return new PGPPublicKeyRing( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPSecretKeyRing( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) throws IOException, PGPException { return new PGPSecretKeyRingCollection( PGPUtil.getDecoderStream(inputStream), - new BcKeyFingerprintCalculator()); + ImplementationFactory.getInstance().getKeyFingerprintCalculator()); } private static void validateStreamsNotBothNull(InputStream publicIn, InputStream secretIn) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java new file mode 100644 index 00000000..75677bbe --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CallbackBasedKeyringProtector.java @@ -0,0 +1,67 @@ +/* + * 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. + * 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.key.protection; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.util.Passphrase; + +public class CallbackBasedKeyringProtector implements SecretKeyRingProtector2 { + + private final Map passphraseCache = new ConcurrentHashMap<>(); + private final Callback callback; + + public CallbackBasedKeyringProtector(Callback callback) { + if (callback == null) { + throw new NullPointerException("Callback MUST NOT be null."); + } + this.callback = callback; + } + + @Override + public PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException { + Passphrase passphrase = lookupPassphraseInCache(key); + if (passphrase != null) { + passphrase = callback.getPassphraseFor(key); + passphraseCache.put(key.getKeyID(), passphrase); + } + return ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); + } + + @Override + public PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException { + Passphrase passphrase = lookupPassphraseInCache(key); + if (passphrase != null) { + passphrase = callback.getPassphraseFor(key); + passphraseCache.put(key.getKeyID(), passphrase); + } + return ImplementationFactory.getInstance().getPBESecretKeyEncryptor(key, passphrase); + } + + private Passphrase lookupPassphraseInCache(PGPSecretKey key) { + return passphraseCache.get(key.getKeyID()); + } + + public interface Callback { + Passphrase getPassphraseFor(PGPSecretKey secretKey); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java index 385e82fb..ad348a7e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java @@ -15,10 +15,10 @@ */ package org.pgpainless.key.protection; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; @@ -69,7 +69,7 @@ public class PassphraseMapKeyRingProtector implements SecretKeyRingProtector, Se @Override @Nullable - public Passphrase getPassphraseFor(@Nonnull Long keyId) { + public Passphrase getPassphraseFor(Long keyId) { Passphrase passphrase = cache.get(keyId); if (passphrase == null || !passphrase.isValid()) { passphrase = provider.getPassphraseFor(keyId); @@ -82,7 +82,7 @@ public class PassphraseMapKeyRingProtector implements SecretKeyRingProtector, Se @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) { + public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { return protector.getDecryptor(keyId); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java index 7328b77b..0bec8b00 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java @@ -23,10 +23,7 @@ import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; @@ -36,8 +33,6 @@ import org.pgpainless.util.Passphrase; */ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtector { - private static final PGPDigestCalculatorProvider calculatorProvider = new BcPGPDigestCalculatorProvider(); - protected final KeyRingProtectionSettings protectionSettings; protected final SecretKeyPassphraseProvider passphraseProvider; @@ -86,11 +81,10 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) { + public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null ? null : - new BcPBESecretKeyDecryptorBuilder(calculatorProvider) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); } @Override @@ -98,10 +92,10 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null ? null : - new BcPBESecretKeyEncryptorBuilder( - protectionSettings.getEncryptionAlgorithm().getAlgorithmId(), - calculatorProvider.get(protectionSettings.getHashAlgorithm().getAlgorithmId()), - protectionSettings.getS2kCount()) - .build(passphrase.getChars()); + ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + protectionSettings.getEncryptionAlgorithm(), + protectionSettings.getHashAlgorithm(), + protectionSettings.getS2kCount(), + passphrase); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 4d27ed02..f75bff25 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -25,6 +25,11 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.pgpainless.util.Passphrase; +/** + * Interface that is used to provide secret key ring encryptors and decryptors. + * + * @deprecated use {@link SecretKeyRingProtector2} instead. + */ public interface SecretKeyRingProtector { /** @@ -34,7 +39,7 @@ public interface SecretKeyRingProtector { * @param keyId id of the key * @return decryptor for the key */ - @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId); + @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException; /** * Return an encryptor for the key of id {@code keyId}. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java new file mode 100644 index 00000000..f215641a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector2.java @@ -0,0 +1,28 @@ +/* + * 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. + * 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.key.protection; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; + +public interface SecretKeyRingProtector2 { + + PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException; + + PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java new file mode 100644 index 00000000..b9e46896 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtectorAdapter.java @@ -0,0 +1,34 @@ +/* + * 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. + * 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.key.protection; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; + +public interface SecretKeyRingProtectorAdapter extends SecretKeyRingProtector, SecretKeyRingProtector2 { + + @Override + default PBESecretKeyDecryptor getDecryptor(PGPSecretKey key) throws PGPException { + return getDecryptor(key.getKeyID()); + } + + @Override + default PBESecretKeyEncryptor getEncryptor(PGPSecretKey key) throws PGPException { + return getEncryptor(key.getKeyID()); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java index aa5d43ef..1b81ef93 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java @@ -17,6 +17,7 @@ package org.pgpainless.key.protection.passphrase_provider; import javax.annotation.Nullable; +import org.bouncycastle.openpgp.PGPSecretKey; import org.pgpainless.util.Passphrase; /** @@ -24,12 +25,15 @@ import org.pgpainless.util.Passphrase; */ public interface SecretKeyPassphraseProvider { + @Nullable default Passphrase getPassphraseFor(PGPSecretKey secretKey) { + return getPassphraseFor(secretKey.getKeyID()); + } /** * Return a passphrase for the given key. If no record has been found, return null. * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with * a content of null. * - * @param keyId id of the key + * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. */ @Nullable Passphrase getPassphraseFor(Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java index 1d29f841..b68f63b3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java @@ -15,7 +15,6 @@ */ package org.pgpainless.util; -import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -26,6 +25,7 @@ import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyRing; @@ -38,13 +38,14 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; import org.bouncycastle.util.io.Streams; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.selection.key.PublicKeySelectionStrategy; -import org.pgpainless.key.selection.key.util.And; import org.pgpainless.key.selection.key.impl.NoRevocation; import org.pgpainless.key.selection.key.impl.SignedByMasterKey; +import org.pgpainless.key.selection.key.util.And; public class BCUtil { @@ -72,8 +73,9 @@ public class BCUtil { publicKey.encode(buffer, false); } } + KeyFingerPrintCalculator fingerprintCalculator = ImplementationFactory.getInstance().getKeyFingerprintCalculator(); - return new PGPPublicKeyRing(buffer.toByteArray(), new BcKeyFingerprintCalculator()); + return new PGPPublicKeyRing(buffer.toByteArray(), fingerprintCalculator); } /* diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index af97d100..437ef12f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -37,7 +37,7 @@ public class PassphraseProtectedKeyTest { @Nullable @Override public Passphrase getPassphraseFor(Long keyId) { - if (keyId == TestKeys.CRYPTIE_KEY_ID) { + if (keyId.equals(TestKeys.CRYPTIE_KEY_ID)) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { return null; 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/MultiPassphraseSymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java new file mode 100644 index 00000000..aee992f2 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java @@ -0,0 +1,65 @@ +/* + * 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. + * 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.util.Passphrase; + +public class MultiPassphraseSymmetricEncryptionTest { + + @Test + public void test() throws IOException, PGPException { + String message = "Here we test if during decryption of a message that was encrypted with two passphrases, " + + "the decryptor finds the session key encrypted for the right passphrase."; + ByteArrayInputStream plaintextIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptor = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .forPassphrases(Passphrase.fromPassword("p1"), Passphrase.fromPassword("p2")) + .usingSecureAlgorithms() + .doNotSign() + .noArmor(); + + Streams.pipeAll(plaintextIn, encryptor); + encryptor.close(); + + byte[] ciphertext = ciphertextOut.toByteArray(); + + // decrypting the p1 package with p2 first will not work. Test if it is handled correctly. + for (Passphrase passphrase : new Passphrase[] {Passphrase.fromPassword("p2"), Passphrase.fromPassword("p1")}) { + DecryptionStream decryptor = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(ciphertext)) + .decryptWith(passphrase) + .doNotVerify() + .build(); + + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, plaintextOut); + + decryptor.close(); + } + } +} 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/build.gradle b/pgpainless-sop/build.gradle new file mode 100644 index 00000000..f71e1772 --- /dev/null +++ b/pgpainless-sop/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'application' +} + +dependencies { + implementation(project(":pgpainless-core")) + + implementation 'info.picocli:picocli:4.5.2' + + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + /* + implementation "org.bouncycastle:bcprov-debug-jdk15on:$bouncyCastleVersion" + /*/ + implementation "org.bouncycastle:bcprov-jdk15on:$bouncyCastleVersion" + //*/ + implementation "org.bouncycastle:bcpg-jdk15on:$bouncyCastleVersion" + + // https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305 + implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' +} + +mainClassName = 'org.pgpainless.sop.PGPainlessCLI' + +jar { + manifest { + attributes 'Main-Class': "$mainClassName" + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } +} + diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java new file mode 100644 index 00000000..1cc7cf4e --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/PGPainlessCLI.java @@ -0,0 +1,52 @@ +/* + * 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. + * 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.sop; + +import org.pgpainless.sop.commands.Armor; +import org.pgpainless.sop.commands.Dearmor; +import org.pgpainless.sop.commands.Decrypt; +import org.pgpainless.sop.commands.Encrypt; +import org.pgpainless.sop.commands.ExtractCert; +import org.pgpainless.sop.commands.GenerateKey; +import org.pgpainless.sop.commands.Sign; +import org.pgpainless.sop.commands.Verify; +import org.pgpainless.sop.commands.Version; +import picocli.CommandLine; + +@CommandLine.Command( + subcommands = { + Armor.class, + Dearmor.class, + Decrypt.class, + Encrypt.class, + ExtractCert.class, + GenerateKey.class, + Sign.class, + Verify.class, + Version.class + } +) +public class PGPainlessCLI implements Runnable { + + public static void main(String[] args) { + CommandLine.run(new PGPainlessCLI(), args); + } + + @Override + public void run() { + + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java new file mode 100644 index 00000000..0fcb5220 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/Print.java @@ -0,0 +1,43 @@ +/* + * 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. + * 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.sop; + +import java.io.IOException; + +import org.pgpainless.util.ArmorUtils; + +public class Print { + + public static String toString(byte[] bytes, boolean armor) throws IOException { + if (armor) { + return ArmorUtils.toAsciiArmoredString(bytes); + } else { + return new String(bytes, "UTF-8"); + } + } + + public static void print_ln(String msg) { + // CHECKSTYLE:OFF + System.out.println(msg); + // CHECKSTYLE:ON + } + + public static void err_ln(String msg) { + // CHECKSTYLE:OFF + System.err.println(msg); + // CHECKSTYLE:ON + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java new file mode 100644 index 00000000..082bdc62 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SopKeyUtil.java @@ -0,0 +1,44 @@ +/* + * 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. + * 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.sop; + +import static org.pgpainless.sop.Print.err_ln; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; + +public class SopKeyUtil { + + public static List loadKeysFromFiles(File... files) throws IOException, PGPException { + List secretKeyRings = new ArrayList<>(); + for (File file : files) { + try (FileInputStream in = new FileInputStream(file)) { + secretKeyRings.add(PGPainless.readKeyRing().secretKeyRing(in)); + } catch (PGPException | IOException e) { + err_ln("Could not load secret key " + file.getName() + ": " + e.getMessage()); + throw e; + } + } + return secretKeyRings; + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java new file mode 100644 index 00000000..7ddf047c --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Armor.java @@ -0,0 +1,67 @@ +/* + * 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. + * 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.sop.commands; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.util.io.Streams; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.PushbackInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static org.pgpainless.sop.Print.err_ln; + +@CommandLine.Command(name = "armor", + description = "Add ASCII Armor to standard input") +public class Armor implements Runnable { + + private static final byte[] BEGIN_ARMOR = "-----BEGIN PGP".getBytes(StandardCharsets.UTF_8); + + private enum Label { + auto, + sig, + key, + cert, + message + } + + @CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") + Label label; + + @CommandLine.Option(names = {"--allow-nested"}, description = "Allow additional armoring of already armored input") + boolean allowNested = false; + + @Override + public void run() { + + try (PushbackInputStream pbIn = new PushbackInputStream(System.in); ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(System.out)) { + byte[] start = new byte[14]; + int read = pbIn.read(start); + pbIn.unread(read); + if (Arrays.equals(BEGIN_ARMOR, start) && !allowNested) { + Streams.pipeAll(pbIn, System.out); + } else { + Streams.pipeAll(pbIn, armoredOutputStream); + } + } catch (IOException e) { + err_ln("Input data cannot be ASCII armored."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java new file mode 100644 index 00000000..737049bb --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Dearmor.java @@ -0,0 +1,40 @@ +/* + * 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. + * 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.sop.commands; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.util.io.Streams; +import picocli.CommandLine; + +import java.io.IOException; + +import static org.pgpainless.sop.Print.err_ln; + +@CommandLine.Command(name = "dearmor", + description = "Remove ASCII Armor from standard input") +public class Dearmor implements Runnable { + + @Override + public void run() { + try (ArmoredInputStream in = new ArmoredInputStream(System.in, true)) { + Streams.pipeAll(in, System.out); + } catch (IOException e) { + err_ln("Data cannot be dearmored."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java new file mode 100644 index 00000000..4be22ebb --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Decrypt.java @@ -0,0 +1,111 @@ +/* + * 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. + * 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.sop.commands; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.pgpainless.PGPainless; +import picocli.CommandLine; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.SopKeyUtil.loadKeysFromFiles; + +@CommandLine.Command(name = "decrypt", + description = "Decrypt a message from standard input") +public class Decrypt implements Runnable { + + @CommandLine.Option( + names = {"--session-key-out"}, + description = "Can be used to learn the session key on successful decryption", + paramLabel = "SESSIONKEY") + File sessionKeyOut; + + @CommandLine.Option( + names = {"--with-session-key"}, + description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", + paramLabel = "SESSIONKEY") + File[] withSessionKey; + + @CommandLine.Option( + names = {"--with-password"}, + description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", + paramLabel = "PASSWORD") + String[] withPassword; + + @CommandLine.Option(names = {"--verify-out"}, + description = "Produces signature verification status to the designated file", + paramLabel = "VERIFICATIONS") + File verifyOut; + + @CommandLine.Option(names = {"--verify-with"}, + description = "Certificates whose signatures would be acceptable for signatures over this message", + paramLabel = "CERT") + File[] certs; + + @CommandLine.Option(names = {"--not-before"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").", + paramLabel = "DATE") + String notBefore = "-"; + + @CommandLine.Option(names = {"--not-after"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to current system time (\"now\").\n" + + "Accepts special value \"-\" for end of time.", + paramLabel = "DATE") + String notAfter = "now"; + + @CommandLine.Parameters(index = "0..*", + description = "Secret keys to attempt decryption with", + paramLabel = "KEY") + File[] keys; + + @Override + public void run() { + if (verifyOut == null ^ certs == null) { + err_ln("To enable signature verification, both --verify-out and at least one --verify-with argument must be supplied."); + System.exit(23); + } + + if (sessionKeyOut != null || withSessionKey != null) { + err_ln("session key in and out are not yet supported."); + System.exit(1); + } + + PGPSecretKeyRingCollection secretKeys; + try { + List secretKeyRings = loadKeysFromFiles(keys); + secretKeys = new PGPSecretKeyRingCollection(secretKeyRings); + } catch (PGPException | IOException e) { + err_ln(e.getMessage()); + System.exit(1); + return; + } + + + + PGPainless.decryptAndOrVerify() + .onInputStream(System.in) + .decryptWith(secretKeys); + } +} 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 new file mode 100644 index 00000000..3f4fb812 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -0,0 +1,172 @@ +/* + * 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. + * 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.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; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionBuilderInterface; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.KeyRingProtectionSettings; +import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; +import org.pgpainless.util.Passphrase; +import picocli.CommandLine; + +@CommandLine.Command(name = "encrypt", + description = "Encrypt a message from standard input") +public class Encrypt implements Runnable { + + public enum Type { + binary, + text, + mime + } + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @CommandLine.Option(names = {"--as"}, + description = "Type of the input data. Defaults to 'binary'", + paramLabel = "{binary|text|mime}") + Type type; + + @CommandLine.Option(names = "--with-password", + description = "Encrypt the message with a password", + paramLabel = "PASSWORD") + String[] withPassword = new String[0]; + + @CommandLine.Option(names = "--sign-with", + description = "Sign the output with a private key", + paramLabel = "KEY") + File[] signWith = new File[0]; + + @CommandLine.Parameters(description = "Certificates the message gets encrypted to", + index = "0..*", + paramLabel = "CERTS") + File[] certs = new File[0]; + + @Override + public void run() { + if (certs.length == 0 && withPassword.length == 0) { + err_ln("Please either provide --with-password or at least one CERT"); + System.exit(19); + } + + PGPPublicKeyRing[] publicKeys = new PGPPublicKeyRing[certs.length]; + for (int i = 0 ; i < certs.length; i++) { + try (InputStream fileIn = new FileInputStream(certs[i])) { + PGPPublicKeyRing publicKey = PGPainless.readKeyRing().publicKeyRing(fileIn); + publicKeys[i] = publicKey; + } catch (IOException e) { + err_ln("Cannot read certificate " + certs[i].getName()); + err_ln(e.getMessage()); + System.exit(1); + } + } + PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[signWith.length]; + for (int i = 0; i < signWith.length; i++) { + try (FileInputStream fileIn = new FileInputStream(signWith[i])) { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(fileIn); + secretKeys[i] = secretKey; + } catch (IOException | PGPException e) { + err_ln("Cannot read secret key from file " + signWith[i].getName()); + err_ln(e.getMessage()); + 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; + for (PGPSecretKeyRing ring : secretKeys) { + for (PGPSecretKey key : ring) { + // Skip non-signing keys + PGPSignature signature = (PGPSignature) key.getPublicKey().getSignatures().next(); + int flags = signature.getHashedSubPackets().getKeyFlags(); + if (!key.isSigningKey() || !KeyFlag.hasKeyFlag(flags, KeyFlag.SIGN_DATA)) { + // Key cannot sign + continue; + } + + if (key.getKeyEncryptionAlgorithm() == SymmetricKeyAlgorithm.NULL.getAlgorithmId()) { + passphraseMap.put(key.getKeyID(), Passphrase.emptyPassphrase()); + } else { + print_ln("Please provide the passphrase for key " + new OpenPgpV4Fingerprint(key)); + if (scanner == null) { + scanner = new Scanner(System.in); + } + String password = scanner.nextLine(); + Passphrase passphrase = Passphrase.fromPassword(password.trim()); + passphraseMap.put(key.getKeyID(), passphrase); + } + } + } + + EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .toRecipients(publicKeys) + .and() + .forPassphrases(passphraseArray) + .usingSecureAlgorithms(); + EncryptionBuilderInterface.Armor builder_armor; + if (signWith.length != 0) { + EncryptionBuilderInterface.DocumentType documentType = builder.signWith(new PassphraseMapKeyRingProtector(passphraseMap, + KeyRingProtectionSettings.secureDefaultSettings(), null), secretKeys); + if (type == Type.text || type == Type.mime) { + builder_armor = documentType.signCanonicalText(); + } else { + builder_armor = documentType.signBinaryDocument(); + } + } else { + builder_armor = builder.doNotSign(); + } + try { + EncryptionStream encryptionStream = !armor ? builder_armor.noArmor() : builder_armor.asciiArmor(); + + Streams.pipeAll(System.in, encryptionStream); + + encryptionStream.close(); + } catch (IOException | PGPException e) { + err_ln("An error happened."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java new file mode 100644 index 00000000..6814d630 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/ExtractCert.java @@ -0,0 +1,53 @@ +/* + * 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. + * 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.sop.commands; + +import java.io.IOException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; +import org.pgpainless.sop.Print; +import org.pgpainless.util.BCUtil; +import picocli.CommandLine; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "extract-cert", + description = "Extract a public key certificate from a secret key from standard input") +public class ExtractCert implements Runnable { + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @Override + public void run() { + try { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(System.in); + PGPPublicKeyRing publicKeys = BCUtil.publicKeyRingFromSecretKeyRing(secretKeys); + + print_ln(Print.toString(publicKeys.getEncoded(), armor)); + } catch (IOException | PGPException e) { + err_ln("Error extracting certificate from keys;"); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java new file mode 100644 index 00000000..66dae486 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/GenerateKey.java @@ -0,0 +1,55 @@ +/* + * 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. + * 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.sop.commands; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.PGPainless; +import org.pgpainless.sop.Print; +import picocli.CommandLine; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "generate-key", description = "Generate a secret key") +public class GenerateKey implements Runnable { + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @CommandLine.Parameters(description = "User-ID, eg. \"Alice \"") + String userId; + + @Override + public void run() { + try { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing(userId); + + print_ln(Print.toString(secretKeys.getEncoded(), armor)); + + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | PGPException | IOException e) { + err_ln("Error creating OpenPGP key:"); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java new file mode 100644 index 00000000..7984e141 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Sign.java @@ -0,0 +1,98 @@ +/* + * 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. + * 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.sop.commands; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionBuilderInterface; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.key.protection.UnprotectedKeysProtector; +import org.pgpainless.sop.Print; +import picocli.CommandLine; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "sign", + description = "Create a detached signature on the data from standard input") +public class Sign implements Runnable { + + public enum Type { + binary, + text + } + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = true) + boolean armor = true; + + @CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.", + paramLabel = "{binary|text}") + Type type; + + @CommandLine.Parameters(description = "Secret keys used for signing", + paramLabel = "KEY", + arity = "1..*") + File[] secretKeyFile; + + @Override + public void run() { + PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[secretKeyFile.length]; + for (int i = 0, secretKeyFileLength = secretKeyFile.length; i < secretKeyFileLength; i++) { + File file = secretKeyFile[i]; + try { + PGPSecretKeyRing secretKey = PGPainless.readKeyRing().secretKeyRing(new FileInputStream(file)); + secretKeys[i] = secretKey; + } catch (IOException | PGPException e) { + err_ln("Error reading secret key ring " + file.getName()); + err_ln(e.getMessage()); + System.exit(1); + return; + } + } + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionBuilderInterface.DocumentType documentType = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .doNotEncrypt() + .createDetachedSignature() + .signWith(new UnprotectedKeysProtector(), secretKeys); + + EncryptionBuilderInterface.Armor builder = type == Type.text ? documentType.signCanonicalText() : documentType.signBinaryDocument(); + EncryptionStream encryptionStream = armor ? builder.asciiArmor() : builder.noArmor(); + + Streams.pipeAll(System.in, encryptionStream); + encryptionStream.close(); + + PGPSignature signature = encryptionStream.getResult().getSignatures().iterator().next(); + + print_ln(Print.toString(signature.getEncoded(), armor)); + } catch (PGPException | IOException e) { + err_ln("Error signing data."); + err_ln(e.getMessage()); + System.exit(1); + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java new file mode 100644 index 00000000..cb66c7a6 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Verify.java @@ -0,0 +1,198 @@ +/* + * 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. + * 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.sop.commands; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import picocli.CommandLine; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.TimeZone; + +import static org.pgpainless.sop.Print.err_ln; +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "verify", + description = "Verify a detached signature over the data from standard input") +public class Verify implements Runnable { + + private static final TimeZone tz = TimeZone.getTimeZone("UTC"); + private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + + private static final Date beginningOfTime = new Date(0); + private static final Date endOfTime = new Date(8640000000000000L); + + static { + df.setTimeZone(tz); + } + + @CommandLine.Parameters(index = "0", + description = "Detached signature", + paramLabel = "SIGNATURE") + File signature; + + @CommandLine.Parameters(index = "1..*", + arity = "1..*", + description = "Public key certificates", + paramLabel = "CERT") + File[] certificates; + + @CommandLine.Option(names = {"--not-before"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").", + paramLabel = "DATE") + String notBefore = "-"; + + @CommandLine.Option(names = {"--not-after"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to current system time (\"now\").\n" + + "Accepts special value \"-\" for end of time.", + paramLabel = "DATE") + String notAfter = "now"; + + @Override + public void run() { + Date notBeforeDate = parseNotBefore(); + Date notAfterDate = parseNotAfter(); + + Map publicKeys = readCertificatesFromFiles(); + if (publicKeys.isEmpty()) { + err_ln("No certificates supplied."); + System.exit(19); + } + + OpenPgpMetadata metadata; + try (FileInputStream sigIn = new FileInputStream(signature)) { + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(System.in) + .doNotDecrypt() + .verifyDetachedSignature(sigIn) + .verifyWith(new HashSet<>(publicKeys.values())) + .ignoreMissingPublicKeys() + .build(); + + OutputStream out = new NullOutputStream(); + Streams.pipeAll(verifier, out); + verifier.close(); + + metadata = verifier.getResult(); + } catch (FileNotFoundException e) { + err_ln("Signature file not found:"); + err_ln(e.getMessage()); + System.exit(1); + return; + } catch (IOException | PGPException e) { + err_ln("Signature validation failed."); + err_ln(e.getMessage()); + System.exit(1); + return; + } + + Map signaturesInTimeRange = new HashMap<>(); + for (OpenPgpV4Fingerprint fingerprint : metadata.getVerifiedSignatures().keySet()) { + PGPSignature signature = metadata.getVerifiedSignatures().get(fingerprint); + Date creationTime = signature.getCreationTime(); + if (!creationTime.before(notBeforeDate) && !creationTime.after(notAfterDate)) { + signaturesInTimeRange.put(fingerprint, signature); + } + } + + if (signaturesInTimeRange.isEmpty()) { + err_ln("No valid signatures found."); + System.exit(3); + } + + printValidSignatures(signaturesInTimeRange, publicKeys); + } + + private void printValidSignatures(Map validSignatures, Map publicKeys) { + for (OpenPgpV4Fingerprint sigKeyFp : validSignatures.keySet()) { + PGPSignature signature = validSignatures.get(sigKeyFp); + for (File file : publicKeys.keySet()) { + // Search signing key ring + PGPPublicKeyRing publicKeyRing = publicKeys.get(file); + if (publicKeyRing.getPublicKey(sigKeyFp.getKeyId()) == null) { + continue; + } + + String utcSigDate = df.format(signature.getCreationTime()); + OpenPgpV4Fingerprint primaryKeyFp = new OpenPgpV4Fingerprint(publicKeyRing); + print_ln(utcSigDate + " " + sigKeyFp.toString() + " " + primaryKeyFp.toString() + + " signed by " + file.getName()); + } + } + } + + private Map readCertificatesFromFiles() { + Map publicKeys = new HashMap<>(); + for (File cert : certificates) { + try (FileInputStream in = new FileInputStream(cert)) { + publicKeys.put(cert, PGPainless.readKeyRing().publicKeyRing(in)); + } catch (IOException e) { + err_ln("Cannot read certificate from file " + cert.getAbsolutePath() + ":"); + err_ln(e.getMessage()); + } + } + return publicKeys; + } + + private Date parseNotAfter() { + try { + return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? endOfTime : df.parse(notAfter); + } catch (ParseException e) { + err_ln("Invalid date string supplied as value of --not-after."); + System.exit(1); + return null; + } + } + + private Date parseNotBefore() { + try { + return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime : df.parse(notBefore); + } catch (ParseException e) { + err_ln("Invalid date string supplied as value of --not-before."); + System.exit(1); + return null; + } + } + + private static class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + // Nope + } + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java new file mode 100644 index 00000000..8a5da78b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Version.java @@ -0,0 +1,29 @@ +/* + * 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. + * 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.sop.commands; + +import picocli.CommandLine; + +import static org.pgpainless.sop.Print.print_ln; + +@CommandLine.Command(name = "version", description = "Display version information about the tool") +public class Version implements Runnable { + + @Override + public void run() { + print_ln("PGPainless CLI version 0.0.1"); + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java new file mode 100644 index 00000000..633d7afb --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + * 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. + */ +/** + * Subcommands of the PGPainless SOP. + */ +package org.pgpainless.sop.commands; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java new file mode 100644 index 00000000..43afb989 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + * 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. + */ +/** + * PGPainless SOP implementing a Stateless OpenPGP Command Line Interface. + * @see + * Stateless OpenPGP Command Line Interface + * draft-dkg-openpgp-stateless-cli-01 + */ +package org.pgpainless.sop; diff --git a/settings.gradle b/settings.gradle index b3ba260c..5cc5fd5c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'PGPainless' -include 'pgpainless-core' +include 'pgpainless-core', + 'pgpainless-sop'