diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java new file mode 100644 index 00000000..e4bcb4dc --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java @@ -0,0 +1,87 @@ +/* + * 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.negotiation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.policy.Policy; + +/** + * Interface for symmetric key algorithm negotiation. + */ +public interface SymmetricKeyAlgorithmNegotiator { + + /** + * Negotiate a symmetric encryption algorithm. + * + * @param policy algorithm policy + * @param override algorithm override (if not null, return this) + * @param keyPreferences list of preferences per key + * @return negotiated algorithm + */ + SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> keyPreferences); + + static SymmetricKeyAlgorithmNegotiator byPopularity() { + return new SymmetricKeyAlgorithmNegotiator() { + @Override + public SymmetricKeyAlgorithm negotiate(Policy.SymmetricKeyAlgorithmPolicy policy, SymmetricKeyAlgorithm override, List> preferences) { + if (override == SymmetricKeyAlgorithm.NULL) { + throw new IllegalArgumentException("Algorithm override cannot be NULL (plaintext)."); + } + + if (override != null) { + return override; + } + + Map supportWeight = new LinkedHashMap<>(); + + for (Set keyPreferences : preferences) { + for (SymmetricKeyAlgorithm preferred : keyPreferences) { + if (supportWeight.containsKey(preferred)) { + supportWeight.put(preferred, supportWeight.get(preferred) + 1); + } else { + supportWeight.put(preferred, 1); + } + } + } + + List scoreboard = new ArrayList<>(supportWeight.keySet()); + // Sort scoreboard by descending popularity + Collections.sort(scoreboard, new Comparator() { + @Override + public int compare(SymmetricKeyAlgorithm t0, SymmetricKeyAlgorithm t1) { + return -supportWeight.get(t0).compareTo(supportWeight.get(t1)); + } + }); + + for (SymmetricKeyAlgorithm mostWanted : scoreboard) { + if (policy.isAcceptable(mostWanted)) { + return mostWanted; + } + } + + return policy.getDefaultSymmetricKeyAlgorithm(); + } + }; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java new file mode 100644 index 00000000..b0ebdaef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +/** + * Classes related to algorithm negotiation. + */ +package org.pgpainless.algorithm.negotiation; 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 0afa8698..2bbbccb9 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 @@ -18,11 +18,8 @@ package org.pgpainless.encryption_signing; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; +import java.util.Set; import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; @@ -33,14 +30,12 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; 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.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.exception.KeyValidationException; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyView; import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.policy.Policy; import org.pgpainless.util.Passphrase; public class EncryptionBuilder implements EncryptionBuilderInterface { @@ -262,69 +257,22 @@ public class EncryptionBuilder implements EncryptionBuilderInterface { /** * 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; - } - - Map supportWeight = new LinkedHashMap<>(); - + List> preferences = new ArrayList<>(); for (SubkeyIdentifier key : encryptionOptions.getKeyViews().keySet()) { - KeyView keyView = encryptionOptions.getKeyViews().get(key); - for (SymmetricKeyAlgorithm preferred : keyView.getPreferredSymmetricKeyAlgorithms()) { - if (supportWeight.containsKey(preferred)) { - supportWeight.put(preferred, supportWeight.get(preferred) + 1); - } else { - supportWeight.put(preferred, 1); - } - } + preferences.add(encryptionOptions.getKeyViews().get(key).getPreferredSymmetricKeyAlgorithms()); } - List scoreboard = new ArrayList<>(supportWeight.keySet()); - // Sort scoreboard by descending popularity - Collections.sort(scoreboard, new Comparator() { - @Override - public int compare(SymmetricKeyAlgorithm t0, SymmetricKeyAlgorithm t1) { - return -supportWeight.get(t0).compareTo(supportWeight.get(t1)); - } - }); - - for (SymmetricKeyAlgorithm mostWanted : scoreboard) { - if (PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().isAcceptable(mostWanted)) { - return mostWanted; - } - } - - return PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy().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(); + return SymmetricKeyAlgorithmNegotiator + .byPopularity() + .negotiate( + PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy(), + encryptionOptions.getEncryptionAlgorithmOverride(), + preferences); } public static CompressionAlgorithm negotiateCompressionAlgorithm(ProducerOptions producerOptions) { 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 index 15a5e544..ab3baf8f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java @@ -32,7 +32,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.info.KeyView; +import org.pgpainless.key.info.KeyAccessor; import org.pgpainless.util.Passphrase; /** @@ -67,7 +67,7 @@ public class EncryptionOptions { private final Set encryptionMethods = new LinkedHashSet<>(); private final Set encryptionKeys = new LinkedHashSet<>(); private final Map keyRingInfo = new HashMap<>(); - private final Map keyViews = new HashMap<>(); + private final Map keyViews = new HashMap<>(); private final EncryptionKeySelector encryptionKeySelector = encryptToFirstSubkey(); private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; @@ -135,7 +135,7 @@ public class EncryptionOptions { for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyView.ViaUserId(info, keyId, userId)); + keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId)); addRecipientKey(key, encryptionSubkey); } @@ -169,7 +169,7 @@ public class EncryptionOptions { for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyView.ViaKeyId(info, keyId)); + keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId)); addRecipientKey(key, encryptionSubkey); } @@ -224,7 +224,7 @@ public class EncryptionOptions { return new HashSet<>(encryptionKeys); } - public Map getKeyViews() { + public Map getKeyViews() { return new HashMap<>(keyViews); } 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 732e1381..55e0f244 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 @@ -140,7 +140,7 @@ public final class EncryptionStream extends OutputStream { } private void prepareCompression() throws IOException { - CompressionAlgorithm compressionAlgorithm = options.getCompressionAlgorithmOverride(); + CompressionAlgorithm compressionAlgorithm = EncryptionBuilder.negotiateCompressionAlgorithm(options); resultBuilder.setCompressionAlgorithm(compressionAlgorithm); compressedDataGenerator = new PGPCompressedDataGenerator( compressionAlgorithm.getAlgorithmId()); 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 index 870388b6..4f5b0d1f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; @@ -28,6 +29,7 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; +import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.exception.KeyValidationException; @@ -35,9 +37,13 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.policy.Policy; public final class SigningOptions { + /** + * A method of signing. + */ public static final class SigningMethod { private final PGPSignatureGenerator signatureGenerator; private final boolean detached; @@ -47,10 +53,25 @@ public final class SigningOptions { this.detached = detached; } + /** + * Inline-signature method. + * The resulting signature will be written into the message itself, together with a one-pass-signature packet. + * + * @param signatureGenerator signature generator + * @return inline signing method + */ public static SigningMethod inlineSignature(PGPSignatureGenerator signatureGenerator) { return new SigningMethod(signatureGenerator, false); } + /** + * Detached signing method. + * The resulting signature will not be added to the message, and instead can be distributed separately + * to the signed message. + * + * @param signatureGenerator signature generator + * @return detached signing method + */ public static SigningMethod detachedSignature(PGPSignatureGenerator signatureGenerator) { return new SigningMethod(signatureGenerator, true); } @@ -64,20 +85,47 @@ public final class SigningOptions { } } - private Map signingMethods = new HashMap<>(); + private final Map signingMethods = new HashMap<>(); private HashAlgorithm hashAlgorithmOverride; - public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + /** + * Add an inline-signature. + * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use + * of one-pass-signature packets. + * + * @param secretKeyDecryptor decryptor to unlock the signing secret key + * @param secretKey signing key + * @param signatureType type of signature (binary, canonical text) + * @throws KeyValidationException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + * @return this + */ + public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + DocumentSignatureType signatureType) throws KeyValidationException, PGPException { - addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); + return addInlineSignature(secretKeyDecryptor, secretKey, null, signatureType); } - public void addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + /** + * Add an inline-signature. + * Inline signatures are being embedded into the message itself and can be processed in one pass, thanks to the use + * of one-pass-signature packets. + * + * This method uses the passed in user-id to select user-specific hash algorithms. + * + * @param secretKeyDecryptor decryptor to unlock the signing secret key + * @param secretKey signing key + * @param userId user-id of the signer + * @param signatureType signature type (binary, canonical text) + * @return this + * @throws KeyValidationException if the key is invalid + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + */ + public SigningOptions addInlineSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + String userId, + DocumentSignatureType signatureType) throws KeyValidationException, PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null) { @@ -94,22 +142,52 @@ public final class SigningOptions { for (PGPPublicKey signingPubKey : signingPubKeys) { 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); + Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID()); + HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); + addSigningMethod(secretKey, signingSubkey, hashAlgorithm, signatureType, false); } + + return this; } - public void addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - DocumentSignatureType signatureType) + /** + * Create a detached signature. + * Detached signatures are not being added into the PGP message itself. + * Instead they can be distributed separately to the message. + * Detached signatures are useful if the data that is being signed shall not be modified (eg. when signing a file). + * + * @param secretKeyDecryptor decryptor to unlock the secret signing key + * @param secretKey signing key + * @param signatureType type of data that is signed (binary, canonical text) + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + * @return this + */ + public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + DocumentSignatureType signatureType) throws PGPException { - addDetachedSignature(secretKeyDecryptor, secretKey, null, signatureType); + return addDetachedSignature(secretKeyDecryptor, secretKey, null, signatureType); } - public void addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, - PGPSecretKeyRing secretKey, - String userId, - DocumentSignatureType signatureType) + /** + * Create a detached signature. + * Detached signatures are not being added into the PGP message itself. + * Instead they can be distributed separately to the message. + * Detached signatures are useful if the data that is being signed shall not be modified (eg. when signing a file). + * + * This method uses the passed in user-id to select user-specific hash algorithms. + * + * @param secretKeyDecryptor decryptor to unlock the secret signing key + * @param secretKey signing key + * @param userId user-id + * @param signatureType type of data that is signed (binary, canonical text) + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + * @return this + */ + public SigningOptions addDetachedSignature(SecretKeyRingProtector secretKeyDecryptor, + PGPSecretKeyRing secretKey, + String userId, + DocumentSignatureType signatureType) throws PGPException { KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); if (userId != null) { @@ -126,9 +204,12 @@ public final class SigningOptions { for (PGPPublicKey signingPubKey : signingPubKeys) { 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, true); + Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(userId, signingPubKey.getKeyID()); + HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); + addSigningMethod(secretKey, signingSubkey, hashAlgorithm, signatureType, true); } + + return this; } private void addSigningMethod(PGPSecretKeyRing secretKey, @@ -143,6 +224,37 @@ public final class SigningOptions { signingMethods.put(signingKeyIdentifier, signingMethod); } + /** + * Negotiate, which hash algorithm to use. + * + * This method gives highest priority to the algorithm override, which can be set via {@link #overrideHashAlgorithm(HashAlgorithm)}. + * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. + * Lastly, should no acceptable algorithm be found, the {@link Policy Policies} default signature hash algorithm is + * used as a fallback. + * + * @param preferences preferences + * @param policy policy + * @return selected hash algorithm + */ + private HashAlgorithm negotiateHashAlgorithm(Set preferences, Policy policy) { + if (hashAlgorithmOverride != null) { + return hashAlgorithmOverride; + } + + HashAlgorithm algorithm = policy.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); + if (preferences.isEmpty()) { + return algorithm; + } + + for (HashAlgorithm pref : preferences) { + if (policy.getSignatureHashAlgorithmPolicy().isAcceptable(pref)) { + return pref; + } + } + + return algorithm; + } + private PGPSignatureGenerator createSignatureGenerator(PGPPrivateKey privateKey, HashAlgorithm hashAlgorithm, DocumentSignatureType signatureType) @@ -156,15 +268,37 @@ public final class SigningOptions { return signatureGenerator; } + /** + * Return a map of key-ids and signing methods. + * For internal use. + * + * @return signing methods + */ public Map getSigningMethods() { return Collections.unmodifiableMap(signingMethods); } + /** + * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. + * If no override has been set, an accetable algorithm will be negotiated instead. + * + * Note: To override the hash algorithm for signing, call this method *before* calling + * {@link #addInlineSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)} or + * {@link #addDetachedSignature(SecretKeyRingProtector, PGPSecretKeyRing, DocumentSignatureType)}. + * + * @param hashAlgorithmOverride override hash algorithm + * @return this + */ public SigningOptions overrideHashAlgorithm(HashAlgorithm hashAlgorithmOverride) { this.hashAlgorithmOverride = hashAlgorithmOverride; return this; } + /** + * Return the hash algorithm override (or null if no override is set). + * + * @return hash algorithm override + */ public HashAlgorithm getHashAlgorithmOverride() { return hashAlgorithmOverride; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java new file mode 100644 index 00000000..0529688d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.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.key.info; + +import java.util.Set; + +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +public abstract class KeyAccessor { + + protected final KeyRingInfo info; + protected final SubkeyIdentifier key; + + public KeyAccessor(KeyRingInfo info, SubkeyIdentifier key) { + this.info = info; + this.key = key; + } + + /** + * Depending on the way we address the key (key-id or user-id), return the respective {@link PGPSignature} + * which contains the algorithm preferences we are going to use. + * + * If we address a key via its user-id, we want to rely on the algorithm preferences in the user-id certification, + * while we would instead rely on those in the direct-key signature if we'd address the key by key-id. + * + * @return signature + */ + public abstract @Nonnull PGPSignature getSignatureWithPreferences(); + + /** + * Return preferred symmetric key encryption algorithms. + * + * @return preferred symmetric algorithms + */ + public Set getPreferredSymmetricKeyAlgorithms() { + return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); + } + + /** + * Return preferred hash algorithms. + * + * @return preferred hash algorithms + */ + public Set getPreferredHashAlgorithms() { + return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); + } + + /** + * Return preferred compression algorithms. + * + * @return preferred compression algorithms + */ + public Set getPreferredCompressionAlgorithms() { + return SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); + } + + /** + * Address the key via a user-id (eg "Alice <alice@wonderland.lit>). + * In this case we are sourcing preferred algorithms from the user-id certification first. + */ + public static class ViaUserId extends KeyAccessor { + + private final String userId; + + /** + * Access a key via user-id. + * + * @param info info about a key at a given date + * @param key id of the subkey + * @param userId user-id + */ + public ViaUserId(KeyRingInfo info, SubkeyIdentifier key, String userId) { + super(info, key); + this.userId = userId; + } + + @Override + public @Nonnull PGPSignature getSignatureWithPreferences() { + PGPSignature signature = info.getLatestUserIdCertification(userId); + if (signature != null) { + return signature; + } + throw new IllegalStateException("No valid user-id certification signature found for '" + userId + "'."); + } + } + + /** + * Address the key via key-id. + * In this case we are sourcing preferred algorithms from the keys direct-key signature first. + */ + public static class ViaKeyId extends KeyAccessor { + + /** + * Address the key via key-id. + * @param info info about the key at a given date + * @param key key-id + */ + public ViaKeyId(KeyRingInfo info, SubkeyIdentifier key) { + super(info, key); + } + + @Override + public @Nonnull PGPSignature getSignatureWithPreferences() { + PGPSignature signature = info.getLatestDirectKeySelfSignature(); + if (signature != null) { + return signature; + } + + signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); + if (signature == null) { + throw new IllegalStateException("No valid signature found."); + } + return signature; + } + } +} 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 fa874157..e425815f 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 @@ -43,6 +43,7 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.exception.KeyValidationException; import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.policy.Policy; import org.pgpainless.signature.SignaturePicker; import org.pgpainless.signature.SignatureUtils; @@ -633,18 +634,10 @@ public class KeyRingInfo { return signingKeys; } - public List getPreferredHashAlgorithms(String userId, long keyID) { - PGPSignature signature = getLatestUserIdCertification(userId == null ? getPrimaryUserId() : userId); - if (signature == null) { - signature = getLatestDirectKeySelfSignature(); - } - if (signature == null) { - signature = getCurrentSubkeyBindingSignature(keyID); - } - if (signature == null) { - throw new IllegalStateException("No valid signature."); - } - return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signature); + public Set getPreferredHashAlgorithms(String userId, long keyID) { + KeyAccessor keyAccessor = userId == null ? new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)) + : new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId); + return keyAccessor.getPreferredHashAlgorithms(); } public static class Signatures { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyView.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyView.java deleted file mode 100644 index 5a23e61d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyView.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.key.info; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; - -public abstract class KeyView { - - protected final KeyRingInfo info; - protected final SubkeyIdentifier key; - - public KeyView(KeyRingInfo info, SubkeyIdentifier key) { - this.info = info; - this.key = key; - } - - public abstract PGPSignature getSignatureWithPreferences(); - - public List getPreferredSymmetricKeyAlgorithms() { - List algos = SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); - return new ArrayList<>(new LinkedHashSet<>(algos)); // remove duplicates - } - - public List getPreferredHashAlgorithms() { - List algos = SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); - return new ArrayList<>(new LinkedHashSet<>(algos)); // remove duplicates - } - - public List getPreferredCompressionAlgorithms() { - List algos = SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); - return new ArrayList<>(new LinkedHashSet<>(algos)); // remove duplicates - } - - public static class ViaUserId extends KeyView { - - private final String userId; - - public ViaUserId(KeyRingInfo info, SubkeyIdentifier key, String userId) { - super(info, key); - this.userId = userId; - } - - @Override - public PGPSignature getSignatureWithPreferences() { - return info.getLatestUserIdCertification(userId); - } - } - - public static class ViaKeyId extends KeyView { - - public ViaKeyId(KeyRingInfo info, SubkeyIdentifier key) { - super(info, key); - } - - @Override - public PGPSignature getSignatureWithPreferences() { - PGPSignature signature = info.getLatestDirectKeySelfSignature(); - if (signature != null) { - return signature; - } - - return info.getLatestUserIdCertification(info.getPrimaryUserId()); - } - } -} 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 f8e2e893..b74f0c28 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 @@ -18,7 +18,9 @@ package org.pgpainless.signature.subpackets; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; @@ -206,8 +208,8 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms); } - public static List parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { - List algorithms = new ArrayList<>(); + public static Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { + Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) { @@ -227,8 +229,8 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredHashAlgorithms); } - public static List parsePreferredHashAlgorithms(PGPSignature signature) { - List algorithms = new ArrayList<>(); + public static Set parsePreferredHashAlgorithms(PGPSignature signature) { + Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) { @@ -248,8 +250,8 @@ public class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms); } - public static List parsePreferredCompressionAlgorithms(PGPSignature signature) { - List algorithms = new ArrayList<>(); + public static Set parsePreferredCompressionAlgorithms(PGPSignature signature) { + Set algorithms = new LinkedHashSet<>(); PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); if (preferences != null) { for (int code : preferences.getPreferences()) {