From 89a0adddd89c39a44dfa2b3964b70b44edb66350 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 6 May 2021 00:04:03 +0200 Subject: [PATCH] Reworking encryption/decryption API. --- README.md | 6 +- .../algorithm/DocumentSignatureType.java | 34 ++ .../encryption_signing/EncryptionBuilder.java | 450 +++++++----------- .../EncryptionBuilderInterface.java | 243 ++++++---- .../encryption_signing/EncryptionOptions.java | 129 +++++ .../encryption_signing/EncryptionResult.java | 127 +++++ .../encryption_signing/EncryptionStream.java | 183 +++---- .../encryption_signing/ProducerOptions.java | 135 ++++++ .../encryption_signing/SigningOptions.java | 136 ++++++ .../exception/KeyValidationException.java | 25 + .../org/pgpainless/key/EvaluatedKeyRing.java | 36 ++ .../org/pgpainless/key/SubkeyIdentifier.java | 27 ++ .../org/pgpainless/key/info/KeyRingInfo.java | 236 ++++++--- .../java/org/pgpainless/policy/Policy.java | 47 ++ .../signature/SelectSignatureFromKey.java | 2 +- .../pgpainless/signature/SignaturePicker.java | 30 +- .../subpackets/SignatureSubpacketsUtil.java | 57 +++ .../bouncycastle/PGPPublicKeyRingTest.java | 22 + .../EncryptDecryptTest.java | 41 +- .../EncryptionStreamClosedTest.java | 4 +- .../encryption_signing/FileInfoTest.java | 4 +- .../encryption_signing/LengthTest.java | 9 +- .../encryption_signing/SigningTest.java | 11 +- .../ChangeSecretKeyRingPassphraseTest.java | 3 +- ...ultiPassphraseSymmetricEncryptionTest.java | 8 +- .../SymmetricEncryptionTest.java | 14 +- ...ncryptCommsStorageFlagsDifferentiated.java | 2 +- .../weird_keys/TestTwoSubkeysEncryption.java | 10 +- .../org/pgpainless/sop/commands/Encrypt.java | 54 ++- 29 files changed, 1454 insertions(+), 631 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationException.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/EvaluatedKeyRing.java diff --git a/README.md b/README.md index c0423c5c..7cea2c76 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ About PGPainless aims to make using OpenPGP in Java projects as simple as possible. It does so by introducing an intuitive Builder structure, which allows easy -setup of encryption / decrytion operations, as well as straight forward key generation. +setup of encryptionOptions / decrytion operations, as well as straight forward key generation. PGPainless is based around the Bouncycastle java library and can be used on Android down to API level 10. @@ -74,7 +74,7 @@ Take for example a look at this delicious key: ### Encrypt / Sign Data -Encrypting and signing data is pretty straight forward as well. +Encrypting and signingOptions data is pretty straight forward as well. ```java EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(targetOuputStream) @@ -102,7 +102,7 @@ Additionally you can get information about the encrypted data by calling OpenPgpMetadata result = encryptor.getResult(); ``` -This object will contain information like to which keys the message is encrypted, which keys were used for signing and so on. +This object will contain information like to which keys the message is encrypted, which keys were used for signingOptions and so on. ### Decrypt / Verify Encrypted Data diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java new file mode 100644 index 00000000..20b3da38 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 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.algorithm; + +public enum DocumentSignatureType { + + BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), + + CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT), + ; + + final SignatureType signatureType; + + DocumentSignatureType(SignatureType signatureType) { + this.signatureType = signatureType; + } + + public SignatureType getSignatureType() { + return signatureType; + } +} 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 c0684438..d830ccc4 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,311 +17,202 @@ 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; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.OpenPgpMetadata; -import org.pgpainless.key.KeyRingValidator; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.exception.KeyValidationException; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; import org.pgpainless.util.Passphrase; -import org.pgpainless.util.Tuple; -import org.pgpainless.util.selection.key.PublicKeySelectionStrategy; -import org.pgpainless.util.selection.key.SecretKeySelectionStrategy; -import org.pgpainless.util.selection.key.impl.And; -import org.pgpainless.util.selection.key.impl.EncryptionKeySelectionStrategy; -import org.pgpainless.util.selection.key.impl.NoRevocation; -import org.pgpainless.util.selection.key.impl.SignatureKeySelectionStrategy; public class EncryptionBuilder implements EncryptionBuilderInterface { - private final EncryptionStream.Purpose purpose; private OutputStream outputStream; - private final Map encryptionKeys = new ConcurrentHashMap<>(); - private final Set encryptionPassphrases = new HashSet<>(); - private boolean detachedSignature = false; - private SignatureType signatureType = SignatureType.BINARY_DOCUMENT; - private final Map signingKeys = new ConcurrentHashMap<>(); - private SecretKeyRingProtector signingKeysDecryptor; - private SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_128; - private HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256; - private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; - private boolean asciiArmor = false; + private EncryptionOptions encryptionOptions; + private SigningOptions signingOptions = new SigningOptions(); + private ProducerOptions options; private OpenPgpMetadata.FileInfo fileInfo; public EncryptionBuilder() { - this.purpose = EncryptionStream.Purpose.COMMUNICATIONS; + this.encryptionOptions = new EncryptionOptions(EncryptionStream.Purpose.COMMUNICATIONS); } public EncryptionBuilder(@Nonnull EncryptionStream.Purpose purpose) { - this.purpose = purpose; + this.encryptionOptions = new EncryptionOptions(purpose); } @Override - public ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo) { + public ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo) { this.outputStream = outputStream; this.fileInfo = fileInfo; - return new ToRecipientsImpl(); + return new ToRecipientsOrNoEncryptionImpl(); } class ToRecipientsImpl implements ToRecipients { @Override - public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys) { - if (keys.length == 0) { - throw new IllegalArgumentException("No public keys provided."); - } + public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key) { + encryptionOptions.addRecipient(key); + return new AdditionalRecipientsImpl(); + } - Map encryptionKeys = new ConcurrentHashMap<>(); + @Override + public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull String userId) { + encryptionOptions.addRecipient(key, userId); + return new AdditionalRecipientsImpl(); + } + + @Override + public AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRingCollection keys, @Nonnull String userId) { for (PGPPublicKeyRing ring : keys) { - PGPPublicKeyRing validatedKeyRing = KeyRingValidator.validate(ring, PGPainless.getPolicy()); - for (PGPPublicKey k : validatedKeyRing) { - if (encryptionKeySelector().accept(k)) { - encryptionKeys.put(new SubkeyIdentifier(ring, k.getKeyID()), ring); - } - } + encryptionOptions.addRecipient(ring, userId); } - if (encryptionKeys.isEmpty()) { - throw new IllegalArgumentException("No valid encryption keys found!"); - } - EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys); - - return new WithAlgorithmsImpl(); - } - - private String getPrimaryUserId(PGPPublicKey publicKey) { - // TODO: Use real function to get primary userId. - return publicKey.getUserIDs().next(); + return new AdditionalRecipientsImpl(); } @Override - public WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... keys) { - if (keys.length == 0) { - throw new IllegalArgumentException("No key ring collections provided."); + public AdditionalRecipients toRecipients(@Nonnull PGPPublicKeyRingCollection keys) { + for (PGPPublicKeyRing ring : keys) { + encryptionOptions.addRecipient(ring); } - - for (PGPPublicKeyRingCollection collection : keys) { - for (PGPPublicKeyRing ring : collection) { - Map encryptionKeys = new ConcurrentHashMap<>(); - for (PGPPublicKey k : ring) { - if (encryptionKeySelector().accept(k)) { - encryptionKeys.put(new SubkeyIdentifier(ring, k.getKeyID()), ring); - } - } - - if (encryptionKeys.isEmpty()) { - throw new IllegalArgumentException("No valid encryption keys found!"); - } - - EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys); - } - } - - return new WithAlgorithmsImpl(); + return new AdditionalRecipientsImpl(); } @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(); + public AdditionalRecipients forPassphrase(Passphrase passphrase) { + encryptionOptions.addPassphrase(passphrase); + return new AdditionalRecipientsImpl(); } } - class WithAlgorithmsImpl implements WithAlgorithms { + class ToRecipientsOrNoEncryptionImpl extends ToRecipientsImpl implements ToRecipientsOrNoEncryption { @Override - public WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRing... keys) { - if (keys.length == 0) { - throw new IllegalArgumentException("Recipient list MUST NOT be empty."); + public EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException { + if (options == null) { + throw new NullPointerException("ProducerOptions cannot be null."); } - for (PGPPublicKeyRing ring : keys) { - Map encryptionKeys = new ConcurrentHashMap<>(); - for (Iterator i = ring.getPublicKeys(); i.hasNext(); ) { - PGPPublicKey key = i.next(); - if (encryptionKeySelector().accept(key)) { - encryptionKeys.put(new SubkeyIdentifier(ring, key.getKeyID()), ring); - } - } - if (encryptionKeys.isEmpty()) { - throw new IllegalArgumentException("No suitable encryption key found in the key ring " + new OpenPgpV4Fingerprint(ring)); - } - EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys); - } - return this; + return new EncryptionStream(outputStream, options, fileInfo); } @Override - public WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRingCollection keys) { - for (PGPPublicKeyRing ring : keys) { - Map encryptionKeys = new ConcurrentHashMap<>(); - for (Iterator i = ring.getPublicKeys(); i.hasNext(); ) { - PGPPublicKey key = i.next(); - if (encryptionKeySelector().accept(key)) { - encryptionKeys.put(new SubkeyIdentifier(ring, key.getKeyID()), ring); - } - } - if (encryptionKeys.isEmpty()) { - throw new IllegalArgumentException("No suitable encryption key found in the key ring " + new OpenPgpV4Fingerprint(ring)); - } - EncryptionBuilder.this.encryptionKeys.putAll(encryptionKeys); - } - return this; - } - - @Override - public DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull CompressionAlgorithm compressionAlgorithm) { - - EncryptionBuilder.this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; - EncryptionBuilder.this.hashAlgorithm = hashAlgorithm; - EncryptionBuilder.this.compressionAlgorithm = compressionAlgorithm; - - return new DetachedSignImpl(); - } - - @Override - public DetachedSign usingSecureAlgorithms() { - EncryptionBuilder.this.symmetricKeyAlgorithm = SymmetricKeyAlgorithm.AES_256; - EncryptionBuilder.this.hashAlgorithm = HashAlgorithm.SHA512; - EncryptionBuilder.this.compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; - - return new DetachedSignImpl(); - } - - @Override - public ToRecipients and() { - return new ToRecipientsImpl(); + public SignWithOrDontSign doNotEncrypt() { + EncryptionBuilder.this.encryptionOptions = null; + return new SignWithOrDontSignImpl(); } } - class DetachedSignImpl implements DetachedSign { + class AdditionalRecipientsImpl implements AdditionalRecipients { + @Override + public ToRecipientsOrSign and() { + return new ToRecipientsOrSignImpl(); + } + } + + class ToRecipientsOrSignImpl extends ToRecipientsImpl implements ToRecipientsOrSign { @Override - public SignWith createDetachedSignature() { - EncryptionBuilder.this.detachedSignature = true; - return new SignWithImpl(); + public Armor doNotSign() { + EncryptionBuilder.this.signingOptions = null; + return new ArmorImpl(); } + @Override + public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) throws KeyValidationException { + return new SignWithImpl().signWith(decryptor, keyRings); + } + + @Override + public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) { + return new SignWithImpl().signWith(decryptor, keyRings); + } + + @Override + public AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException { + return new SignWithImpl().signInlineWith(secretKeyDecryptor, signingKey, userId, signatureType); + } + + @Override + public AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException { + return new SignWithImpl().signDetachedWith(secretKeyDecryptor, signingKey, userId, signatureType); + } + } + + class SignWithOrDontSignImpl extends SignWithImpl implements SignWithOrDontSign { + @Override public Armor doNotSign() { return new ArmorImpl(); } - - @Override - public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) { - return new SignWithImpl().signWith(decryptor, keyRings); - } - - @Override - public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) { - return new SignWithImpl().signWith(decryptor, keyRings); - } - } class SignWithImpl implements SignWith { @Override - public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, - @Nonnull PGPSecretKeyRing... keyRings) { - if (keyRings.length == 0) { - throw new IllegalArgumentException("Signing key list MUST NOT be empty."); + public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, + @Nonnull PGPSecretKeyRing... keyRings) + throws KeyValidationException { + for (PGPSecretKeyRing secretKeyRing : keyRings) { + signingOptions.addInlineSignature(decryptor, secretKeyRing, DocumentSignatureType.BINARY_DOCUMENT); } - for (PGPSecretKeyRing ring : keyRings) { - Map signingKeys = new ConcurrentHashMap<>(); - for (Iterator i = ring.getSecretKeys(); i.hasNext(); ) { - PGPSecretKey s = i.next(); - if (EncryptionBuilder.this.signingKeySelector().accept(s)) { - signingKeys.put(new SubkeyIdentifier(ring, s.getKeyID()), ring); - } - } - - if (signingKeys.isEmpty()) { - throw new IllegalArgumentException("No suitable signing key found in the key ring " + new OpenPgpV4Fingerprint(ring)); - } - - EncryptionBuilder.this.signingKeys.putAll(signingKeys); - } - EncryptionBuilder.this.signingKeysDecryptor = decryptor; - return new DocumentTypeImpl(); + return new AdditionalSignWithImpl(); } @Override - public DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) { - Iterator iterator = keyRings.iterator(); - if (!iterator.hasNext()) { - throw new IllegalArgumentException("Signing key collection MUST NOT be empty."); + public AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) + throws KeyValidationException { + for (PGPSecretKeyRing key : keyRings) { + signingOptions.addInlineSignature(decryptor, key, DocumentSignatureType.BINARY_DOCUMENT); } - while (iterator.hasNext()) { - PGPSecretKeyRing ring = iterator.next(); - Map signingKeys = new ConcurrentHashMap<>(); - for (Iterator i = ring.getSecretKeys(); i.hasNext(); ) { - PGPSecretKey s = i.next(); - if (EncryptionBuilder.this.signingKeySelector().accept(s)) { - signingKeys.put(new SubkeyIdentifier(ring, s.getKeyID()), ring); - } - } + return new AdditionalSignWithImpl(); + } - if (signingKeys.isEmpty()) { - throw new IllegalArgumentException("No suitable signing key found in the key ring " + new OpenPgpV4Fingerprint(ring)); - } + @Override + public AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing signingKey, + String userId, + DocumentSignatureType signatureType) + throws KeyValidationException, PGPException { + signingOptions.addInlineSignature(secretKeyDecryptor, signingKey, userId, signatureType); + return new AdditionalSignWithImpl(); + } - EncryptionBuilder.this.signingKeys.putAll(signingKeys); - } - - EncryptionBuilder.this.signingKeysDecryptor = decryptor; - return new DocumentTypeImpl(); + @Override + public AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, + @Nonnull PGPSecretKeyRing signingKey, + String userId, + DocumentSignatureType signatureType) + throws PGPException, KeyValidationException { + signingOptions.addInlineSignature(secretKeyDecryptor, signingKey, userId, signatureType); + return new AdditionalSignWithImpl(); } } - class DocumentTypeImpl implements DocumentType { + class AdditionalSignWithImpl implements AdditionalSignWith { @Override - public Armor signBinaryDocument() { - EncryptionBuilder.this.signatureType = SignatureType.BINARY_DOCUMENT; - return new ArmorImpl(); + public SignWith and() { + return new SignWithImpl(); } @Override - public Armor signCanonicalText() { - EncryptionBuilder.this.signatureType = SignatureType.CANONICAL_TEXT_DOCUMENT; - return new ArmorImpl(); + public EncryptionStream asciiArmor() throws IOException, PGPException { + return new ArmorImpl().asciiArmor(); + } + + @Override + public EncryptionStream noArmor() throws IOException, PGPException { + return new ArmorImpl().noArmor(); } } @@ -329,70 +220,87 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { @Override public EncryptionStream asciiArmor() throws IOException, PGPException { - EncryptionBuilder.this.asciiArmor = true; + assignProducerOptions(); + options.setAsciiArmor(true); return build(); } @Override public EncryptionStream noArmor() throws IOException, PGPException { - EncryptionBuilder.this.asciiArmor = false; + assignProducerOptions(); + options.setAsciiArmor(false); return build(); } private EncryptionStream build() throws IOException, PGPException { - - Map> privateKeys = new ConcurrentHashMap<>(); - for (SubkeyIdentifier signingKey : signingKeys.keySet()) { - PGPSecretKeyRing secretKeyRing = signingKeys.get(signingKey); - PGPSecretKey secretKey = secretKeyRing.getSecretKey(signingKey.getSubkeyFingerprint().getKeyId()); - PBESecretKeyDecryptor decryptor = signingKeysDecryptor.getDecryptor(secretKey.getKeyID()); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, decryptor); - privateKeys.put(signingKey, new Tuple<>(secretKeyRing, privateKey)); - } - return new EncryptionStream( EncryptionBuilder.this.outputStream, - EncryptionBuilder.this.encryptionKeys, - EncryptionBuilder.this.encryptionPassphrases, - EncryptionBuilder.this.detachedSignature, - signatureType, - privateKeys, - EncryptionBuilder.this.symmetricKeyAlgorithm, - EncryptionBuilder.this.hashAlgorithm, - EncryptionBuilder.this.compressionAlgorithm, - EncryptionBuilder.this.asciiArmor, + EncryptionBuilder.this.options, fileInfo); } - } - PublicKeySelectionStrategy encryptionKeySelector() { - KeyFlag[] flags = mapPurposeToKeyFlags(purpose); - return new And.PubKeySelectionStrategy( - new NoRevocation.PubKeySelectionStrategy(), - new EncryptionKeySelectionStrategy(flags)); - } - - SecretKeySelectionStrategy signingKeySelector() { - return new And.SecKeySelectionStrategy( - new NoRevocation.SecKeySelectionStrategy(), - new SignatureKeySelectionStrategy()); - } - - private static KeyFlag[] mapPurposeToKeyFlags(EncryptionStream.Purpose purpose) { - KeyFlag[] flags; - switch (purpose) { - case COMMUNICATIONS: - flags = new KeyFlag[] {KeyFlag.ENCRYPT_COMMS}; - break; - case STORAGE: - flags = new KeyFlag[] {KeyFlag.ENCRYPT_STORAGE}; - break; - case STORAGE_AND_COMMUNICATIONS: - flags = new KeyFlag[] {KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE}; - break; - default: - throw new AssertionError("Illegal purpose enum value encountered."); + private void assignProducerOptions() { + if (encryptionOptions != null && signingOptions != null) { + options = ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions); + } else if (encryptionOptions != null) { + options = ProducerOptions.encrypt(encryptionOptions); + } else if (signingOptions != null) { + options = ProducerOptions.sign(signingOptions); + } else { + options = ProducerOptions.noEncryptionNoSigning(); + } } - return flags; + } + + /** + * Negotiate the {@link SymmetricKeyAlgorithm} used for message encryption. + * If the user chose to set an override ({@link EncryptionOptions#overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}, use that. + * Otherwise find an algorithm which is acceptable for all recipients. + * If no consensus can be reached, use {@link Policy.SymmetricKeyAlgorithmPolicy#getDefaultSymmetricKeyAlgorithm()}. + * + * @param encryptionOptions encryption options + * @return negotiated symmetric key algorithm + */ + public static SymmetricKeyAlgorithm negotiateSymmetricEncryptionAlgorithm(EncryptionOptions encryptionOptions) { + SymmetricKeyAlgorithm encryptionAlgorithmOverride = encryptionOptions.getEncryptionAlgorithmOverride(); + if (encryptionAlgorithmOverride != null) { + return encryptionAlgorithmOverride; + } + + // TODO: Negotiation + + return PGPainless.getPolicy().getSymmetricKeyAlgorithmPolicy().getDefaultSymmetricKeyAlgorithm(); + } + + /** + * Negotiate the {@link HashAlgorithm} used for signatures. + * + * If we encrypt and sign, we look at the recipients keys to determine which algorithm to use. + * If we only sign, we look at the singing keys preferences instead. + * + * @param encryptionOptions encryption options (recipients keys) + * @param signingOptions signing options (signing keys) + * @return negotiated hash algorithm + */ + public static HashAlgorithm negotiateSignatureHashAlgorithm(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { + HashAlgorithm hashAlgorithmOverride = signingOptions.getHashAlgorithmOverride(); + if (hashAlgorithmOverride != null) { + return hashAlgorithmOverride; + } + + // TODO: Negotiation + + return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); + } + + public static CompressionAlgorithm negotiateCompressionAlgorithm(ProducerOptions producerOptions) { + CompressionAlgorithm compressionAlgorithmOverride = producerOptions.getCompressionAlgorithmOverride(); + if (compressionAlgorithmOverride != null) { + return compressionAlgorithmOverride; + } + + // TODO: Negotiation + + return PGPainless.getPolicy().getCompressionAlgorithmPolicy().defaultCompressionAlgorithm(); } } 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 0e16f33e..78e25b22 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 @@ -25,11 +25,10 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.exception.KeyValidationException; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; @@ -42,7 +41,7 @@ public interface EncryptionBuilderInterface { * @param outputStream output stream of the plain data. * @return api handle */ - default ToRecipients onOutputStream(@Nonnull OutputStream outputStream) { + default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream) { return onOutputStream(outputStream, OpenPgpMetadata.FileInfo.binaryStream()); } /** @@ -55,7 +54,7 @@ public interface EncryptionBuilderInterface { * * @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead. */ - default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, boolean forYourEyesOnly) { + default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, boolean forYourEyesOnly) { return onOutputStream(outputStream, forYourEyesOnly ? OpenPgpMetadata.FileInfo.forYourEyesOnly() : OpenPgpMetadata.FileInfo.binaryStream()); } @@ -70,7 +69,7 @@ public interface EncryptionBuilderInterface { * * @deprecated use {@link #onOutputStream(OutputStream, OpenPgpMetadata.FileInfo)} instead. */ - default ToRecipients onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) { + default ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, String fileName, boolean forYourEyesOnly) { return onOutputStream(outputStream, new OpenPgpMetadata.FileInfo(forYourEyesOnly ? "_CONSOLE" : fileName, new Date(), StreamEncoding.BINARY)); } @@ -82,102 +81,99 @@ public interface EncryptionBuilderInterface { * @param fileInfo file information * @return api handle */ - ToRecipients onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo); + ToRecipientsOrNoEncryption onOutputStream(@Nonnull OutputStream outputStream, OpenPgpMetadata.FileInfo fileInfo); - interface ToRecipients { + interface ToRecipientsOrNoEncryption extends ToRecipients { /** - * Pass in a list of trusted public key rings of the recipients. + * Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...). * - * @param keys recipient keys for which the message will be encrypted. - * @return api handle + * @param options options + * @return encryption strea */ - WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRing... keys); - - /** - * Pass in a list of trusted public key ring collections of the recipients. - * - * @param keys recipient keys for which the message will be encrypted. - * @return api handle - */ - WithAlgorithms toRecipients(@Nonnull PGPPublicKeyRingCollection... 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); + EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException; /** * Instruct the {@link EncryptionStream} to not encrypt any data. * * @return api handle */ - DetachedSign doNotEncrypt(); + SignWithOrDontSign doNotEncrypt(); + } + + interface ToRecipients { + + /** + * Encrypt for the given valid public key. + * TODO: Explain the difference between this and {@link #toRecipient(PGPPublicKeyRing, String)}. + * + * @param key recipient key for which the message will be encrypted. + * @return api handle + */ + AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key); + + /** + * Encrypt for the given valid key using the provided user-id signature to determine preferences. + * + * @param key public key + * @param userId user-id which is used to select the correct encryption parameters based on preferences. + * @return api handle + */ + AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull String userId); + + /** + * Encrypt for the first valid key in the provided keys collection which has a valid user-id that matches + * the provided userId. + * The user-id is also used to determine encryption preferences. + * + * @param keys collection of keys + * @param userId user-id used to select the correct key + * @return api handle + */ + AdditionalRecipients toRecipient(@Nonnull PGPPublicKeyRingCollection keys, @Nonnull String userId); + + /** + * Encrypt for all valid public keys in the provided collection. + * If any key is not eligible for encryption (e.g. expired, revoked...), an exception will be thrown. + * TODO: which exception? + * + * @param keys collection of public keys + * @return api handle + */ + AdditionalRecipients toRecipients(@Nonnull PGPPublicKeyRingCollection keys); + + /** + * Symmetrically encrypt the message using a passphrase. + * Note that the passphrase MUST NOT be empty. + * + * @param passphrase passphrase + * @return api handle + */ + AdditionalRecipients forPassphrase(Passphrase passphrase); } - interface WithAlgorithms { - + interface AdditionalRecipients { /** - * Add our own public key to the list of recipient keys. - * - * @param keys own public keys - * @return api handle - */ - WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRing... keys); - - /** - * Add our own public keys to the list of recipient keys. - * - * @param keys own public keys - * @return api handle - */ - WithAlgorithms andToSelf(@Nonnull PGPPublicKeyRingCollection keys); - - /** - * Specify which algorithms should be used for the encryption. - * - * @param symmetricKeyAlgorithm symmetric algorithm for the session key - * @param hashAlgorithm hash algorithm - * @param compressionAlgorithm compression algorithm - * @return api handle - */ - DetachedSign usingAlgorithms(@Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull CompressionAlgorithm compressionAlgorithm); - - /** - * Use a suite of algorithms that are considered secure. + * Add an additional recipient key/passphrase or configure signing. * * @return api handle */ - DetachedSign usingSecureAlgorithms(); - - ToRecipients and(); - + ToRecipientsOrSign and(); } - interface DetachedSign extends SignWith { - - /** - * Instruct the {@link EncryptionStream} to generate detached signatures instead of One-Pass-Signatures. - * Those can be retrieved later via {@link OpenPgpMetadata#getSignatures()}. - * - * @return api handle - */ - SignWith createDetachedSignature(); + // Allow additional recipient or signing configuration + interface ToRecipientsOrSign extends ToRecipients, SignWithOrDontSign { + } + // Allow signing configuration or no signing at all + interface SignWithOrDontSign extends SignWith { /** * Do not sign the plain data at all. * * @return api handle */ Armor doNotSign(); - } interface SignWith { @@ -186,21 +182,104 @@ public interface EncryptionBuilderInterface { * Pass in a list of secret keys used for signing, along with a {@link SecretKeyRingProtector} used to unlock * the secret keys. * + * @deprecated use {@link #signInlineWith(SecretKeyRingProtector, PGPSecretKeyRing)} instead. * @param decryptor {@link SecretKeyRingProtector} used to unlock the secret keys * @param keyRings secret keys used for signing * @return api handle */ - DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings); + @Deprecated + AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRing... keyRings) throws KeyValidationException; - DocumentType signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings); + /** + * Sign inline using the passed in secret keys. + * + * @deprecated use {@link #signInlineWith(SecretKeyRingProtector, PGPSecretKeyRing)} instead. + * @param decryptor for unlocking the secret keys + * @param keyRings secret keys + * @return api handle + */ + @Deprecated + AdditionalSignWith signWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection keyRings) throws KeyValidationException; + /** + * Create an inline signature using the provided secret key. + * The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @return api handle + */ + default AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey) throws PGPException, KeyValidationException { + return signInlineWith(secretKeyDecryptor, signingKey, null); + } + + /** + * Create an inline signature using the provided secret key. + * If userId is not null, the preferences of the matching user-id on the key will be used for signing. + * The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @param userId userId whose preferences shall be used for signing + * @return api handle + */ + default AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId) throws PGPException, KeyValidationException { + return signInlineWith(secretKeyDecryptor, signingKey, userId, DocumentSignatureType.BINARY_DOCUMENT); + } + + /** + * Create an inline signature using the provided secret key with the algorithm preferences of the provided user-id. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @param userId user-id whose preferences shall be used for signing + * @param signatureType signature type + * @return api handle + */ + AdditionalSignWith signInlineWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws KeyValidationException, PGPException; + + /** + * Create a detached signature using the provided secret key. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @return api handle + */ + default AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey) throws PGPException, KeyValidationException { + return signDetachedWith(secretKeyDecryptor, signingKey, null); + } + + /** + * Create a detached signature using the provided secret key with the algorithm preferences of the provided user-id. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @param userId user-id whose preferences shall be used for signing + * @return api handle + */ + default AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId) throws PGPException, KeyValidationException { + return signDetachedWith(secretKeyDecryptor, signingKey, userId, DocumentSignatureType.BINARY_DOCUMENT); + } + + /** + * Create a detached signature using the provided secret key with the algorithm preferences of the provided user-id. + * + * @param secretKeyDecryptor for unlocking the secret key + * @param signingKey signing key + * @param userId user-id whose preferences shall be used for signing + * @param signatureType type of the signature + * @return api handle + */ + AdditionalSignWith signDetachedWith(@Nonnull SecretKeyRingProtector secretKeyDecryptor, @Nonnull PGPSecretKeyRing signingKey, String userId, DocumentSignatureType signatureType) throws PGPException, KeyValidationException; } - interface DocumentType { - - Armor signBinaryDocument(); - - Armor signCanonicalText(); + interface AdditionalSignWith extends Armor { + /** + * Add an additional signing key/method. + * + * @return api handle + */ + SignWith and(); } interface Armor { diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java new file mode 100644 index 00000000..f3428c45 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -0,0 +1,129 @@ +/* + * Copyright 2021 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.encryption_signing; + +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.util.Passphrase; + +public class EncryptionOptions { + + private final EncryptionStream.Purpose purpose; + private final Set encryptionMethods = new LinkedHashSet<>(); + private final Set encryptionKeys = new LinkedHashSet<>(); + private final Map keyRingInfo = new HashMap<>(); + + private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; + + public EncryptionOptions(EncryptionStream.Purpose purpose) { + this.purpose = purpose; + } + + /** + * Add a recipient by providing a key and recipient user-id. + * The user-id is used to determine the recipients preferences (algorithms etc.). + * + * @param key key ring + * @param userId user id + */ + public void addRecipient(PGPPublicKeyRing key, String userId) { + KeyRingInfo info = new KeyRingInfo(key, new Date()); + + PGPPublicKey encryptionSubkey = info.getEncryptionSubkey(userId, purpose); + if (encryptionSubkey == null) { + throw new AssertionError("Key has no encryption subkey."); + } + addRecipientKey(key, encryptionSubkey); + } + + /** + * Add a recipient by providing a key. + * + * @param key key ring + */ + public void addRecipient(PGPPublicKeyRing key) { + KeyRingInfo info = new KeyRingInfo(key, new Date()); + PGPPublicKey encryptionSubkey = info.getEncryptionSubkey(purpose); + if (encryptionSubkey == null) { + throw new AssertionError("Key has no encryption subkey."); + } + addRecipientKey(key, encryptionSubkey); + } + + private void addRecipientKey(PGPPublicKeyRing keyRing, PGPPublicKey key) { + encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); + PGPKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory + .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); + addEncryptionMethod(encryptionMethod); + } + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + */ + public void addPassphrase(Passphrase passphrase) { + if (passphrase.isEmpty()) { + throw new IllegalArgumentException("Passphrase must not be empty."); + } + PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory + .getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); + addEncryptionMethod(encryptionMethod); + } + + /** + * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. + * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) + * or {@link PGPKeyEncryptionMethodGenerator} (public key). + * + * This method is intended for advanced users to allow encryption for specific subkeys. + * This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless. + * + * @param encryptionMethod encryption method + */ + public void addEncryptionMethod(PGPKeyEncryptionMethodGenerator encryptionMethod) { + encryptionMethods.add(encryptionMethod); + } + + public Set getEncryptionMethods() { + return new HashSet<>(encryptionMethods); + } + + public Set getEncryptionKeyIdentifiers() { + return new HashSet<>(encryptionKeys); + } + + public SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() { + return encryptionAlgorithmOverride; + } + + public void overrideEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { + this.encryptionAlgorithmOverride = encryptionAlgorithm; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java new file mode 100644 index 00000000..e050ad04 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java @@ -0,0 +1,127 @@ +/* + * Copyright 2021 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.encryption_signing; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.MultiMap; + +public final class EncryptionResult { + + private final SymmetricKeyAlgorithm encryptionAlgorithm; + private final CompressionAlgorithm compressionAlgorithm; + + private final MultiMap detachedSignatures; + private final Set recipients; + private final OpenPgpMetadata.FileInfo fileInfo; + + private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm, + CompressionAlgorithm compressionAlgorithm, + MultiMap detachedSignatures, + Set recipients, + OpenPgpMetadata.FileInfo fileInfo) { + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + this.detachedSignatures = detachedSignatures; + this.recipients = Collections.unmodifiableSet(recipients); + this.fileInfo = fileInfo; + } + + @Deprecated + public SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { + return getEncryptionAlgorithm(); + } + + public SymmetricKeyAlgorithm getEncryptionAlgorithm() { + return encryptionAlgorithm; + } + + public CompressionAlgorithm getCompressionAlgorithm() { + return compressionAlgorithm; + } + + public MultiMap getDetachedSignatures() { + return detachedSignatures; + } + + public Set getRecipients() { + return recipients; + } + + public OpenPgpMetadata.FileInfo getFileInfo() { + return fileInfo; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private SymmetricKeyAlgorithm encryptionAlgorithm; + private CompressionAlgorithm compressionAlgorithm; + + private final MultiMap detachedSignatures = new MultiMap<>(); + private Set recipients = new HashSet<>(); + private OpenPgpMetadata.FileInfo fileInfo; + + public Builder setEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { + this.encryptionAlgorithm = encryptionAlgorithm; + return this; + } + + public Builder setCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) { + this.compressionAlgorithm = compressionAlgorithm; + return this; + } + + public Builder addRecipient(SubkeyIdentifier recipient) { + this.recipients.add(recipient); + return this; + } + + public Builder addDetachedSignature(SubkeyIdentifier signingSubkeyIdentifier, PGPSignature detachedSignature) { + this.detachedSignatures.put(signingSubkeyIdentifier, detachedSignature); + return this; + } + + public Builder setFileInfo(OpenPgpMetadata.FileInfo fileInfo) { + this.fileInfo = fileInfo; + return this; + } + + public EncryptionResult build() { + if (encryptionAlgorithm == null) { + throw new IllegalStateException("Encryption algorithm not set."); + } + if (compressionAlgorithm == null) { + throw new IllegalStateException("Compression algorithm not set."); + } + if (fileInfo == null) { + throw new IllegalStateException("File info not set."); + } + + return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients, fileInfo); + } + } +} 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 8cc4d974..732e1381 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 @@ -17,10 +17,6 @@ package org.pgpainless.encryption_signing; import java.io.IOException; import java.io.OutputStream; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; @@ -31,29 +27,18 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; -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.PGPKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; 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.OpenPgpMetadata; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.signature.DetachedSignature; import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.Tuple; /** * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. @@ -81,70 +66,36 @@ public final class EncryptionStream extends OutputStream { private static final Logger LOGGER = Logger.getLogger(EncryptionStream.class.getName()); private static final Level LEVEL = Level.FINE; + private final ProducerOptions options; + private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder(); + + private boolean closed = false; private static final int BUFFER_SIZE = 1 << 8; - private final SymmetricKeyAlgorithm symmetricKeyAlgorithm; - private final HashAlgorithm hashAlgorithm; - private final CompressionAlgorithm compressionAlgorithm; - private final Map encryptionKeys; - private final Set encryptionPassphrases; - private final boolean detachedSignature; - private final SignatureType signatureType; - private final Map> signingKeys; - private final boolean asciiArmor; - - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - - private Map> signatureGenerators = new ConcurrentHashMap<>(); - private boolean closed = false; - - OutputStream outermostStream = null; - + OutputStream outermostStream; private ArmoredOutputStream armorOutputStream = null; private OutputStream publicKeyEncryptedStream = null; - private PGPCompressedDataGenerator compressedDataGenerator; private BCPGOutputStream basicCompressionStream; - private PGPLiteralDataGenerator literalDataGenerator; private OutputStream literalDataStream; EncryptionStream(@Nonnull OutputStream targetOutputStream, - @Nonnull Map encryptionKeys, - @Nonnull Set encryptionPassphrases, - boolean detachedSignature, - SignatureType signatureType, - @Nonnull Map> signingKeys, - @Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull CompressionAlgorithm compressionAlgorithm, - boolean asciiArmor, + @Nonnull ProducerOptions options, @Nonnull OpenPgpMetadata.FileInfo fileInfo) throws IOException, PGPException { - - this.symmetricKeyAlgorithm = symmetricKeyAlgorithm; - this.hashAlgorithm = hashAlgorithm; - this.compressionAlgorithm = compressionAlgorithm; - this.encryptionKeys = Collections.unmodifiableMap(encryptionKeys); - this.encryptionPassphrases = Collections.unmodifiableSet(encryptionPassphrases); - this.detachedSignature = detachedSignature; - this.signatureType = signatureType; - this.signingKeys = Collections.unmodifiableMap(signingKeys); - this.asciiArmor = asciiArmor; - + this.options = options; outermostStream = targetOutputStream; + prepareArmor(); prepareEncryption(); - prepareSigning(); prepareCompression(); prepareOnePassSignatures(); prepareLiteralDataProcessing(fileInfo); - prepareResultBuilder(); - resultBuilder.setFileInfo(fileInfo); } private void prepareArmor() { - if (!asciiArmor) { + if (!options.isAsciiArmor()) { LOGGER.log(LEVEL, "Encryption output will be binary"); return; } @@ -155,14 +106,18 @@ public final class EncryptionStream extends OutputStream { } private void prepareEncryption() throws IOException, PGPException { - if (encryptionKeys.isEmpty() && encryptionPassphrases.isEmpty()) { + EncryptionOptions encryptionOptions = options.getEncryptionOptions(); + if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) { + // No encryption options/methods -> no encryption + resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL); return; } - LOGGER.log(LEVEL, "At least one encryption key is available -> encrypt using " + symmetricKeyAlgorithm); + SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions); + resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm); + LOGGER.log(LEVEL, "Encrypt message using " + encryptionAlgorithm); PGPDataEncryptorBuilder dataEncryptorBuilder = - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(symmetricKeyAlgorithm); - + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm); // Simplify once https://github.com/bcgit/bc-java/pull/859 is merged if (dataEncryptorBuilder instanceof BcPGPDataEncryptorBuilder) { ((BcPGPDataEncryptorBuilder) dataEncryptorBuilder).setWithIntegrityPacket(true); @@ -172,46 +127,21 @@ public final class EncryptionStream extends OutputStream { PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(dataEncryptorBuilder); - - for (SubkeyIdentifier keyIdentifier : encryptionKeys.keySet()) { - LOGGER.log(LEVEL, "Encrypt for key " + keyIdentifier); - PGPPublicKey key = encryptionKeys.get(keyIdentifier).getPublicKey(keyIdentifier.getSubkeyFingerprint().getKeyId()); - PublicKeyKeyEncryptionMethodGenerator keyEncryption = - ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); - encryptedDataGenerator.addMethod(keyEncryption); + for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) { + encryptedDataGenerator.addMethod(encryptionMethod); } - for (Passphrase passphrase : encryptionPassphrases) { - PBEKeyEncryptionMethodGenerator passphraseEncryption = - ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); - encryptedDataGenerator.addMethod(passphraseEncryption); + for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) { + resultBuilder.addRecipient(recipientSubkeyIdentifier); } publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); outermostStream = publicKeyEncryptedStream; } - private void prepareSigning() throws PGPException { - if (signingKeys.isEmpty()) { - return; - } - - LOGGER.log(LEVEL, "At least one signing key is available -> sign " + hashAlgorithm + " hash of message"); - for (SubkeyIdentifier subkeyIdentifier : signingKeys.keySet()) { - LOGGER.log(LEVEL, "Sign using key " + subkeyIdentifier); - - PGPPrivateKey privateKey = signingKeys.get(subkeyIdentifier).getSecond(); - PGPContentSignerBuilder contentSignerBuilder = ImplementationFactory.getInstance() - .getPGPContentSignerBuilder( - privateKey.getPublicKeyPacket().getAlgorithm(), - hashAlgorithm.getAlgorithmId()); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); - signatureGenerator.init(signatureType.getCode(), privateKey); - signatureGenerators.put(subkeyIdentifier, new Tuple<>(signingKeys.get(subkeyIdentifier).getFirst(), signatureGenerator)); - } - } - private void prepareCompression() throws IOException { + CompressionAlgorithm compressionAlgorithm = options.getCompressionAlgorithmOverride(); + resultBuilder.setCompressionAlgorithm(compressionAlgorithm); compressedDataGenerator = new PGPCompressedDataGenerator( compressionAlgorithm.getAlgorithmId()); if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) { @@ -224,9 +154,19 @@ public final class EncryptionStream extends OutputStream { } private void prepareOnePassSignatures() throws IOException, PGPException { - for (SubkeyIdentifier identifier : signatureGenerators.keySet()) { - PGPSignatureGenerator signatureGenerator = signatureGenerators.get(identifier).getSecond(); - signatureGenerator.generateOnePassVersion(false).encode(outermostStream); + SigningOptions signingOptions = options.getSigningOptions(); + if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { + // No singing options/methods -> no signing + return; + } + + for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier); + + if (!signingMethod.isDetached()) { + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); + signatureGenerator.generateOnePassVersion(false).encode(outermostStream); + } } } @@ -238,38 +178,41 @@ public final class EncryptionStream extends OutputStream { fileInfo.getModificationDate(), new byte[BUFFER_SIZE]); outermostStream = literalDataStream; - } - - private void prepareResultBuilder() { - for (SubkeyIdentifier recipient : encryptionKeys.keySet()) { - resultBuilder.addRecipientKeyId(recipient.getSubkeyFingerprint().getKeyId()); - } - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); + resultBuilder.setFileInfo(fileInfo); } @Override public void write(int data) throws IOException { outermostStream.write(data); + SigningOptions signingOptions = options.getSigningOptions(); + if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { + return; + } - for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) { - PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond(); + for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); byte asByte = (byte) (data & 0xff); signatureGenerator.update(asByte); } } @Override - public void write(byte[] buffer) throws IOException { + public void write(@Nonnull byte[] buffer) throws IOException { write(buffer, 0, buffer.length); } @Override - public void write(byte[] buffer, int off, int len) throws IOException { + public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { outermostStream.write(buffer, 0, len); - for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) { - PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond(); + SigningOptions signingOptions = options.getSigningOptions(); + if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { + return; + } + for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); signatureGenerator.update(buffer, 0, len); } } @@ -314,19 +257,23 @@ public final class EncryptionStream extends OutputStream { } private void writeSignatures() throws PGPException, IOException { - for (SubkeyIdentifier signingKey : signatureGenerators.keySet()) { - PGPSignatureGenerator signatureGenerator = signatureGenerators.get(signingKey).getSecond(); + SigningOptions signingOptions = options.getSigningOptions(); + if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { + return; + } + for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); PGPSignature signature = signatureGenerator.generate(); - if (!detachedSignature) { + if (signingMethod.isDetached()) { + resultBuilder.addDetachedSignature(signingKey, signature); + } else { signature.encode(outermostStream); } - DetachedSignature detachedSignature = new DetachedSignature( - signature, signatureGenerators.get(signingKey).getFirst(), signingKey); - resultBuilder.addDetachedSignature(detachedSignature); } } - public OpenPgpMetadata getResult() { + public EncryptionResult getResult() { if (!closed) { throw new IllegalStateException("EncryptionStream must be closed before accessing the Result."); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java new file mode 100644 index 00000000..0bf1411e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021 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.encryption_signing; + +import javax.annotation.Nullable; + +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; + +public final class ProducerOptions { + + private final EncryptionOptions encryptionOptions; + private final SigningOptions signingOptions; + + private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() + .defaultCompressionAlgorithm(); + private boolean asciiArmor = true; + + private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { + this.encryptionOptions = encryptionOptions; + this.signingOptions = signingOptions; + } + + /** + * Sign and encrypt some data. + * + * @param encryptionOptions encryption options + * @param signingOptions signing options + * @return builder + */ + public static ProducerOptions signAndEncrypt(EncryptionOptions encryptionOptions, + SigningOptions signingOptions) { + throwIfNull(encryptionOptions); + throwIfNull(signingOptions); + return new ProducerOptions(encryptionOptions, signingOptions); + } + + /** + * Sign some data without encryption. + * + * @param signingOptions signing options + * @return builder + */ + public static ProducerOptions sign(SigningOptions signingOptions) { + throwIfNull(signingOptions); + return new ProducerOptions(null, signingOptions); + } + + /** + * Encrypt some data without signing. + * + * @param encryptionOptions encryption options + * @return builder + */ + public static ProducerOptions encrypt(EncryptionOptions encryptionOptions) { + throwIfNull(encryptionOptions); + return new ProducerOptions(encryptionOptions, null); + } + + public static ProducerOptions noEncryptionNoSigning() { + return new ProducerOptions(null, null); + } + + private static void throwIfNull(EncryptionOptions encryptionOptions) { + if (encryptionOptions == null) { + throw new NullPointerException("EncryptionOptions cannot be null."); + } + } + + private static void throwIfNull(SigningOptions signingOptions) { + if (signingOptions == null) { + throw new NullPointerException("SigningOptions cannot be null."); + } + } + + /** + * Override which compression algorithm shall be used. + * + * @param compressionAlgorithm compression algorithm override + * @return builder + */ + public ProducerOptions overrideCompressionAlgorithm(CompressionAlgorithm compressionAlgorithm) { + if (compressionAlgorithm == null) { + throw new NullPointerException("Compression algorithm cannot be null."); + } + this.compressionAlgorithmOverride = compressionAlgorithm; + return this; + } + + /** + * Specify, whether or not the result of the encryption/signing operation shall be ascii armored. + * The default value is true. + * + * @param asciiArmor ascii armor + * @return builder + */ + public ProducerOptions setAsciiArmor(boolean asciiArmor) { + this.asciiArmor = asciiArmor; + return this; + } + + /** + * Return true if the output of the encryption/signing operation shall be ascii armored. + * + * @return ascii armored + */ + public boolean isAsciiArmor() { + return asciiArmor; + } + + public CompressionAlgorithm getCompressionAlgorithmOverride() { + return compressionAlgorithmOverride; + } + + public @Nullable EncryptionOptions getEncryptionOptions() { + return encryptionOptions; + } + + public @Nullable SigningOptions getSigningOptions() { + return signingOptions; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java new file mode 100644 index 00000000..2fec3d26 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -0,0 +1,136 @@ +/* + * Copyright 2021 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.encryption_signing; + +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.exception.KeyValidationException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public final class SigningOptions { + + public static final class SigningMethod { + private final PGPSignatureGenerator signatureGenerator; + private final boolean detached; + + private SigningMethod(PGPSignatureGenerator signatureGenerator, boolean detached) { + this.signatureGenerator = signatureGenerator; + this.detached = detached; + } + + public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator) { + return new SigningMethod(signatureGenerator, false); + } + + public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator) { + return new SigningMethod(signatureGenerator, true); + } + + public boolean isDetached() { + return detached; + } + + public PGPSignatureGenerator getSignatureGenerator() { + return signatureGenerator; + } + } + + private Map signingMethods = new HashMap<>(); + private HashAlgorithm hashAlgorithmOverride; + + public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + DocumentSignatureType signatureType) + throws KeyValidationException { + + } + + public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + String userId, + DocumentSignatureType signatureType) + throws KeyValidationException, PGPException { + KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); + if (userId != null) { + if (!keyRingInfo.isUserIdValid(userId)) { + throw new KeyValidationException(userId, keyRingInfo.getCurrentUserIdCertification(userId), keyRingInfo.getUserIdRevocation(userId)); + } + } + + PGPPublicKey signingPubKey = keyRingInfo.getSigningSubkey(); + if (signingPubKey == null) { + throw new AssertionError("Key has no valid signing key."); + } + PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); + PGPPrivateKey signingSubkey = signingSecKey.extractPrivateKey(secretKeyDecryptor.getDecryptor(signingPubKey.getKeyID())); + List hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID()); + addSigningMethod(secretKey, signingSubkey, hashAlgorithms.get(0), signatureType, false); + } + + private void addSigningMethod(PGPSecretKeyRing secretKey, + PGPPrivateKey signingSubkey, + HashAlgorithm hashAlgorithm, + DocumentSignatureType signatureType, + boolean detached) + throws PGPException { + SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); + PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType); + SigningMethod signingMethod = detached ? SigningMethod.detachedSignature(generator) : SigningMethod.inlineSignature(generator); + signingMethods.put(signingKeyIdentifier, signingMethod); + } + + private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey, + HashAlgorithm hashAlgorithm, + DocumentSignatureType signatureType) + throws PGPException { + int publicKeyAlgorithm = privateKey.getPublicKeyPacket().getAlgorithm(); + PGPContentSignerBuilder signerBuilder = ImplementationFactory.getInstance() + .getPGPContentSignerBuilder(publicKeyAlgorithm, hashAlgorithm.getAlgorithmId()); + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signerBuilder); + signatureGenerator.init(signatureType.getSignatureType().getCode(), privateKey); + + return signatureGenerator; + } + + public Map getSigningMethods() { + return Collections.unmodifiableMap(signingMethods); + } + + public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) { + this.hashAlgorithmOverride = hashAlgorithmOverride; + return this; + } + + public HashAlgorithm getHashAlgorithmOverride() { + return hashAlgorithmOverride; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationException.java new file mode 100644 index 00000000..a025c6bf --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyValidationException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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.exception; + +import org.bouncycastle.openpgp.PGPSignature; + +public class KeyValidationException extends AssertionError { + + public KeyValidationException(String userId, PGPSignature userIdSig, PGPSignature userIdRevocation) { + super("User-ID '" + userId + "' is not valid: Sig: " + userIdSig + " Rev: " + userIdRevocation); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/EvaluatedKeyRing.java b/pgpainless-core/src/main/java/org/pgpainless/key/EvaluatedKeyRing.java new file mode 100644 index 00000000..393666d5 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/EvaluatedKeyRing.java @@ -0,0 +1,36 @@ +package org.pgpainless.key; + +import java.util.List; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +public interface EvaluatedKeyRing { + + PGPSignature getUserIdCertification(String userId); + + PGPSignature getUserIdRevocation(String userId); + + PGPSignature getSubkeyBinding(long subkeyId); + + PGPSignature getSubkeyRevocation(long subkeyId); + + default boolean isUserIdRevoked(String userId) { + return getUserIdRevocation(userId) != null; + } + + default boolean isSubkeyRevoked(long subkeyId) { + return getSubkeyRevocation(subkeyId) != null; + } + + default @Nullable List getUserIdKeyFlags(String userId) { + PGPSignature signature = getUserIdCertification(userId); + return SignatureSubpacketsUtil.parseKeyFlags(signature); + } + + + + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java index 3a7d5856..1837131f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java @@ -62,6 +62,14 @@ public class SubkeyIdentifier { this.subkeyFingerprint = subkeyFingerprint; } + public @Nonnull OpenPgpV4Fingerprint getFingerprint() { + return getSubkeyFingerprint(); + } + + public long getKeyId() { + return getSubkeyId(); + } + /** * Return the {@link OpenPgpV4Fingerprint} of the primary key of the identified key. * This might be the same as {@link #getSubkeyFingerprint()} if the identified subkey is the primary key. @@ -72,6 +80,16 @@ public class SubkeyIdentifier { return primaryKeyFingerprint; } + /** + * Return the key id of the primary key of the identified key. + * This might be the same as {@link #getSubkeyId()} if the identified subkey is the primary key. + * + * @return primary key id + */ + public long getPrimaryKeyId() { + return getPrimaryKeyFingerprint().getKeyId(); + } + /** * Return the {@link OpenPgpV4Fingerprint} of the identified subkey. * @@ -81,6 +99,15 @@ public class SubkeyIdentifier { return subkeyFingerprint; } + /** + * Return the key id of the identified subkey. + * + * @return subkey id + */ + public long getSubkeyId() { + return getSubkeyFingerprint().getKeyId(); + } + @Override public int hashCode() { return primaryKeyFingerprint.hashCode() * 31 + subkeyFingerprint.hashCode(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 46ae9e4a..1bc3a50b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -20,12 +20,12 @@ import static org.pgpainless.util.CollectionUtils.iteratorToList; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; 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; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,9 +37,14 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.exception.KeyValidationException; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignaturePicker; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; @@ -52,13 +57,7 @@ public class KeyRingInfo { private static final Pattern PATTERN_EMAIL = Pattern.compile("[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"); private final PGPKeyRing keys; - - private final PGPSignature revocationSelfSignature; - private final PGPSignature mostRecentSelfSignature; - private final Map mostRecentUserIdSignatures = new ConcurrentHashMap<>(); - private final Map mostRecentUserIdRevocations = new ConcurrentHashMap<>(); - private final Map mostRecentSubkeyBindings = new ConcurrentHashMap<>(); - private final Map mostRecentSubkeyRevocations = new ConcurrentHashMap<>(); + private Signatures signatures; /** * Evaluate the key ring at creation time of the given signature. @@ -82,36 +81,7 @@ public class KeyRingInfo { public KeyRingInfo(PGPKeyRing keys, Date validationDate) { this.keys = keys; - - revocationSelfSignature = SignaturePicker.pickCurrentRevocationSelfSignature(keys, validationDate); - mostRecentSelfSignature = SignaturePicker.pickCurrentDirectKeySelfSignature(keys, validationDate); - - for (Iterator it = keys.getPublicKey().getUserIDs(); it.hasNext(); ) { - String userId = it.next(); - PGPSignature certification = SignaturePicker.pickCurrentUserIdCertificationSignature(keys, userId, validationDate); - if (certification != null) { - mostRecentUserIdSignatures.put(userId, certification); - } - PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keys, userId, validationDate); - if (revocation != null) { - mostRecentUserIdRevocations.put(userId, revocation); - } - } - - Iterator publicKeys = keys.getPublicKeys(); - publicKeys.next(); // Skip primary key - - while (publicKeys.hasNext()) { - PGPPublicKey subkey = publicKeys.next(); - PGPSignature bindingSig = SignaturePicker.pickCurrentSubkeyBindingSignature(keys, subkey, validationDate); - if (bindingSig != null) { - mostRecentSubkeyBindings.put(subkey.getKeyID(), bindingSig); - } - PGPSignature bindingRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keys, subkey, validationDate); - if (bindingRevocation != null) { - mostRecentSubkeyRevocations.put(subkey.getKeyID(), bindingRevocation); - } - } + this.signatures = new Signatures(keys, validationDate, PGPainless.getPolicy()); } /** @@ -142,11 +112,11 @@ public class KeyRingInfo { } if (publicKey == getPublicKey()) { - return revocationSelfSignature == null; + return signatures.primaryKeyRevocation == null; } - PGPSignature binding = mostRecentSubkeyBindings.get(keyId); - PGPSignature revocation = mostRecentSubkeyRevocations.get(keyId); + PGPSignature binding = signatures.subkeyBindings.get(keyId); + PGPSignature revocation = signatures.subkeyRevocations.get(keyId); return binding != null && revocation == null; } @@ -225,7 +195,7 @@ public class KeyRingInfo { String primaryUserId = null; Date modificationDate = null; for (String userId : getValidUserIds()) { - PGPSignature signature = mostRecentUserIdSignatures.get(userId); + PGPSignature signature = signatures.userIdCertifications.get(userId); PrimaryUserID subpacket = SignatureSubpacketsUtil.getPrimaryUserId(signature); if (subpacket != null && subpacket.isPrimaryUserID()) { // if there are multiple primary userIDs, return most recently signed @@ -235,6 +205,11 @@ public class KeyRingInfo { } } } + // Workaround for keys with only one user-id but no primary user-id packet. + if (primaryUserId == null) { + return getValidUserIds().get(0); + } + return primaryUserId; } @@ -261,8 +236,8 @@ public class KeyRingInfo { } public boolean isUserIdValid(String userId) { - PGPSignature certification = mostRecentUserIdSignatures.get(userId); - PGPSignature revocation = mostRecentUserIdRevocations.get(userId); + PGPSignature certification = signatures.userIdCertifications.get(userId); + PGPSignature revocation = signatures.userIdRevocations.get(userId); return certification != null && revocation == null; } @@ -285,34 +260,35 @@ public class KeyRingInfo { } public PGPSignature getCurrentDirectKeySelfSignature() { - return mostRecentSelfSignature; + return signatures.primaryKeySelfSignature; } public PGPSignature getRevocationSelfSignature() { - return revocationSelfSignature; + return signatures.primaryKeyRevocation; } public PGPSignature getCurrentUserIdCertification(String userId) { - return mostRecentUserIdSignatures.get(userId); + return signatures.userIdCertifications.get(userId); } public PGPSignature getUserIdRevocation(String userId) { - return mostRecentUserIdRevocations.get(userId); + return signatures.userIdRevocations.get(userId); } public PGPSignature getCurrentSubkeyBindingSignature(long keyId) { - return mostRecentSubkeyBindings.get(keyId); + return signatures.subkeyBindings.get(keyId); } public PGPSignature getSubkeyRevocationSignature(long keyId) { - return mostRecentSubkeyRevocations.get(keyId); + return signatures.subkeyRevocations.get(keyId); } public List getKeyFlagsOf(long keyId) { if (getPublicKey().getKeyID() == keyId) { - if (mostRecentSelfSignature != null) { - KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(mostRecentSelfSignature); + PGPSignature directKeySignature = getCurrentDirectKeySelfSignature(); + if (directKeySignature != null) { + KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature); if (flags != null) { return KeyFlag.fromBitmask(flags.getFlags()); } @@ -320,7 +296,15 @@ public class KeyRingInfo { String primaryUserId = getPrimaryUserId(); if (primaryUserId != null) { - KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(mostRecentUserIdSignatures.get(primaryUserId)); + KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(getCurrentUserIdCertification(primaryUserId)); + if (flags != null) { + return KeyFlag.fromBitmask(flags.getFlags()); + } + } + } else { + PGPSignature bindingSignature = getCurrentSubkeyBindingSignature(keyId); + if (bindingSignature != null) { + KeyFlags flags = SignatureSubpacketsUtil.getKeyFlags(bindingSignature); if (flags != null) { return KeyFlag.fromBitmask(flags.getFlags()); } @@ -334,7 +318,7 @@ public class KeyRingInfo { return Collections.emptyList(); } - PGPSignature userIdCertification = mostRecentUserIdSignatures.get(userId); + PGPSignature userIdCertification = getCurrentUserIdCertification(userId); if (userIdCertification == null) { return Collections.emptyList(); } @@ -377,12 +361,14 @@ public class KeyRingInfo { private PGPSignature getMostRecentSignature() { Set allSignatures = new HashSet<>(); + PGPSignature mostRecentSelfSignature = getCurrentDirectKeySelfSignature(); + PGPSignature revocationSelfSignature = getRevocationSelfSignature(); if (mostRecentSelfSignature != null) allSignatures.add(mostRecentSelfSignature); if (revocationSelfSignature != null) allSignatures.add(revocationSelfSignature); - allSignatures.addAll(mostRecentUserIdSignatures.values()); - allSignatures.addAll(mostRecentUserIdRevocations.values()); - allSignatures.addAll(mostRecentSubkeyBindings.values()); - allSignatures.addAll(mostRecentSubkeyRevocations.values()); + allSignatures.addAll(signatures.userIdCertifications.values()); + allSignatures.addAll(signatures.userIdRevocations.values()); + allSignatures.addAll(signatures.subkeyBindings.values()); + allSignatures.addAll(signatures.subkeyRevocations.values()); PGPSignature mostRecent = null; for (PGPSignature signature : allSignatures) { @@ -399,7 +385,7 @@ public class KeyRingInfo { * @return revocation date or null */ public Date getRevocationDate() { - return revocationSelfSignature == null ? null : revocationSelfSignature.getCreationTime(); + return getRevocationSelfSignature() == null ? null : getRevocationSelfSignature().getCreationTime(); } /** @@ -409,8 +395,8 @@ public class KeyRingInfo { */ public Date getPrimaryKeyExpirationDate() { Date lastExpiration = null; - if (mostRecentSelfSignature != null) { - lastExpiration = SignatureUtils.getKeyExpirationDate(getCreationDate(), mostRecentSelfSignature); + if (getCurrentDirectKeySelfSignature() != null) { + lastExpiration = SignatureUtils.getKeyExpirationDate(getCreationDate(), getCurrentDirectKeySelfSignature()); } for (String userId : getValidUserIds()) { @@ -432,7 +418,7 @@ public class KeyRingInfo { if (subkey == null) { throw new IllegalArgumentException("No subkey with fingerprint " + fingerprint + " found."); } - return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), mostRecentSubkeyBindings.get(fingerprint.getKeyId())); + return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), getCurrentSubkeyBindingSignature(fingerprint.getKeyId())); } /** @@ -489,4 +475,130 @@ public class KeyRingInfo { } return false; } + + public PGPPublicKey getEncryptionSubkey(EncryptionStream.Purpose purpose) { + Iterator subkeys = keys.getPublicKeys(); + while (subkeys.hasNext()) { + PGPPublicKey subKey = subkeys.next(); + + if (!isKeyValidlyBound(subKey.getKeyID())) { + continue; + } + + if (!subKey.isEncryptionKey()) { + continue; + } + + List keyFlags = getKeyFlagsOf(subKey.getKeyID()); + switch (purpose) { + case COMMUNICATIONS: + if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS)) { + return subKey; + } + break; + case STORAGE: + if (keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { + return subKey; + } + break; + case STORAGE_AND_COMMUNICATIONS: + if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { + return subKey; + } + break; + } + } + return null; + } + + public PGPPublicKey getEncryptionSubkey(String userId, EncryptionStream.Purpose purpose) { + if (userId != null) { + if (!isUserIdValid(userId)) { + throw new KeyValidationException(userId, getCurrentUserIdCertification(userId), getUserIdRevocation(userId)); + } + } + + return getEncryptionSubkey(purpose); + } + + public PGPPublicKey getSigningSubkey() { + Iterator subkeys = keys.getPublicKeys(); + while (subkeys.hasNext()) { + PGPPublicKey subKey = subkeys.next(); + + if (!isKeyValidlyBound(subKey.getKeyID())) { + continue; + } + + if (!subKey.isEncryptionKey()) { + continue; + } + + List keyFlags = getKeyFlagsOf(subKey.getKeyID()); + if (keyFlags.contains(KeyFlag.SIGN_DATA)) { + return subKey; + } + } + return null; + } + + public List getPreferredHashAlgorithms(String userId, long keyID) { + PGPSignature signature = getCurrentUserIdCertification(userId == null ? getPrimaryUserId() : userId); + if (signature == null) { + signature = getCurrentDirectKeySelfSignature(); + } + if (signature == null) { + signature = getCurrentSubkeyBindingSignature(keyID); + } + if (signature == null) { + throw new IllegalStateException("No valid signature."); + } + return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signature); + } + + public static class Signatures { + + private final PGPSignature primaryKeyRevocation; + private final PGPSignature primaryKeySelfSignature; + private final Map userIdRevocations; + private final Map userIdCertifications; + private final Map subkeyRevocations; + private final Map subkeyBindings; + + public Signatures(PGPKeyRing keyRing, Date evaluationDate, Policy policy) { + primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, evaluationDate); + primaryKeySelfSignature = SignaturePicker.pickCurrentDirectKeySelfSignature(keyRing, evaluationDate); + userIdRevocations = new HashMap<>(); + userIdCertifications = new HashMap<>(); + subkeyRevocations = new HashMap<>(); + subkeyBindings = new HashMap<>(); + + for (Iterator it = keyRing.getPublicKey().getUserIDs(); it.hasNext(); ) { + String userId = it.next(); + PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, evaluationDate); + if (revocation != null) { + userIdRevocations.put(userId, revocation); + } + PGPSignature certification = SignaturePicker.pickCurrentUserIdCertificationSignature(keyRing, userId, evaluationDate); + if (certification != null) { + userIdCertifications.put(userId, certification); + } + } + + Iterator keys = keyRing.getPublicKeys(); + keys.next(); // Skip primary key + while (keys.hasNext()) { + PGPPublicKey subkey = keys.next(); + PGPSignature subkeyRevocation = SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keyRing, subkey, evaluationDate); + if (subkeyRevocation != null) { + subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation); + } + PGPSignature subkeyBinding = SignaturePicker.pickCurrentSubkeyBindingSignature(keyRing, subkey, evaluationDate); + if (subkeyBinding != null) { + subkeyBindings.put(subkey.getKeyID(), subkeyBinding); + } + } + } + + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index 4ca63707..295bc7b3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.util.NotationRegistry; @@ -38,6 +39,8 @@ public final class Policy { SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyEncryptionAlgorithmPolicy(); private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy = SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy(); + private CompressionAlgorithmPolicy compressionAlgorithmPolicy = + CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy(); private final NotationRegistry notationRegistry = new NotationRegistry(); private Policy() { @@ -142,6 +145,17 @@ public final class Policy { this.symmetricKeyDecryptionAlgorithmPolicy = policy; } + public CompressionAlgorithmPolicy getCompressionAlgorithmPolicy() { + return compressionAlgorithmPolicy; + } + + public void setCompressionAlgorithmPolicy(CompressionAlgorithmPolicy policy) { + if (policy == null) { + throw new NullPointerException("Compression policy cannot be null."); + } + this.compressionAlgorithmPolicy = policy; + } + public static final class SymmetricKeyAlgorithmPolicy { private final SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm; @@ -297,6 +311,39 @@ public final class Policy { } } + public static final class CompressionAlgorithmPolicy { + + private final CompressionAlgorithm defaultCompressionAlgorithm; + private final List acceptableCompressionAlgorithms; + + public CompressionAlgorithmPolicy(CompressionAlgorithm defaultCompressionAlgorithm, + List acceptableCompressionAlgorithms) { + this.defaultCompressionAlgorithm = defaultCompressionAlgorithm; + this.acceptableCompressionAlgorithms = Collections.unmodifiableList(acceptableCompressionAlgorithms); + } + + public CompressionAlgorithm defaultCompressionAlgorithm() { + return defaultCompressionAlgorithm; + } + + public boolean isAcceptable(int compressionAlgorithmTag) { + return isAcceptable(CompressionAlgorithm.fromId(compressionAlgorithmTag)); + } + + public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) { + return acceptableCompressionAlgorithms.contains(compressionAlgorithm); + } + + public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() { + return new CompressionAlgorithmPolicy(CompressionAlgorithm.UNCOMPRESSED, Arrays.asList( + CompressionAlgorithm.UNCOMPRESSED, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZLIB + )); + } + } + /** * Return the {@link NotationRegistry} of PGPainless. * The notation registry is used to decide, whether or not a Notation is known or not. diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java index 0ccaed34..e8a3a207 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SelectSignatureFromKey.java @@ -59,7 +59,7 @@ public abstract class SelectSignatureFromKey { * Criterion that checks if the signature is valid at the validation date. * A signature is not valid if it was created after the validation date, or if it is expired at the validation date. * - * creationTime less than or equal validationDate less than expirationDate. + * creationTime ≤ validationDate < expirationDate. * * @param validationDate validation date * @return criterion implementation diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java index eb2c178d..b90702c5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignaturePicker.java @@ -25,8 +25,11 @@ import org.bouncycastle.bcpg.sig.SignatureCreationTime; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.policy.Policy; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; import org.pgpainless.util.CollectionUtils; @@ -53,36 +56,19 @@ public class SignaturePicker { * @return most recent, valid key revocation signature */ public static PGPSignature pickCurrentRevocationSelfSignature(PGPKeyRing keyRing, Date validationDate) { + Policy policy = PGPainless.getPolicy(); PGPPublicKey primaryKey = keyRing.getPublicKey(); List signatures = getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION); PGPSignature mostCurrentValidSig = null; for (PGPSignature signature : signatures) { - if (!SelectSignatureFromKey.isWellFormed().accept(signature, primaryKey, keyRing)) { - // Signature is not well-formed. Reject + try { + SignatureValidator.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate); + } catch (SignatureValidationException e) { + // Signature is not valid continue; } - - if (!SelectSignatureFromKey.isCreatedBy(keyRing.getPublicKey()).accept(signature, primaryKey, keyRing)) { - // Revocation signature was not created by primary key - continue; - } - - RevocationReason reason = SignatureSubpacketsUtil.getRevocationReason(signature); - if (reason != null && !RevocationAttributes.Reason.isHardRevocation(reason.getRevocationReason())) { - // reason code states soft revocation - if (!SelectSignatureFromKey.isValidAt(validationDate).accept(signature, primaryKey, keyRing)) { - // Soft revocation is either expired or not yet valid - continue; - } - } - - if (!SelectSignatureFromKey.isValidKeyRevocationSignature(primaryKey).accept(signature, primaryKey, keyRing)) { - // sig does not check out - continue; - } - mostCurrentValidSig = signature; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 8eb5393c..f8e2e893 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import javax.annotation.Nullable; + import org.bouncycastle.bcpg.sig.Exportable; import org.bouncycastle.bcpg.sig.Features; import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; @@ -44,7 +46,11 @@ import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.util.encoders.Hex; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SignatureSubpacket; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.signature.SignatureUtils; @@ -200,6 +206,17 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms); } + public static List parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { + List algorithms = new ArrayList<>(); + PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); + if (preferences != null) { + for (int code : preferences.getPreferences()) { + algorithms.add(SymmetricKeyAlgorithm.fromId(code)); + } + } + return algorithms; + } + /** * Return the hash algorithm preferences from the signatures hashed area. * @@ -210,6 +227,17 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredHashAlgorithms); } + public static List parsePreferredHashAlgorithms(PGPSignature signature) { + List algorithms = new ArrayList<>(); + PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); + if (preferences != null) { + for (int code : preferences.getPreferences()) { + algorithms.add(HashAlgorithm.fromId(code)); + } + } + return algorithms; + } + /** * Return the compression algorithm preferences from the signatures hashed area. * @@ -220,6 +248,17 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms); } + public static List parsePreferredCompressionAlgorithms(PGPSignature signature) { + List algorithms = new ArrayList<>(); + PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); + if (preferences != null) { + for (int code : preferences.getPreferences()) { + algorithms.add(CompressionAlgorithm.fromId(code)); + } + } + return algorithms; + } + /** * Return the primary user-id subpacket from the signatures hashed area. * @@ -240,6 +279,24 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.keyFlags); } + /** + * Return a list of key flags carried by the signature. + * If the signature is null, or has no {@link KeyFlags} subpacket, return null. + * + * @param signature signature + * @return list of key flags + */ + public static List parseKeyFlags(@Nullable PGPSignature signature) { + if (signature == null) { + return null; + } + KeyFlags keyFlags = getKeyFlags(signature); + if (keyFlags == null) { + return null; + } + return KeyFlag.fromBitmask(keyFlags.getFlags()); + } + /** * Return the features subpacket from the signatures hashed area. * diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java index c69008ea..43fb135b 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java @@ -17,18 +17,23 @@ package org.bouncycastle; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.Iterator; +import java.util.List; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.util.CollectionUtils; public class PGPPublicKeyRingTest { @@ -57,4 +62,21 @@ public class PGPPublicKeyRingTest { } } } + + @Test + public void removeUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + String userId = "alice@wonderland.lit"; + PGPSecretKeyRing secretKeyRing = PGPainless.generateKeyRing().simpleEcKeyRing(userId); + PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeyRing); + + List userIds = CollectionUtils.iteratorToList(publicKeys.getPublicKey().getUserIDs()); + assertTrue(userIds.contains(userId)); + + PGPPublicKey publicKey = publicKeys.getPublicKey(); + PGPSignature cert = publicKey.getSignaturesForID(userId).next(); + publicKey = PGPPublicKey.removeCertification(publicKey, cert); + + userIds = CollectionUtils.iteratorToList(publicKey.getUserIDs()); + assertFalse(userIds.contains(userId)); + } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index 666a652e..b2938501 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -41,12 +41,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; @@ -155,26 +156,20 @@ public class EncryptDecryptTest { EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(envelope) - .toRecipients(recipientPub) - .usingSecureAlgorithms() - .signWith(keyDecryptor, senderSec) - .signBinaryDocument() + .toRecipient(recipientPub) + .and() + .signInlineWith(keyDecryptor, senderSec, null, DocumentSignatureType.BINARY_DOCUMENT) .noArmor(); Streams.pipeAll(new ByteArrayInputStream(secretMessage), encryptor); encryptor.close(); byte[] encryptedSecretMessage = envelope.toByteArray(); - OpenPgpMetadata encryptionResult = encryptor.getResult(); + EncryptionResult encryptionResult = encryptor.getResult(); - assertFalse(encryptionResult.getSignatures().isEmpty()); - for (OpenPgpV4Fingerprint fingerprint : encryptionResult.getVerifiedSignatures().keySet()) { - assertTrue(BCUtil.keyRingContainsKeyWithId(senderPub, fingerprint.getKeyId())); - } - - assertFalse(encryptionResult.getRecipientKeyIds().isEmpty()); - for (long keyId : encryptionResult.getRecipientKeyIds()) { - assertTrue(BCUtil.keyRingContainsKeyWithId(recipientPub, keyId)); + assertFalse(encryptionResult.getRecipients().isEmpty()); + for (SubkeyIdentifier encryptionKey : encryptionResult.getRecipients()) { + assertTrue(BCUtil.keyRingContainsKeyWithId(recipientPub, encryptionKey.getKeyId())); } assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionResult.getSymmetricKeyAlgorithm()); @@ -214,15 +209,14 @@ public class EncryptDecryptTest { ByteArrayOutputStream dummyOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(dummyOut) .doNotEncrypt() - .createDetachedSignature() - .signWith(keyRingProtector, signingKeys) - .signBinaryDocument() + .signDetachedWith(keyRingProtector, signingKeys) .noArmor(); Streams.pipeAll(inputStream, signer); signer.close(); - OpenPgpMetadata metadata = signer.getResult(); - Set signatureSet = metadata.getSignatures(); + EncryptionResult metadata = signer.getResult(); + + Set signatureSet = metadata.getDetachedSignatures().get(metadata.getDetachedSignatures().keySet().iterator().next()); ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); signatureSet.iterator().next().encode(armorOut); @@ -244,8 +238,8 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, dummyOut); verifier.close(); - metadata = verifier.getResult(); - assertFalse(metadata.getVerifiedSignatures().isEmpty()); + OpenPgpMetadata decryptionResult = verifier.getResult(); + assertFalse(decryptionResult.getVerifiedSignatures().isEmpty()); } @ParameterizedTest @@ -259,8 +253,7 @@ public class EncryptDecryptTest { ByteArrayOutputStream signOut = new ByteArrayOutputStream(); EncryptionStream signer = PGPainless.encryptAndOrSign().onOutputStream(signOut) .doNotEncrypt() - .signWith(keyRingProtector, signingKeys) - .signBinaryDocument() + .signInlineWith(keyRingProtector, signingKeys) .asciiArmor(); Streams.pipeAll(inputStream, signer); signer.close(); @@ -344,6 +337,6 @@ public class EncryptDecryptTest { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); assertThrows(IllegalArgumentException.class, () -> PGPainless.encryptAndOrSign().onOutputStream(outputStream) - .toRecipients(publicKeys)); + .toRecipient(publicKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java index 7efec438..5dc960c5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionStreamClosedTest.java @@ -37,8 +37,8 @@ public class EncryptionStreamClosedTest { OutputStream out = new ByteArrayOutputStream(); EncryptionStream stream = PGPainless.encryptAndOrSign() .onOutputStream(out) - .forPassphrases(Passphrase.fromPassword("dummy")) - .usingSecureAlgorithms() + .forPassphrase(Passphrase.fromPassword("dummy")) + .and() .doNotSign() .asciiArmor(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java index 3e3306f5..97fdac81 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/FileInfoTest.java @@ -69,8 +69,8 @@ public class FileInfoTest { ByteArrayOutputStream dataOut = new ByteArrayOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() .onOutputStream(dataOut, fileInfo) - .toRecipients(publicKeys) - .usingSecureAlgorithms() + .toRecipient(publicKeys) + .and() .doNotSign() .noArmor(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/LengthTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/LengthTest.java index ccb9afda..63862e61 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/LengthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/LengthTest.java @@ -31,6 +31,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -111,11 +112,9 @@ public class LengthTest { OutputStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(envelope) - .toRecipients(recipientPub) - // .doNotEncrypt() - .usingSecureAlgorithms() - .signWith(keyDecryptor, senderSec) - .signBinaryDocument() + .toRecipient(recipientPub) + .and() + .signInlineWith(keyDecryptor, senderSec, "simplejid@server.tld", DocumentSignatureType.BINARY_DOCUMENT) .noArmor(); Streams.pipeAll(new ByteArrayInputStream(secretMessage), encryptor); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java index fde520ec..1398393c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/SigningTest.java @@ -37,7 +37,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.DecryptionStream; @@ -70,10 +70,11 @@ public class SigningTest { EncryptionStream encryptionStream = PGPainless.encryptAndOrSign(EncryptionStream.Purpose.STORAGE) .onOutputStream(out) .toRecipients(keys) - .andToSelf(KeyRingUtils.publicKeyRingFrom(cryptieKeys)) - .usingAlgorithms(SymmetricKeyAlgorithm.AES_192, HashAlgorithm.SHA384, CompressionAlgorithm.ZIP) - .signWith(SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), cryptieKeys) - .signCanonicalText() + .and() + .toRecipient(KeyRingUtils.publicKeyRingFrom(cryptieKeys)) + .and() + .signInlineWith(SecretKeyRingProtector.unlockSingleKeyWith(TestKeys.CRYPTIE_PASSPHRASE, cryptieSigningKey), + cryptieKeys, TestKeys.CRYPTIE_UID, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) .asciiArmor(); byte[] messageBytes = "This message is signed and encrypted to Romeo and Juliet.".getBytes(StandardCharsets.UTF_8); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java index 98cb8c49..8f6bafe1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSecretKeyRingPassphraseTest.java @@ -193,8 +193,7 @@ public class ChangeSecretKeyRingPassphraseTest { ByteArrayOutputStream dummy = new ByteArrayOutputStream(); EncryptionStream stream = PGPainless.encryptAndOrSign().onOutputStream(dummy) .doNotEncrypt() - .signWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing, passphrase), keyRing) - .signBinaryDocument() + .signInlineWith(PasswordBasedSecretKeyRingProtector.forKey(keyRing, passphrase), keyRing) .noArmor(); Streams.pipeAll(new ByteArrayInputStream(dummyMessage.getBytes()), stream); 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 index da903196..50c3662a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/MultiPassphraseSymmetricEncryptionTest.java @@ -36,7 +36,7 @@ public class MultiPassphraseSymmetricEncryptionTest { @ParameterizedTest @MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories") @Disabled - public void test(ImplementationFactory implementationFactory) throws IOException, PGPException { + public void encryptDecryptWithMultiplePassphrases(ImplementationFactory implementationFactory) throws IOException, PGPException { ImplementationFactory.setFactoryImplementation(implementationFactory); 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."; @@ -44,8 +44,10 @@ public class MultiPassphraseSymmetricEncryptionTest { ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); EncryptionStream encryptor = PGPainless.encryptAndOrSign() .onOutputStream(ciphertextOut) - .forPassphrases(Passphrase.fromPassword("p1"), Passphrase.fromPassword("p2")) - .usingSecureAlgorithms() + .forPassphrase(Passphrase.fromPassword("p1")) + .and() + .forPassphrase(Passphrase.fromPassword("p2")) + .and() .doNotSign() .noArmor(); 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 7d6e834b..93476202 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 @@ -30,6 +30,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.encryption_signing.EncryptionBuilderInterface; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.TestKeys; @@ -46,7 +47,7 @@ public class SymmetricEncryptionTest { @ParameterizedTest @MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories") - public void test(ImplementationFactory implementationFactory) throws IOException, PGPException { + public void encryptWithKeyAndPassphrase_DecryptWithKey(ImplementationFactory implementationFactory) throws IOException, PGPException { ImplementationFactory.setFactoryImplementation(implementationFactory); byte[] plaintext = "This is a secret message".getBytes(StandardCharsets.UTF_8); ByteArrayInputStream plaintextIn = new ByteArrayInputStream(plaintext); @@ -54,12 +55,13 @@ public class SymmetricEncryptionTest { Passphrase encryptionPassphrase = Passphrase.fromPassword("greenBeans"); ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); - EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) - .forPassphrases(encryptionPassphrase) + EncryptionBuilderInterface.Armor armor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) + .forPassphrase(encryptionPassphrase) .and() - .toRecipients(encryptionKey) - .usingSecureAlgorithms() - .doNotSign() + .toRecipient(encryptionKey) + .and() + .doNotSign(); + EncryptionStream encryptor = armor .noArmor(); Streams.pipeAll(plaintextIn, encryptor); diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java index e584ada6..a4e648dd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestEncryptCommsStorageFlagsDifferentiated.java @@ -56,6 +56,6 @@ public class TestEncryptCommsStorageFlagsDifferentiated { .onOutputStream(out); // since the key does not carry the flag ENCRYPT_COMMS, it cannot be used by the stream. - assertThrows(IllegalArgumentException.class, () -> builder.toRecipients(publicKeys)); + assertThrows(IllegalArgumentException.class, () -> builder.toRecipient(publicKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java index 0d978838..5d6dcd72 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java +++ b/pgpainless-core/src/test/java/org/pgpainless/weird_keys/TestTwoSubkeysEncryption.java @@ -28,7 +28,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.EncryptionResult; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.key.WeirdKeys; import org.pgpainless.key.util.KeyRingUtils; @@ -57,16 +57,16 @@ public class TestTwoSubkeysEncryption { ByteArrayOutputStream out = new ByteArrayOutputStream(); EncryptionStream encryptionStream = PGPainless.encryptAndOrSign(EncryptionStream.Purpose.STORAGE) .onOutputStream(out) - .toRecipients(publicKeys) - .usingSecureAlgorithms() + .toRecipient(publicKeys) + .and() .doNotSign() .noArmor(); Streams.pipeAll(getPlainIn(), encryptionStream); encryptionStream.close(); - OpenPgpMetadata metadata = encryptionStream.getResult(); + EncryptionResult metadata = encryptionStream.getResult(); - assertEquals(2, metadata.getRecipientKeyIds().size()); + assertEquals(2, metadata.getRecipients().size()); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index f49d17cb..db092dcd 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -22,17 +22,20 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; 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.PGPPublicKeyRingCollection; 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.DocumentSignatureType; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionBuilderInterface; @@ -40,6 +43,7 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.KeyRingProtectionSettings; import org.pgpainless.key.protection.CachingSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.Passphrase; import picocli.CommandLine; @@ -86,17 +90,26 @@ public class Encrypt implements Runnable { System.exit(19); } - PGPPublicKeyRing[] publicKeys = new PGPPublicKeyRing[certs.length]; + PGPPublicKeyRing[] pubKeysArray = 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; + pubKeysArray[i] = publicKey; } catch (IOException e) { err_ln("Cannot read certificate " + certs[i].getName()); err_ln(e.getMessage()); System.exit(1); } } + PGPPublicKeyRingCollection publicKeys; + try { + publicKeys = new PGPPublicKeyRingCollection(Arrays.asList(pubKeysArray)); + } catch (IOException | PGPException e) { + err_ln("Cannot construct public key collection."); + err_ln(e.getMessage()); + System.exit(1); + return; + } PGPSecretKeyRing[] secretKeys = new PGPSecretKeyRing[signWith.length]; for (int i = 0; i < signWith.length; i++) { try (FileInputStream fileIn = new FileInputStream(signWith[i])) { @@ -140,25 +153,32 @@ public class Encrypt implements Runnable { } } - EncryptionBuilderInterface.DetachedSign builder = PGPainless.encryptAndOrSign() + EncryptionBuilderInterface.ToRecipientsOrSign 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 CachingSecretKeyRingProtector(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(); + .and(); + for (Passphrase passphrase : passphraseArray) { + builder = builder.forPassphrase(passphrase).and(); } + EncryptionBuilderInterface.Armor builder_armor = null; + EncryptionBuilderInterface.SignWith builder1 = builder; try { + if (signWith.length != 0) { + for (int i = 0; i < signWith.length; i++) { + PGPSecretKeyRing secretKeyRing = secretKeys[i]; + EncryptionBuilderInterface.AdditionalSignWith additionalSignWith = builder1.signInlineWith( + SecretKeyRingProtector.unlockAllKeysWith( + passphraseMap.get(secretKeyRing.getPublicKey().getKeyID()), + secretKeyRing), + secretKeyRing, null, + type == Type.text || type == Type.mime ? + DocumentSignatureType.CANONICAL_TEXT_DOCUMENT : DocumentSignatureType.BINARY_DOCUMENT); + builder_armor = additionalSignWith; + builder1 = additionalSignWith.and(); + } + } else { + builder_armor = builder.doNotSign(); + } EncryptionStream encryptionStream = !armor ? builder_armor.noArmor() : builder_armor.asciiArmor(); Streams.pipeAll(System.in, encryptionStream);