From 0fa62991ec7a65f4f3d391a4060dce50b9fed764 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 16:25:31 +0200 Subject: [PATCH 001/351] PGPainless 1.6.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 8dc16ee7..4e996794 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.6.0' - isSnapshot = false + shortVersion = '1.6.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.75' From f46790be00dfc482e940ea7d4e8a970a3e2cb13a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 16:49:38 +0200 Subject: [PATCH 002/351] Require UTF8 for KeyRingBuilder.addUserId(byte[]) --- .../java/org/pgpainless/key/generation/KeyRingBuilder.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index a5263391..c9dfdb5c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -6,7 +6,6 @@ package org.pgpainless.key.generation; import java.io.IOException; -import java.nio.charset.Charset; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -36,6 +35,7 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.util.Strings; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; @@ -56,9 +56,6 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { private static final long YEAR_IN_SECONDS = 1000L * 60 * 60 * 24 * 365; - @SuppressWarnings("CharsetObjectCanBeUsed") - private final Charset UTF8 = Charset.forName("UTF-8"); - private KeySpec primaryKeySpec; private final List subkeySpecs = new ArrayList<>(); private final Map userIds = new LinkedHashMap<>(); @@ -95,7 +92,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { @Override public KeyRingBuilder addUserId(@Nonnull byte[] userId) { - return addUserId(new String(userId, UTF8)); + return addUserId(Strings.fromUTF8ByteArray(userId)); } @Override From 59fa51bdf3a09f1e5608f059400a8fac44a0d6ef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 19 Jul 2023 11:47:53 +0200 Subject: [PATCH 003/351] Expose SignatureValidator methods --- .../org/pgpainless/signature/consumer/SignatureValidator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 27f99499..8ab31b2f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -182,7 +182,7 @@ public abstract class SignatureValidator { * @param signingKey signing key * @return validator */ - private static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, + public static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, PGPPublicKey signingKey) { return new SignatureValidator() { @Override @@ -207,7 +207,7 @@ public abstract class SignatureValidator { * @param policy policy * @return validator */ - private static SignatureValidator signatureUsesAcceptableHashAlgorithm(Policy policy) { + public static SignatureValidator signatureUsesAcceptableHashAlgorithm(Policy policy) { return new SignatureValidator() { @Override public void verify(PGPSignature signature) throws SignatureValidationException { From 22b4b93be84e3a2f857a33bbbdee01ce17f4ffd6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 19 Jul 2023 12:43:23 +0200 Subject: [PATCH 004/351] Replace jetbrains annotations package with jsr305 --- pgpainless-sop/build.gradle | 3 ++- .../org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 3a911a86..bb4ddaca 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -14,7 +14,6 @@ repositories { } dependencies { - implementation 'org.jetbrains:annotations:20.1.0' testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" @@ -27,6 +26,8 @@ dependencies { implementation(project(":pgpainless-core")) api "org.pgpainless:sop-java:$sopJavaVersion" + + implementation "com.google.code.findbugs:jsr305:3.0.2" } test { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java index 0b88cb5d..5badf1f1 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -12,7 +12,6 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.jetbrains.annotations.Nullable; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.protection.CachingSecretKeyRingProtector; @@ -20,6 +19,8 @@ import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.util.Passphrase; +import javax.annotation.Nullable; + /** * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, * and which then matches up passphrases and keys when needed. From 9d93c0f5ae58155a5dc3040f1a1a41e618761818 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 16:25:29 +0200 Subject: [PATCH 005/351] Add CertificateAuthority interface to enable integration with pgpainless-wot --- .../CertificateAuthenticity.java | 139 ++++++++++++++++++ .../authentication/CertificateAuthority.java | 33 +++++ .../authentication/package-info.java | 8 + 3 files changed, 180 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java new file mode 100644 index 00000000..ac059646 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.authentication; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPPublicKeyRing; + +public class CertificateAuthenticity { + + private final String userId; + private final PGPPublicKeyRing certificate; + private final Map certificationChains = new HashMap<>(); + private final int targetAmount; + + public CertificateAuthenticity(@Nonnull PGPPublicKeyRing certificate, + @Nonnull String userId, + @Nonnull Map certificationChains, + int targetAmount) { + this.userId = userId; + this.certificate = certificate; + this.certificationChains.putAll(certificationChains); + this.targetAmount = targetAmount; + } + + @Nonnull + public String getUserId() { + return userId; + } + + @Nonnull + public PGPPublicKeyRing getCertificate() { + return certificate; + } + + public int getTotalTrustAmount() { + int total = 0; + for (int v : certificationChains.values()) { + total += v; + } + return total; + } + + /** + * Return the degree of authentication of the binding in percent. + * 100% means full authentication. + * Values smaller than 100% mean partial authentication. + * + * @return authenticity in percent + */ + public int getAuthenticityPercentage() { + return targetAmount * 100 / getTotalTrustAmount(); + } + + /** + * Return true, if the binding is authenticated to a sufficient degree. + * + * @return true if total gathered evidence outweighs the target trust amount. + */ + public boolean isAuthenticated() { + return targetAmount <= getTotalTrustAmount(); + } + + /** + * Return a map of {@link CertificationChain CertificationChains} and their respective effective trust amount. + * The effective trust amount of a path might be smaller than its actual trust amount, for example if nodes of a + * path are used multiple times. + * + * @return map of certification chains and their effective trust amounts + */ + @Nonnull + public Map getCertificationChains() { + return Collections.unmodifiableMap(certificationChains); + } + + public static class CertificationChain { + private final int trustAmount; + private final List chainLinks = new ArrayList<>(); + + /** + * A chain of certifications. + * + * @param trustAmount actual trust amount of the chain + * @param chainLinks links of the chain, starting at the trust-root, ending at the target. + */ + public CertificationChain(int trustAmount, @Nonnull List chainLinks) { + this.trustAmount = trustAmount; + this.chainLinks.addAll(chainLinks); + } + + /** + * Actual trust amount of the certification chain. + * @return trust amount + */ + public int getTrustAmount() { + return trustAmount; + } + + /** + * Return all links in the chain, starting at the trust-root and ending at the target. + * @return chain links + */ + @Nonnull + public List getChainLinks() { + return Collections.unmodifiableList(chainLinks); + } + } + + /** + * A chain link contains a node in the trust chain. + */ + public static class ChainLink { + private final PGPPublicKeyRing certificate; + + /** + * Create a chain link. + * @param certificate node in the trust chain + */ + public ChainLink(@Nonnull PGPPublicKeyRing certificate) { + this.certificate = certificate; + } + + /** + * Return the certificate that belongs to the node. + * @return certificate + */ + @Nonnull + public PGPPublicKeyRing getCertificate() { + return certificate; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java new file mode 100644 index 00000000..7f48c9c6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.authentication; + +import org.pgpainless.key.OpenPgpFingerprint; + +import javax.annotation.Nonnull; +import java.util.Date; + +public interface CertificateAuthority { + + /** + * Determine the authenticity of the binding between the given fingerprint and the userId. + * In other words, determine, how much evidence can be gathered, that the certificate with the given + * fingerprint really belongs to the user with the given userId. + * + * @param fingerprint fingerprint of the certificate + * @param userId userId + * @param email if true, the userId will be treated as an email address and all user-IDs containing + * the email address will be matched. + * @param referenceTime reference time at which the binding shall be evaluated + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return information about the authenticity of the binding + */ + CertificateAuthenticity authenticate(@Nonnull OpenPgpFingerprint fingerprint, + @Nonnull String userId, + boolean email, + @Nonnull Date referenceTime, + int targetAmount); +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java new file mode 100644 index 00000000..495ab1f7 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Classes and interfaces related to certificate authenticity. + */ +package org.pgpainless.authentication; From bf9bf94fb0bd4ec88bdee7157765cfc826dc1f1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 16:38:34 +0200 Subject: [PATCH 006/351] Integrate WoT by adding EncryptionOptions.addAuthenticatableRecipients() method --- .../authentication/CertificateAuthority.java | 17 +++++++++++ .../encryption_signing/EncryptionOptions.java | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java index 7f48c9c6..5c7f60cd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -8,6 +8,7 @@ import org.pgpainless.key.OpenPgpFingerprint; import javax.annotation.Nonnull; import java.util.Date; +import java.util.List; public interface CertificateAuthority { @@ -30,4 +31,20 @@ public interface CertificateAuthority { boolean email, @Nonnull Date referenceTime, int targetAmount); + + /** + * Identify certificates, which carry a trustworthy binding to the given userId. + * + * @param userId userId + * @param email if true, the user-ID will be treated as an email address and all user-IDs containing + * the email address will be matched. + * @param referenceTime reference time at which the binding shall be evaluated + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return list of identified bindings + */ + List identify(@Nonnull String userId, + boolean email, + @Nonnull Date referenceTime, + int targetAmount); } 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 248a8ce4..44122bdc 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 @@ -23,6 +23,8 @@ import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.authentication.CertificateAuthenticity; +import org.pgpainless.authentication.CertificateAuthority; import org.pgpainless.exception.KeyException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpFingerprint; @@ -113,6 +115,32 @@ public class EncryptionOptions { return new EncryptionOptions(EncryptionPurpose.STORAGE); } + /** + * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for + * identifiable bindings. + * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. + * @param userId userId + * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address + * @param authority certificate authority + * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return encryption options + */ + public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { + List identifiedCertificates = authority.identify(userId, email, new Date(), targetAmount); + boolean foundAcceptable = false; + for (CertificateAuthenticity candidate : identifiedCertificates) { + if (candidate.isAuthenticated()) { + addRecipient(candidate.getCertificate()); + foundAcceptable = true; + } + } + if (!foundAcceptable) { + throw new IllegalArgumentException("Could not identify any trust-worthy certificates for '" + userId + "' and target trust amount " + targetAmount); + } + return this; + } + /** * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. * From 8926ff9dfb74d62d7cec56ca1a75bb1a8b9ae90c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 16:50:49 +0200 Subject: [PATCH 007/351] s/identify/lookup --- .../authentication/CertificateAuthority.java | 10 +++++----- .../encryption_signing/EncryptionOptions.java | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java index 5c7f60cd..01b1a6bc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -33,7 +33,7 @@ public interface CertificateAuthority { int targetAmount); /** - * Identify certificates, which carry a trustworthy binding to the given userId. + * Lookup certificates, which carry a trustworthy binding to the given userId. * * @param userId userId * @param email if true, the user-ID will be treated as an email address and all user-IDs containing @@ -43,8 +43,8 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return list of identified bindings */ - List identify(@Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); + List lookup(@Nonnull String userId, + boolean email, + @Nonnull Date referenceTime, + int targetAmount); } 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 44122bdc..cbb6bbd9 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 @@ -127,7 +127,7 @@ public class EncryptionOptions { * @return encryption options */ public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.identify(userId, email, new Date(), targetAmount); + List identifiedCertificates = authority.lookup(userId, email, new Date(), targetAmount); boolean foundAcceptable = false; for (CertificateAuthenticity candidate : identifiedCertificates) { if (candidate.isAuthenticated()) { From ccbf4ab84d70d6f3174eafa0d2975c0484c83e57 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 16:55:21 +0200 Subject: [PATCH 008/351] Add documentation to CertificateAuthority --- .../pgpainless/authentication/CertificateAuthority.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java index 01b1a6bc..d124902d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -10,6 +10,13 @@ import javax.annotation.Nonnull; import java.util.Date; import java.util.List; +/** + * Interface for a CA that can authenticate trust-worthy certificates. + * Such a CA might be a fixed list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust. + * + * @see PGPainless-WOT + * @see OpenPGP Web of Trust + */ public interface CertificateAuthority { /** From c26ddc116ecb24569db92ed16922503b7ab41f5f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 17:08:48 +0200 Subject: [PATCH 009/351] Add identify API endpoint --- .../authentication/CertificateAuthority.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java index d124902d..468de022 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -54,4 +54,18 @@ public interface CertificateAuthority { boolean email, @Nonnull Date referenceTime, int targetAmount); + + /** + * Identify trustworthy bindings for a certificate. + * The result is a list of authenticatable userIds on the certificate. + * + * @param fingerprint fingerprint of the certificate + * @param referenceTime reference time for trust calculations + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return list of identified bindings + */ + List identify(@Nonnull OpenPgpFingerprint fingerprint, + @Nonnull Date referenceTime, + int targetAmount); } From 44690d063cf048c4e2bc79529022ec8de70a269f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 17:11:56 +0200 Subject: [PATCH 010/351] Rename CertificateAuthority methods --- .../authentication/CertificateAuthority.java | 24 +++++++++---------- .../encryption_signing/EncryptionOptions.java | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java index 468de022..36bf9e5f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java @@ -33,11 +33,11 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return information about the authenticity of the binding */ - CertificateAuthenticity authenticate(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); + CertificateAuthenticity authenticateBinding(@Nonnull OpenPgpFingerprint fingerprint, + @Nonnull String userId, + boolean email, + @Nonnull Date referenceTime, + int targetAmount); /** * Lookup certificates, which carry a trustworthy binding to the given userId. @@ -50,10 +50,10 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return list of identified bindings */ - List lookup(@Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); + List lookupByUserId(@Nonnull String userId, + boolean email, + @Nonnull Date referenceTime, + int targetAmount); /** * Identify trustworthy bindings for a certificate. @@ -65,7 +65,7 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return list of identified bindings */ - List identify(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull Date referenceTime, - int targetAmount); + List identifyByFingerprint(@Nonnull OpenPgpFingerprint fingerprint, + @Nonnull Date referenceTime, + int targetAmount); } 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 cbb6bbd9..b3293142 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 @@ -127,7 +127,7 @@ public class EncryptionOptions { * @return encryption options */ public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.lookup(userId, email, new Date(), targetAmount); + List identifiedCertificates = authority.lookupByUserId(userId, email, new Date(), targetAmount); boolean foundAcceptable = false; for (CertificateAuthenticity candidate : identifiedCertificates) { if (candidate.isAuthenticated()) { From 6ac019a42088eb71291f484da9727767ac49b13c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jul 2023 17:30:11 +0200 Subject: [PATCH 011/351] Add isAuthenticatablySignedBy() to MessageMetadata --- .../MessageMetadata.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 146c165d..4b55f268 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -18,6 +18,8 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.authentication.CertificateAuthenticity; +import org.pgpainless.authentication.CertificateAuthority; import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; @@ -92,6 +94,39 @@ public class MessageMetadata { return false; } + /** + * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * + * @param userId userId + * @param email if true, treat the user-id as an email address and match all userIDs containing this address + * @param certificateAuthority certificate authority + * @return true, if we can authenticate a binding for a signing key with sufficient evidence + */ + public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority) { + return isAuthenticatablySignedBy(userId, email, certificateAuthority, 120); + } + + /** + * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * + * @param userId userId + * @param email if true, treat the user-id as an email address and match all userIDs containing this address + * @param certificateAuthority certificate authority + * @param targetAmount target trust amount + * @return true, if we can authenticate a binding for a signing key with sufficient evidence + */ + public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority, int targetAmount) { + for (SignatureVerification verification : getVerifiedSignatures()) { + CertificateAuthenticity authenticity = certificateAuthority.authenticateBinding( + verification.getSigningKey().getFingerprint(), userId, email, + verification.getSignature().getCreationTime(), targetAmount); + if (authenticity.isAuthenticated()) { + return true; + } + } + return false; + } + /** * Return a list containing all recipient keyIDs. * From 616820fe0fe79efdc9eaf060802f2bd2465749cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Jul 2023 00:30:39 +0200 Subject: [PATCH 012/351] Update ecosystem diagram --- docs/source/ecosystem.md | 7 +++++++ docs/source/ecosystem_dia.md | 7 +++++++ docs/source/ecosystem_dia.png | Bin 110743 -> 112151 bytes docs/source/ecosystem_dia.svg | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/source/ecosystem.md b/docs/source/ecosystem.md index cd88cfd8..e5294b9b 100644 --- a/docs/source/ecosystem.md +++ b/docs/source/ecosystem.md @@ -47,6 +47,13 @@ The diagram below shows, how the different projects relate to one another. * `pgpainless-cert-d` - PGPainless-based implementation of `pgp-cert-d-java` * `pgpainless-cert-d-cli` - CLI frontend for `pgpainless-cert-d` +* {{ '[PGPainless-WOT](https://{}/pgpainless/pgpainless-wot)'.format(repo_host) }} + Implementation of the [OpenPGP Web of Trust specification](https://sequoia-pgp.gitlab.io/sequoia-wot/) using PGPainless. + * `pgpainless-wot` - Parse OpenPGP keyrings into a generic `Network` object + * `wot-dijkstra` - Perform queries to find paths inside a `Network` object + * `pgpainless-wot-cli` - CLI frontend for `pgpainless-wot` and `wot-dijkstra` + * `wot-test-suite` - Test vectors ported from [Sequoia-PGPs WoT implementation](https://gitlab.com/sequoia-pgp/sequoia-wot/-/tree/main/tests/data) + * {{ '[PGPeasy](https://{}/pgpainless/pgpeasy)'.format(repo_host) }} Prototypical, comprehensive OpenPGP CLI application * `pgpeasy` - CLI application \ No newline at end of file diff --git a/docs/source/ecosystem_dia.md b/docs/source/ecosystem_dia.md index 6469faaa..bc4bd190 100644 --- a/docs/source/ecosystem_dia.md +++ b/docs/source/ecosystem_dia.md @@ -27,6 +27,13 @@ flowchart LR subgraph VKS-JAVA vks-java-cli-->vks-java end + subgraph PGPAINLESS-WOT + wot-test-suite-->pgpainless-wot + pgpainless-wot-->wot-dijkstra + pgpainless-wot-cli-->pgpainless-wot + pgpainless-wot-->pgpainless-core + pgpainless-wot-cli-->pgpainless-cert-d + end subgraph PGPEASY pgpeasy-->pgpainless-cli pgpeasy-->wkd-java-cli diff --git a/docs/source/ecosystem_dia.png b/docs/source/ecosystem_dia.png index 70872a9f97b0918aef1108c8cb78a4bae603d44a..0efa16a8b58cd4054ab930e0bed3f2f956a07bc6 100644 GIT binary patch literal 112151 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

rzKr4n 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 z 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 J^4n%l{udJv(Qp6& literal 110743 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

Aq#)kEz)VC2N< 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%0A&-k7dr4E!zH$VqYQ(9QmRLwNn% z=YW;gC5Rcfc^qqAX)En+6504L*^7&bL8O^!%+zRW0;qVP*DeM=J$3Rjrn6?GcBp*i z`e@C{bX#L@>Gzt%K`n&#r?K6pHaQ)T>@1l0j+dXZzYY@uI;~LlTF>Z8tb^T~zwR5J zEq9m@k#@GM*}s23D@<=^0^)pF|8+jBmkl1&2KnWv;Ctp4N{zdIEiHmgxw5<_oE3b! zEAh7kIlNxc5Jh`D6PtuX(>CSLXOoGL`y<6Is6`wSCPCSjZU6%-8TPmtDSamf#~b7o z_tcu~c`N+F+nx^|Vnt8u&7r{wnbw!0xZ*taSqjLVIBk1`7!DW z3d#CYQa+mz%hMwEwd&uR1WVJp$7k-sx92h7!(V)WV!{{R)FIvJt?9`~{jK$dYO_GE zvB^l~ui+J>eySw{LL49Ipyp#qAS^Saw z8uBgf&lk5_!=?F)S1}ljgRepI=eML?zvE}D(~&C2i*c)iU#%r zdM3%SOXSp0dY`92A6Uz#s8P*=Cs}BME@>TF-KQUBp3nR1uRv*!p82wWglZ;@l)^0P|7u%g&=fQ>9< z!*~`0+p0$NWgphF{XavDM(wb}cQzKAdIw4ta>OjgA;ZNUVkxR^wdGh?DU9EV(w)8_DJcn}vawcn#xOTU+o`L>_MBF+b)R7s z78TYp+)sCf43Br^PT2;}`|OE$a^rKCuG$}eGAp_Y@oX6MY%OoM$)AIaG<>%0V5{4; zo!nBRu0%OM$aRaVxZdygAE>%mweikrVzin1Yotl~gpbm@1)2lP*|vzA{KSnXYLlx^ z>?$H7Os%NjUQ%Q!J44UT%PsVaVP)KW>2_Ti_``7thZHtV5K;q;G2jfsjz!RH&32)l zn%V-A$I`5VvgbzVs=u=fZJAFcY3KDe{Q$_ay@N)}hf%YCg3U2NBC4pNVhA;CB-Q`g zWb6JMP%{!F?v}4faNJy~S23?gedp0NL%jOs6=M#@Js#xGh$Q#yUH{)4YEMJ;-YB{q&i;U|-j)#5P-lO5vr1_=ZeCOdA`qwkrIr5UojWQH=Jo!V{mO9(sIn_H zPnuQSD4$N6Uqty=BE(IBjjewKKJuV$^v2bUZ?5yp4%6=WC{v^6$NxUs{|fm-Zv8*V zCI7G8Zf}(elV)ZD3LpJ4{te<2ir(8?Bwm#0z8gyiP@TGs{|r~wEQ!h-f_-Pqd~cbZ jB=+xY|9AgA+qS#U<#NV=jF`_Y

VKS-JAVA
CERT-D-PGPAINLESS
CERT-D-JAVA
WKD-JAVA
PGPAINLESS
SOP-JAVA
pgpeasy
vks-java
vks-java-cli
pgpainless-cert-d
pgpainless-cert-d-cli
pgp-certificate-store
pgp-cert-d-java
pgp-cert-d-java-jdbc-sqlite-lookup
wkd-java
wkd-java-cli
wkd-test-suite
pgpainless-core
pgpainless-sop
pgpainless-cli
sop-java
sop-java-picocli
\ No newline at end of file +
PGPEASY
PGPAINLESS-WOT
VKS-JAVA
CERT-D-PGPAINLESS
CERT-D-JAVA
WKD-JAVA
PGPAINLESS
SOP-JAVA
pgpeasy
pgpainless-wot
wot-test-suite
wot-dijkstra
pgpainless-wot-cli
vks-java
vks-java-cli
pgpainless-cert-d
pgpainless-cert-d-cli
pgp-certificate-store
pgp-cert-d-java
pgp-cert-d-java-jdbc-sqlite-lookup
wkd-java
wkd-java-cli
wkd-test-suite
pgpainless-core
pgpainless-sop
pgpainless-cli
sop-java
sop-java-picocli
\ No newline at end of file From 28e4bc44a1e738f6285a610856fb88214fe01880 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Jul 2023 00:30:52 +0200 Subject: [PATCH 013/351] Further integration of pgpainless-wot --- .../decryption_verification/MessageMetadata.java | 2 +- .../encryption_signing/EncryptionOptions.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 4b55f268..041a5437 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -107,7 +107,7 @@ public class MessageMetadata { } /** - * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * Return true, if the message was verifiably signed by a certificate for which we can authenticate a binding to the given userId. * * @param userId userId * @param email if true, treat the user-id as an email address and match all userIDs containing this address 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 b3293142..9d2aacee 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 @@ -115,6 +115,19 @@ public class EncryptionOptions { return new EncryptionOptions(EncryptionPurpose.STORAGE); } + /** + * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for + * identifiable bindings. + * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. + * @param userId userId + * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address + * @param authority certificate authority + * @return encryption options + */ + public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority) { + return addAuthenticatableRecipients(userId, email, authority, 120); + } + /** * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for * identifiable bindings. From b2ea55a67de7df296ba6353f55257ba197ce566c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Jul 2023 15:13:01 +0200 Subject: [PATCH 014/351] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 837e94ee..7bb48b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +# 1.6.1-SNAPSHOT +- `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` +- `pgpainless-sop`: Remove dependency on jetbrains annotations +- Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot) + - Add `EncryptionOptions.addAuthenticatableRecipients()` method + - Add `MessageMetadata.isAuthenticatablySignedBy()` method + ## 1.6.0 - Bump `sop-java` to `7.0.0`, implementing [SOP Spec Revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html) - Implement `revoke-key` subcommand and API From 17681568998721a15c1a5414140ff9d5afef52d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Jul 2023 15:19:17 +0200 Subject: [PATCH 015/351] PGPainless 1.6.1 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bb48b30..96a4ed46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -# 1.6.1-SNAPSHOT +# 1.6.1 - `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` - `pgpainless-sop`: Remove dependency on jetbrains annotations - Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot) diff --git a/README.md b/README.md index 5de1d761..31301cf8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.6.0' + implementation 'org.pgpainless:pgpainless-core:1.6.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 6bb1f738..ed96511c 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.6.0" + implementation "org.pgpainless:pgpainless-sop:1.6.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.6.0 + 1.6.1 ... diff --git a/version.gradle b/version.gradle index 4e996794..2d3e439b 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.6.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.75' From 789fa507d150484aa56126aca51f14cc4f3a649b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Jul 2023 15:21:10 +0200 Subject: [PATCH 016/351] PGPainless 1.6.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 2d3e439b..2e0c2c23 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.6.1' - isSnapshot = false + shortVersion = '1.6.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.75' From 8bc2338463c6595b53d747e0a40da5879d82012a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Aug 2023 14:27:49 +0200 Subject: [PATCH 017/351] Bump BC to 1.76 --- pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java | 3 ++- version.gradle | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index 59515200..a2876a6e 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -48,7 +48,8 @@ class KeyReader { try { certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); } catch (IOException e) { - if (e.getMessage() != null && e.getMessage().startsWith("unknown object in stream:")) { + String msg = e.getMessage(); + if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) { throw new SOPGPException.BadData(e); } throw e; diff --git a/version.gradle b/version.gradle index 2e0c2c23..532b0a3f 100644 --- a/version.gradle +++ b/version.gradle @@ -8,7 +8,7 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.75' + bouncyCastleVersion = '1.76' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.2.11' From 15af265e3f8d05742767200b87ac913ec989c90a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Aug 2023 14:31:09 +0200 Subject: [PATCH 018/351] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a4ed46..bfc8ab6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +# 1.6.2-SNAPSHOT +- Bump `bcpg-jdk15to18` to `1.76` +- Bump `bcprov-jdk15to18` to `1.76` + # 1.6.1 - `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` - `pgpainless-sop`: Remove dependency on jetbrains annotations From 8cdb7ee4e06327943b5d48b2bdcf0d1bf76ecc93 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Aug 2023 15:29:24 +0200 Subject: [PATCH 019/351] Add more tests for V6 fingerprints --- .../pgpainless/key/OpenPgpFingerprint.java | 12 +++ .../key/OpenPgpV6FingerprintTest.java | 73 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java index 1ac900a0..13804061 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java @@ -10,6 +10,7 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPKeyRing; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.encoders.Hex; @@ -22,6 +23,17 @@ public abstract class OpenPgpFingerprint implements CharSequence, Comparable keys = cert.getPublicKeys(); + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", + fingerprint.toString()); + + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885", + fingerprint.toString()); + } + + @Test + public void fromSampleV6SecretKey() throws IOException { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "xUsGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laMAGXKB\n" + + "exK+cH6NX1hs5hNhIB00TrJmosgv3mg1ditlsLfCsQYfGwoAAABCBYJjh3/jAwsJ\n" + + "BwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lwgyU2kCcUmKfvBXbAf6rh\n" + + "RYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaEQsiPlR4zxP/TP7mhfVEe\n" + + "7XWPxtnMUMtf15OyA51YBMdLBmOHf+MZAAAAIIaTJINn+eUBXbki+PSAld2nhJh/\n" + + "LVmFsS+60WyvXkQ1AE1gCk95TUR3XFeibg/u/tVY6a//1q0NWC1X+yui3O24wpsG\n" + + "GBsKAAAALAWCY4d/4wKbDCIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6\n" + + "2azJAAAAAAQBIKbpGG2dWTX8j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDE\n" + + "M0g12vYxoWM8Y81W+bHBw805I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUr\n" + + "k0mXubZvyl4GBg==\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + assertNotNull(secretKeys); + + OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(secretKeys); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", fingerprint.toString()); + + Iterator keys = secretKeys.getSecretKeys(); + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("CB186C4F0609A697E4D52DFA6C722B0C1F1E27C18A56708F6525EC27BAD9ACC9", + fingerprint.toString()); + + fingerprint = OpenPgpFingerprint.of(keys.next()); + assertEquals(6, fingerprint.getVersion()); + assertEquals("12C83F1E706F6308FE151A417743A1F033790E93E9978488D1DB378DA9930885", + fingerprint.toString()); + } + private PGPPublicKey getMockedPublicKey(String hex) { byte[] binary = Hex.decode(hex); From e167fa37f34e924712456859856400c0c4775393 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Aug 2023 16:53:55 +0200 Subject: [PATCH 020/351] Make use of new ArmoredOutputStream.Builder --- .../encryption_signing/EncryptionStream.java | 15 -------- .../util/ArmoredOutputStreamFactory.java | 37 +++++++++++-------- 2 files changed, 22 insertions(+), 30 deletions(-) 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 d2a16e08..7af5a7b3 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 @@ -30,7 +30,6 @@ import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.ArmoredOutputStreamFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,20 +90,6 @@ public final class EncryptionStream extends OutputStream { LOGGER.debug("Wrap encryption output in ASCII armor"); armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options); - if (options.hasComment()) { - String[] commentLines = options.getComment().split("\n"); - for (String commentLine : commentLines) { - if (!commentLine.trim().isEmpty()) { - ArmorUtils.addCommentHeader(armorOutputStream, commentLine.trim()); - } - } - } - if (options.hasVersion()) { - String version = options.getVersion().trim(); - if (!version.isEmpty()) { - ArmorUtils.setVersionHeader(armorOutputStream, version); - } - } outermostStream = armorOutputStream; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java index fc3a0bea..269f8674 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java @@ -29,6 +29,18 @@ public final class ArmoredOutputStreamFactory { } + private static ArmoredOutputStream.Builder getBuilder() { + ArmoredOutputStream.Builder builder = ArmoredOutputStream.builder(); + builder.clearHeaders(); + if (version != null && !version.isEmpty()) { + builder.setVersion(version); + } + for (String comment : comment) { + builder.addComment(comment); + } + return builder; + } + /** * Wrap an {@link OutputStream} inside a preconfigured {@link ArmoredOutputStream}. * @@ -37,16 +49,7 @@ public final class ArmoredOutputStreamFactory { */ @Nonnull public static ArmoredOutputStream get(@Nonnull OutputStream outputStream) { - ArmoredOutputStream armoredOutputStream = new ArmoredOutputStream(outputStream); - armoredOutputStream.clearHeaders(); - if (version != null && !version.isEmpty()) { - armoredOutputStream.setHeader(ArmorUtils.HEADER_VERSION, version); - } - - for (String comment : comment) { - ArmorUtils.addCommentHeader(armoredOutputStream, comment); - } - return armoredOutputStream; + return getBuilder().build(outputStream); } /** @@ -58,13 +61,17 @@ public final class ArmoredOutputStreamFactory { */ @Nonnull public static ArmoredOutputStream get(@Nonnull OutputStream outputStream, @Nonnull ProducerOptions options) { + ArmoredOutputStream.Builder builder = getBuilder(); if (options.isHideArmorHeaders()) { - ArmoredOutputStream armorOut = new ArmoredOutputStream(outputStream); - armorOut.clearHeaders(); - return armorOut; - } else { - return get(outputStream); + builder.clearHeaders(); } + if (options.hasVersion()) { + builder.setVersion(options.getVersion()); + } + if (options.hasComment()) { + builder.setComment(options.getComment()); + } + return builder.build(outputStream); } /** From 1df6dcce131320f369af29243fe82c268cf65d69 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Aug 2023 14:46:35 +0200 Subject: [PATCH 021/351] Update sop quickstart document --- docs/source/pgpainless-sop/quickstart.md | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 60e1df29..04417816 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -114,6 +114,56 @@ To disable ASCII armoring, call `noArmor()` before calling `key(_)`. In our example, `certificateBytes` can now safely be shared with anyone. +### Change Key Password + +OpenPGP keys can (but don't need to) be password protected. +The `changeKeyPassword()` API can be used to add, change or remove password protection from OpenPGP keys. +While the input to this operation can be keys with different per-subkey passwords, the output will use at most one password. + +Using `oldKeyPassphrase()` multiple decryption passphrase candidates can be provided. +These are tried one after another to unlock protected subkeys. + +In order to successfully change the passphrase of an OpenPGP key, the all subkeys needs to be decrypted. +If one or more subkeys cannot be decrypted, the operation fails with a `KeyIsProtected` exception. +The result is either fully encrypted for a single passphrase (passed via `newKeyPassphrase()`), +or unprotected if the new key passphrase is omitted. + + +```java +byte[] keyBefore = ... +byte[] keyAfter = sop.changeKeyPassword() + // Provide old passphrases - all subkeys need to be decryptable, + // otherwise KeyIsProtected exception will be thrown + .oldKeyPassphrase("4d4m5m1th") + .oldKeyPassphrase("d4v1dR1c4rd0") + // Provide the new passphrase - if omitted, key will be unprotected + .newKeyPassphrase("fr1edr1ch3n93l5") + .keys(keyBefore) + .getBytes(); +``` + +### Generate Revocation Certificates + +You might want to generate a revocation certificate for your OpenPGP key. +This certificate can be published to a key server to let your contacts known that your key is no longer +trustworthy. +The `revokeKey()` API can be used to generate a "hard-revocation", which retroactively invalidates all +signatures previously issued by the key. + +If the input secret key is an OpenPGP v6 key, the result will be a minimal revocation certificate, +consisting of only the bare primary public key and a revocation signature. For v4 keys, the result +will consist of the whole public certificate plus a revocation signature. + +```java +byte[] keys = ... +byte[] revoked = sop.revokeKey() + // primary key password(s) if the key(s) are protected + .withKeyPassword("5w0rdf1sh") + // one or more secret keys + .keys(keys) + .getBytes(); +``` + ### Apply / Remove ASCII Armor Perhaps you want to print your secret key onto a piece of paper for backup purposes, From 0d8db24b1a43ed647e844e88261c531518bbbc96 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 2 Aug 2023 16:02:01 +0200 Subject: [PATCH 022/351] Fix typo --- docs/source/pgpainless-sop/quickstart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 04417816..8b8281ac 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -120,10 +120,10 @@ OpenPGP keys can (but don't need to) be password protected. The `changeKeyPassword()` API can be used to add, change or remove password protection from OpenPGP keys. While the input to this operation can be keys with different per-subkey passwords, the output will use at most one password. -Using `oldKeyPassphrase()` multiple decryption passphrase candidates can be provided. +Via `oldKeyPassphrase()`, multiple decryption passphrase candidates can be provided. These are tried one after another to unlock protected subkeys. -In order to successfully change the passphrase of an OpenPGP key, the all subkeys needs to be decrypted. +In order to successfully change the passphrase of an OpenPGP key, all of its subkeys needs to be successfully decrypted. If one or more subkeys cannot be decrypted, the operation fails with a `KeyIsProtected` exception. The result is either fully encrypted for a single passphrase (passed via `newKeyPassphrase()`), or unprotected if the new key passphrase is omitted. From 975d59c5a93f5d79819e0604eb32663545196339 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 14:04:40 +0200 Subject: [PATCH 023/351] Add method to allow for encryption for keys with missing keyflags. There are legacy keys around, which do not carry any key flags. This commit adds a method to EncryptionOptions that allow PGPainless to encrypt for such keys. Fixes #400 --- .../encryption_signing/EncryptionOptions.java | 34 +++- .../key/generation/KeyRingBuilder.java | 2 +- .../EncryptionWithMissingKeyFlagsTest.java | 187 ++++++++++++++++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java 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 9d2aacee..e2e8cf5a 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 @@ -67,6 +67,7 @@ public class EncryptionOptions { private final Map keyRingInfo = new HashMap<>(); private final Map keyViews = new HashMap<>(); private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys(); + private boolean allowEncryptionWithMissingKeyFlags = false; private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; @@ -277,8 +278,7 @@ public class EncryptionOptions { private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) { Date evaluationDate = new Date(); - KeyRingInfo info; - info = new KeyRingInfo(key, evaluationDate); + KeyRingInfo info = new KeyRingInfo(key, evaluationDate); Date primaryKeyExpiration; try { @@ -292,6 +292,23 @@ public class EncryptionOptions { List encryptionSubkeys = encryptionKeySelectionStrategy .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); + + // There are some legacy keys around without key flags. + // If we allow encryption for those keys, we add valid keys without any key flags, if they are + // capable of encryption by means of their algorithm + if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { + List validSubkeys = info.getValidSubkeys(); + for (PGPPublicKey validSubkey : validSubkeys) { + if (!validSubkey.isEncryptionKey()) { + continue; + } + // only add encryption keys with no key flags. + if (info.getKeyFlagsOf(validSubkey.getKeyID()).isEmpty()) { + encryptionSubkeys.add(validSubkey); + } + } + } + if (encryptionSubkeys.isEmpty()) { throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); } @@ -386,6 +403,19 @@ public class EncryptionOptions { return this; } + /** + * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption + * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. + * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm + * type to convey the subkeys use. + * + * @return this + */ + public EncryptionOptions setAllowEncryptionWithMissingKeyFlags() { + this.allowEncryptionWithMissingKeyFlags = true; + return this; + } + /** * Return
true
iff the user specified at least one encryption method, *
false
otherwise. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index c9dfdb5c..208c17b6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -60,7 +60,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { private final List subkeySpecs = new ArrayList<>(); private final Map userIds = new LinkedHashMap<>(); private Passphrase passphrase = Passphrase.emptyPassphrase(); - private Date expirationDate = new Date(System.currentTimeMillis() + YEAR_IN_SECONDS * 5); // Expiration in 5 yeras + private Date expirationDate = new Date(System.currentTimeMillis() + YEAR_IN_SECONDS * 5); // Expiration in 5 years @Override public KeyRingBuilder setPrimaryKey(@Nonnull KeySpec keySpec) { diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java new file mode 100644 index 00000000..3fe8178b --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +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.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.exception.KeyException; + +public class EncryptionWithMissingKeyFlagsTest { + + private static final String KEY_WITHOUT_KEY_FLAGS = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: A991 A732 9021 7D2D 1250 93C3 2503 6033 1D81 E264\n" + + "Comment: Test \n" + + "\n" + + "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\n" + + "lh0k0LBQrX/YNegabaT+3gffzz7/\n" + + "=TKhx\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MESSAGE = "Hello, World!\n"; + + @Test + public void testEncryptionForKeyWithoutKeyFlagsFailsByDefault() + throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing() + .secretKeyRing(KEY_WITHOUT_KEY_FLAGS); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> + EncryptionOptions.get().addRecipient(publicKeys)); + } + + + @Test + public void testEncryptionForKeyWithoutKeyFlagsSucceedsWithActiveWorkaround() + throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing() + .secretKeyRing(KEY_WITHOUT_KEY_FLAGS); + PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); + + // Prepare encryption + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encOut = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .setAllowEncryptionWithMissingKeyFlags() // Workaround + .addRecipient(publicKeys))); + + // Encrypt + encOut.write(MESSAGE.getBytes(StandardCharsets.UTF_8)); + encOut.close(); + + // Prepare decryption + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(in) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys)); + ByteArrayOutputStream plain = new ByteArrayOutputStream(); + + // Decrypt + Streams.pipeAll(decryptionStream, plain); + decryptionStream.close(); + + // Check result + assertEquals(MESSAGE, plain.toString()); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncryptedFor(publicKeys)); + } +} From d08bc6bd4bec4765df96f747f63be4ddf5c1b8a1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 14:12:58 +0200 Subject: [PATCH 024/351] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc8ab6d..080af075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ SPDX-License-Identifier: CC0-1.0 # 1.6.2-SNAPSHOT - Bump `bcpg-jdk15to18` to `1.76` - Bump `bcprov-jdk15to18` to `1.76` +- Add `EncryptionOptions.setAllowEncryptionWithMissingKeyFlags()` to properly allow + encrypting to legacy keys which do not carry any key flags. # 1.6.1 - `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` From f0e59ecef5da6c50487b2568daef9c37832369dc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 14:48:57 +0200 Subject: [PATCH 025/351] EncryptionOptions: Allow overriding evaluation date for recipient keys --- .../encryption_signing/EncryptionOptions.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 e2e8cf5a..bb937a1a 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 @@ -68,6 +68,7 @@ public class EncryptionOptions { private final Map keyViews = new HashMap<>(); private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys(); private boolean allowEncryptionWithMissingKeyFlags = false; + private Date evaluationDate = new Date(); private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; @@ -96,6 +97,17 @@ public class EncryptionOptions { return new EncryptionOptions(); } + /** + * Override the evaluation date for recipient keys with the given date. + * + * @param evaluationDate new evaluation date + * @return this + */ + public EncryptionOptions setEvaluationDate(@Nonnull Date evaluationDate) { + this.evaluationDate = evaluationDate; + return this; + } + /** * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. @@ -141,7 +153,7 @@ public class EncryptionOptions { * @return encryption options */ public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.lookupByUserId(userId, email, new Date(), targetAmount); + List identifiedCertificates = authority.lookupByUserId(userId, email, evaluationDate, targetAmount); boolean foundAcceptable = false; for (CertificateAuthenticity candidate : identifiedCertificates) { if (candidate.isAuthenticated()) { @@ -213,7 +225,7 @@ public class EncryptionOptions { public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId, @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, new Date()); + KeyRingInfo info = new KeyRingInfo(key, evaluationDate); List encryptionSubkeys = encryptionKeySelectionStrategy .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose)); @@ -277,7 +289,6 @@ public class EncryptionOptions { } private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) { - Date evaluationDate = new Date(); KeyRingInfo info = new KeyRingInfo(key, evaluationDate); Date primaryKeyExpiration; From 16a4836f8a3e7964e444f909954384411b775d3c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 14:51:43 +0200 Subject: [PATCH 026/351] Override evaluation date in test with expiring key --- .../EncryptionWithMissingKeyFlagsTest.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java index 3fe8178b..5fd4a674 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionWithMissingKeyFlagsTest.java @@ -12,6 +12,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Date; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -23,6 +24,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.KeyException; +import org.pgpainless.util.DateUtil; public class EncryptionWithMissingKeyFlagsTest { @@ -135,6 +137,8 @@ public class EncryptionWithMissingKeyFlagsTest { "=TKhx\n" + "-----END PGP PRIVATE KEY BLOCK-----"; + // Above key expires in 5 years, so we fix the evaluation date to a known-good value + private static final Date evaluationDate = DateUtil.parseUTCDate("2023-08-03 12:50:06 UTC"); private static final String MESSAGE = "Hello, World!\n"; @Test @@ -145,7 +149,9 @@ public class EncryptionWithMissingKeyFlagsTest { PGPPublicKeyRing publicKeys = PGPainless.extractCertificate(secretKeys); assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> - EncryptionOptions.get().addRecipient(publicKeys)); + EncryptionOptions.get() + .setEvaluationDate(evaluationDate) + .addRecipient(publicKeys)); } @@ -161,6 +167,7 @@ public class EncryptionWithMissingKeyFlagsTest { EncryptionStream encOut = PGPainless.encryptAndOrSign() .onOutputStream(out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .setEvaluationDate(evaluationDate) .setAllowEncryptionWithMissingKeyFlags() // Workaround .addRecipient(publicKeys))); From db7e1ce9423f53f2de5bb6fa353159ab60fee154 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 14:57:31 +0200 Subject: [PATCH 027/351] Allow overriding evluation date in SigningOptions --- .../encryption_signing/SigningOptions.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 589e8d92..34d4bbcf 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 @@ -97,12 +97,24 @@ public final class SigningOptions { private final Map signingMethods = new HashMap<>(); private HashAlgorithm hashAlgorithmOverride; + private Date evaluationDate = new Date(); @Nonnull public static SigningOptions get() { return new SigningOptions(); } + /** + * Override the evaluation date for signing keys with the given date. + * + * @param evaluationDate new evaluation date + * @return this + */ + public SigningOptions setEvaluationDate(@Nonnull Date evaluationDate) { + this.evaluationDate = evaluationDate; + return this; + } + /** * Sign the message using an inline signature made by the provided signing key. * @@ -212,7 +224,7 @@ public final class SigningOptions { @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws KeyException, PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), @@ -280,7 +292,7 @@ public final class SigningOptions { long keyId, @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); List signingPubKeys = keyRingInfo.getSigningSubkeys(); if (signingPubKeys.isEmpty()) { @@ -418,7 +430,7 @@ public final class SigningOptions { @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketCallback) throws PGPException { - KeyRingInfo keyRingInfo = new KeyRingInfo(secretKey, new Date()); + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw new KeyException.UnboundUserIdException( OpenPgpFingerprint.of(secretKey), @@ -485,7 +497,7 @@ public final class SigningOptions { long keyId, @Nonnull DocumentSignatureType signatureType, @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey); + KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); List signingPubKeys = keyRingInfo.getSigningSubkeys(); if (signingPubKeys.isEmpty()) { From 1c3cc19ff7f56a4087805fbe79800d03c240b115 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Aug 2023 15:20:24 +0200 Subject: [PATCH 028/351] Add documentation to OpenPgpInputStream --- .../pgpainless/decryption_verification/OpenPgpInputStream.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index 028fa666..ff020a7b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -45,6 +45,9 @@ import org.pgpainless.algorithm.SignatureType; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +/** + * InputStream used to determine the nature of potential OpenPGP data. + */ public class OpenPgpInputStream extends BufferedInputStream { @SuppressWarnings("CharsetObjectCanBeUsed") From 29165f3df4b8b7552e592897b493460adf34e863 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 29 Aug 2023 16:57:05 +0200 Subject: [PATCH 029/351] Switch bcpg and bcprov artifacts from -jdk15to18 variant to -jdk18on --- pgpainless-core/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 3c73121f..830e573d 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -20,8 +20,8 @@ dependencies { testImplementation "ch.qos.logback:logback-classic:$logbackVersion" // Bouncy Castle - api "org.bouncycastle:bcprov-jdk15to18:$bouncyCastleVersion" - api "org.bouncycastle:bcpg-jdk15to18:$bouncyPgVersion" + api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion" + api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) // @Nullable, @Nonnull annotations From 3eb1c609910a28df04ed0f8cdaa5f265bf888b04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 29 Aug 2023 16:59:43 +0200 Subject: [PATCH 030/351] Update changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 080af075..9ff12cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog # 1.6.2-SNAPSHOT -- Bump `bcpg-jdk15to18` to `1.76` -- Bump `bcprov-jdk15to18` to `1.76` +- Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on` +- Bump `bcpg-jdk8on` to `1.76` +- Bump `bcprov-jdk18on` to `1.76` - Add `EncryptionOptions.setAllowEncryptionWithMissingKeyFlags()` to properly allow encrypting to legacy keys which do not carry any key flags. +- Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`. # 1.6.1 - `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` From ea7e5e8022922bb245add4eec1a6b6eca4d0296e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 12:14:20 +0200 Subject: [PATCH 031/351] PGPainless 1.6.2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff12cd5..4f86b67f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -# 1.6.2-SNAPSHOT +# 1.6.2 - Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on` - Bump `bcpg-jdk8on` to `1.76` - Bump `bcprov-jdk18on` to `1.76` diff --git a/README.md b/README.md index 31301cf8..659ca896 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.6.1' + implementation 'org.pgpainless:pgpainless-core:1.6.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ed96511c..55cf0cb2 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.6.1" + implementation "org.pgpainless:pgpainless-sop:1.6.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.6.1 + 1.6.2 ... diff --git a/version.gradle b/version.gradle index 532b0a3f..89a7d03d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.6.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.76' From 32b4a49102399e80fedbf42952569b3c447346fe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 12:19:13 +0200 Subject: [PATCH 032/351] PGPainless 1.6.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 89a7d03d..73c5547c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.6.2' - isSnapshot = false + shortVersion = '1.6.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.76' From 682042f563b277e20846bebff25b7dbe94f1c93f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 12:23:11 +0200 Subject: [PATCH 033/351] Update codeql action to v2 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b8726876..031ba56d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -71,4 +71,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 71ea2ce8c15f4195b94213bafbdbe9793c25b851 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 13:17:56 +0200 Subject: [PATCH 034/351] Remove sop-java and sop-java-picocli notices --- sop-java-picocli/README.md | 8 -------- sop-java/README.md | 8 -------- 2 files changed, 16 deletions(-) delete mode 100644 sop-java-picocli/README.md delete mode 100644 sop-java/README.md diff --git a/sop-java-picocli/README.md b/sop-java-picocli/README.md deleted file mode 100644 index d67967b9..00000000 --- a/sop-java-picocli/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -# MOVED -* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java-picocli) -* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java-picocli) diff --git a/sop-java/README.md b/sop-java/README.md deleted file mode 100644 index dd59c615..00000000 --- a/sop-java/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -# MOVED -* [Github](https://github.com/pgpainless/sop-java/tree/master/sop-java) -* [Codeberg](https://codeberg.org/PGPainless/sop-java/src/branch/master/sop-java) From 9ac681d88cd63e016bb0069ecc96ac0d648c210a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 13:18:39 +0200 Subject: [PATCH 035/351] Update SECURITY.md --- SECURITY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SECURITY.md b/SECURITY.md index 2036180a..bffceb73 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,6 +14,7 @@ currently being supported with security updates. | Version | Supported | |---------|--------------------| +| 1.6.X | :white_check_mark: | | 1.5.X | :white_check_mark: | | 1.4.X | :white_check_mark: | | 1.3.X | :white_check_mark: | From 41dfe71994f55e2f9c5e4c88f5d62ca5968dcba4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 14:17:53 +0200 Subject: [PATCH 036/351] Pad long KeyIDs with zeros to 16 chars --- .../org/pgpainless/key/util/KeyIdUtil.java | 2 +- .../pgpainless/key/util/KeyIdUtilTest.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java index 7ebac8fd..78e03f71 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -32,6 +32,6 @@ public final class KeyIdUtil { } public static String formatKeyId(long keyId) { - return Long.toHexString(keyId).toUpperCase(); + return String.format("%016X", keyId); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java new file mode 100644 index 00000000..f85bd43f --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/util/KeyIdUtilTest.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class KeyIdUtilTest { + + @Test + public void testParsing() { + String longKeyId = "b1bd1f049ec87f3d"; // we parse from lowercase, but formatting will produce uppercase + long asLong = KeyIdUtil.fromLongKeyId(longKeyId); + assertEquals(-5639317053693722819L, asLong); + assertEquals(longKeyId.toUpperCase(), KeyIdUtil.formatKeyId(-5639317053693722819L)); + } + + @Test + public void testParsingLowerAndUppercase() { + long fromLower = KeyIdUtil.fromLongKeyId("f5ffdf6d71dd5789"); + assertEquals(-720611754201229431L, fromLower); + long fromUpper = KeyIdUtil.fromLongKeyId("F5FFDF6D71DD5789"); + assertEquals(-720611754201229431L, fromUpper); + } + + @Test + public void formatLowerAsUpper() { + assertEquals("5F04ACF44FD822B1", KeyIdUtil.formatKeyId(KeyIdUtil.fromLongKeyId("5f04acf44fd822b1"))); + } + + @Test + public void testParsing0() { + long asLong = 0L; + String formatted = KeyIdUtil.formatKeyId(asLong); + assertEquals("0000000000000000", formatted); + assertEquals(asLong, KeyIdUtil.fromLongKeyId("0000000000000000")); + } +} From e7e269d7ceabf7dac3226417ea42b5fee30a0cb9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 6 Oct 2023 12:46:17 +0200 Subject: [PATCH 037/351] Documentation: Add section on reading certificates --- docs/source/pgpainless-core/quickstart.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/pgpainless-core/quickstart.md b/docs/source/pgpainless-core/quickstart.md index abb86358..1816983f 100644 --- a/docs/source/pgpainless-core/quickstart.md +++ b/docs/source/pgpainless-core/quickstart.md @@ -50,9 +50,15 @@ There is a very good chance that you can find code examples there that fit your Reading keys from ASCII armored strings or from binary files is easy: ```java +// Secret Keys String key = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"...; PGPSecretKeyRing secretKey = PGPainless.readKeyRing() .secretKeyRing(key); + +// Certificates (Public Keys) +String cert = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."; +PGPPublicKeyRing certificate = PGPainless.readKeyRing() + .publicKeyRing(cert); ``` Similarly, keys or certificates can quickly be exported: From 1b96919d84d3297a64a67d7bf2ee4b37f98a3d0f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:02:10 +0200 Subject: [PATCH 038/351] Allow generation of keys with empty key flags. Forbid certification of thirdparty certificates if CERTIFY_OTHERS flag is missing --- .../org/pgpainless/algorithm/KeyFlag.java | 2 +- .../pgpainless/exception/KeyException.java | 7 +++ .../key/certification/CertifyCertificate.java | 4 ++ .../key/generation/KeyRingBuilder.java | 9 ---- .../pgpainless/key/generation/KeySpec.java | 4 +- .../key/generation/KeySpecBuilder.java | 14 ++--- .../org/pgpainless/key/info/KeyRingInfo.java | 4 ++ ...GenerateKeyWithoutPrimaryKeyFlagsTest.java | 53 +++++++++++++++++++ 8 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java index 92205fd9..149ef9d0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java @@ -18,7 +18,7 @@ import org.bouncycastle.bcpg.sig.KeyFlags; public enum KeyFlag { /** - * This key may be used to certify other keys. + * This key may be used to certify third-party keys. */ CERTIFY_OTHER (KeyFlags.CERTIFY_OTHER), diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java index 66c5d2cf..65d27390 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyException.java @@ -67,6 +67,13 @@ public abstract class KeyException extends RuntimeException { } } + public static class UnacceptableThirdPartyCertificationKeyException extends KeyException { + + public UnacceptableThirdPartyCertificationKeyException(@Nonnull OpenPgpFingerprint fingerprint) { + super("Key " + fingerprint + " has no acceptable certification key.", fingerprint); + } + } + public static class UnacceptableSelfSignatureException extends KeyException { public UnacceptableSelfSignatureException(@Nonnull OpenPgpFingerprint fingerprint) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java index 9340d93d..5b6c6fe2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java @@ -280,6 +280,10 @@ public class CertifyCertificate { throw new KeyException.RevokedKeyException(fingerprint); } + if (!info.isUsableForThirdPartyCertification()) { + throw new KeyException.UnacceptableThirdPartyCertificationKeyException(fingerprint); + } + Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); if (expirationDate != null && expirationDate.before(now)) { throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index 208c17b6..84251c12 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -19,7 +19,6 @@ import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyRingGenerator; @@ -128,19 +127,11 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { } private void verifyMasterKeyCanCertify(KeySpec spec) { - if (!hasCertifyOthersFlag(spec)) { - throw new IllegalArgumentException("Certification Key MUST have KeyFlag CERTIFY_OTHER"); - } if (!keyIsCertificationCapable(spec)) { throw new IllegalArgumentException("Key algorithm " + spec.getKeyType().getName() + " is not capable of creating certifications."); } } - private boolean hasCertifyOthersFlag(KeySpec keySpec) { - KeyFlags keyFlags = keySpec.getSubpacketGenerator().getKeyFlagsSubpacket(); - return keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER); - } - private boolean keyIsCertificationCapable(KeySpec keySpec) { return keySpec.getKeyType().canCertify(); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java index 63645edd..6ac08aa7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java @@ -56,7 +56,7 @@ public class KeySpec { return keyCreationDate; } - public static KeySpecBuilder getBuilder(KeyType type, KeyFlag flag, KeyFlag... flags) { - return new KeySpecBuilder(type, flag, flags); + public static KeySpecBuilder getBuilder(KeyType type, KeyFlag... flags) { + return new KeySpecBuilder(type, flags); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java index 559dd3ce..1abd7dcd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java @@ -21,7 +21,6 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpackets; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.CollectionUtils; public class KeySpecBuilder implements KeySpecBuilderInterface { @@ -34,17 +33,14 @@ public class KeySpecBuilder implements KeySpecBuilderInterface { private Set preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms(); private Date keyCreationDate; - KeySpecBuilder(@Nonnull KeyType type, KeyFlag flag, KeyFlag... flags) { - if (flag == null) { - throw new IllegalArgumentException("Key MUST carry at least one key flag"); - } + KeySpecBuilder(@Nonnull KeyType type, KeyFlag... flags) { if (flags == null) { - throw new IllegalArgumentException("List of additional flags MUST NOT be null."); + this.keyFlags = new KeyFlag[0]; + } else { + SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, flags); + this.keyFlags = flags; } - flags = CollectionUtils.concat(flag, flags); - SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, flags); this.type = type; - this.keyFlags = flags; } @Override 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 77277622..31a6dd86 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 @@ -1170,6 +1170,10 @@ public class KeyRingInfo { return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); } + public boolean isUsableForThirdPartyCertification() { + return isKeyValidlyBound(getKeyId()) && getKeyFlagsOf(getKeyId()).contains(KeyFlag.CERTIFY_OTHER); + } + /** * Returns true, if the certificate has at least one usable encryption subkey. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java new file mode 100644 index 00000000..e2db311d --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.exception.KeyException; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenerateKeyWithoutPrimaryKeyFlagsTest { + + @Test + public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + .addUserId("Alice") + .build(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + assertTrue(info.getValidUserIds().contains("Alice")); + + long primaryKeyId = info.getKeyId(); + assertTrue(info.getKeyFlagsOf("Alice").isEmpty()); + assertTrue(info.getKeyFlagsOf(primaryKeyId).isEmpty()); + assertFalse(info.isUsableForThirdPartyCertification()); + + // Key without CERTIFY_OTHER flag cannot be used to certify other keys + PGPPublicKeyRing thirdPartyCert = TestKeys.getCryptiePublicKeyRing(); + assertThrows(KeyException.UnacceptableThirdPartyCertificationKeyException.class, () -> + PGPainless.certify().certificate(thirdPartyCert) + .withKey(secretKeys, SecretKeyRingProtector.unprotectedKeys())); + } +} From bf6c89af648f9dee39488f580d1a436e5ae5a10a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:09:22 +0200 Subject: [PATCH 039/351] Test usability of keyflag-less key --- ...GenerateKeyWithoutPrimaryKeyFlagsTest.java | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java index e2db311d..63f04a8f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -4,12 +4,33 @@ package org.pgpainless.key.generation; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.MessageMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.type.KeyType; @@ -18,14 +39,6 @@ import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class GenerateKeyWithoutPrimaryKeyFlagsTest { @Test @@ -35,6 +48,7 @@ public class GenerateKeyWithoutPrimaryKeyFlagsTest { .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addUserId("Alice") .build(); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); assertTrue(info.getValidUserIds().contains("Alice")); @@ -49,5 +63,31 @@ public class GenerateKeyWithoutPrimaryKeyFlagsTest { assertThrows(KeyException.UnacceptableThirdPartyCertificationKeyException.class, () -> PGPainless.certify().certificate(thirdPartyCert) .withKey(secretKeys, SecretKeyRingProtector.unprotectedKeys())); + + // Key without CERTIFY_OTHER flags is usable for encryption and signing + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.get().addRecipient(cert), + SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT) + )); + encryptionStream.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(cert)); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertext.toByteArray())) + .withOptions(ConsumerOptions.get().addDecryptionKey(secretKeys) + .addVerificationCert(cert)); + + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, plaintext); + decryptionStream.close(); + + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isEncryptedFor(cert)); + assertTrue(metadata.isVerifiedSignedBy(cert)); } } From 64dfefc9e7e1abc02b483022635457910302d053 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:00:24 +0200 Subject: [PATCH 040/351] Remove usage of PublicKeyAlgorithm.EC --- pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java | 1 - .../pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java | 1 - 2 files changed, 2 deletions(-) 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 6da94bf0..35787138 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -645,7 +645,6 @@ public final class Policy { minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); // §7.2.2 minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - minimalBitStrengths.put(PublicKeyAlgorithm.EC, 250); return new PublicKeyAlgorithmPolicy(minimalBitStrengths); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index e129e4be..f49a6271 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -72,7 +72,6 @@ public class RefuseToAddWeakSubkeyTest { minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); // §7.2.2 minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - minimalBitStrengths.put(PublicKeyAlgorithm.EC, 250); PGPainless.getPolicy().setPublicKeyAlgorithmPolicy(new Policy.PublicKeyAlgorithmPolicy(minimalBitStrengths)); SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); From c9dce319f8dcf377f496fcbbdced1ebe098d9097 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:01:12 +0200 Subject: [PATCH 041/351] Prepare for Kotlin conversion --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index d04b6345..a0f2d0e4 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'org.jetbrains.kotlin.jvm' version "1.8.10" } apply from: 'version.gradle' @@ -29,6 +30,7 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' + apply plugin: 'kotlin' compileJava { options.release = 8 From 94ad4cfbe7da3ed8925b80be8a40c025eab15776 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:01:54 +0200 Subject: [PATCH 042/351] Kotlin conversion: AEADAlgorithm --- .../pgpainless/algorithm/AEADAlgorithm.java | 100 ------------------ .../org/pgpainless/algorithm/AEADAlgorithm.kt | 30 ++++++ 2 files changed, 30 insertions(+), 100 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java deleted file mode 100644 index a5885005..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.java +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -/** - * List of AEAD algorithms defined in crypto-refresh-06. - * - * @see
- * Crypto-Refresh-06 §9.6 - AEAD Algorithms - */ -public enum AEADAlgorithm { - - EAX(1, 16, 16), - OCB(2, 15, 16), - GCM(3, 12, 16), - ; - - private final int algorithmId; - private final int ivLength; - private final int tagLength; - - private static final Map MAP = new HashMap<>(); - - static { - for (AEADAlgorithm h : AEADAlgorithm.values()) { - MAP.put(h.algorithmId, h); - } - } - - AEADAlgorithm(int id, int ivLength, int tagLength) { - this.algorithmId = id; - this.ivLength = ivLength; - this.tagLength = tagLength; - } - - /** - * Return the ID of the AEAD algorithm. - * - * @return algorithm ID - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return the length (in octets) of the IV. - * - * @return iv length - */ - public int getIvLength() { - return ivLength; - } - - /** - * Return the length (in octets) of the authentication tag. - * - * @return tag length - */ - public int getTagLength() { - return tagLength; - } - - /** - * Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, null is returned. - * - * @param id numeric id - * @return enum value - */ - @Nullable - public static AEADAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link AEADAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. - * - * @param id algorithm id - * @return enum value - * @throws NoSuchElementException in case of an unknown algorithm id - */ - @Nonnull - public static AEADAlgorithm requireFromId(int id) { - AEADAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No AEADAlgorithm found for id " + id); - } - return algorithm; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt new file mode 100644 index 00000000..61672122 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class AEADAlgorithm( + val algorithmId: Int, + val ivLength: Int, + val tagLength: Int) { + EAX(1, 16, 16), + OCB(2, 15, 16), + GCM(3, 12, 16), + ; + + companion object { + @JvmStatic + fun fromId(id: Int): AEADAlgorithm? { + return values().firstOrNull { + algorithm -> algorithm.algorithmId == id + } + } + + @JvmStatic + fun requireFromId(id: Int): AEADAlgorithm { + return fromId(id) ?: + throw NoSuchElementException("No AEADAlgorithm found for id $id") + } + } +} \ No newline at end of file From 644700c8eefaec407ed267a7bbc3babcb6fafd7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:02:13 +0200 Subject: [PATCH 043/351] Kotlin conversion: CertificationType --- ...ficationType.java => CertificationType.kt} | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/algorithm/{CertificationType.java => CertificationType.kt} (63%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.kt similarity index 63% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java rename to pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.kt index f5c8ec7e..33025ad6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.kt @@ -1,16 +1,12 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; - -import javax.annotation.Nonnull; - -/** - * Subset of {@link SignatureType}, reduced to certification types. - */ -public enum CertificationType { +package org.pgpainless.algorithm +enum class CertificationType( + val signatureType: SignatureType +) { /** * The issuer of this certification does not make any particular assertion as to how well the certifier has * checked that the owner of the key is in fact the person described by the User ID. @@ -34,13 +30,5 @@ public enum CertificationType { POSITIVE(SignatureType.POSITIVE_CERTIFICATION), ; - private final SignatureType signatureType; - - CertificationType(@Nonnull SignatureType signatureType) { - this.signatureType = signatureType; - } - - public @Nonnull SignatureType asSignatureType() { - return signatureType; - } -} + fun asSignatureType() = signatureType +} \ No newline at end of file From dfb33a5ecbbf7c5d9ca13575ca15b26f3d56232a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:02:37 +0200 Subject: [PATCH 044/351] Kotlin conversion: CompressionAlgorithm --- .../algorithm/CompressionAlgorithm.java | 79 ------------------- .../algorithm/CompressionAlgorithm.kt | 50 ++++++++++++ 2 files changed, 50 insertions(+), 79 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java deleted file mode 100644 index a2e78c5f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.java +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.CompressionAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Enumeration of possible compression algorithms. - * - * @see RFC4880: Compression Algorithm Tags - */ -public enum CompressionAlgorithm { - - UNCOMPRESSED (CompressionAlgorithmTags.UNCOMPRESSED), - ZIP (CompressionAlgorithmTags.ZIP), - ZLIB (CompressionAlgorithmTags.ZLIB), - BZIP2 (CompressionAlgorithmTags.BZIP2), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (CompressionAlgorithm c : CompressionAlgorithm.values()) { - MAP.put(c.algorithmId, c); - } - } - - /** - * Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id. - * If an invalid id is provided, null is returned. - * - * @param id id - * @return compression algorithm - */ - @Nullable - public static CompressionAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link CompressionAlgorithm} value that corresponds to the provided numerical id. - * If an invalid id is provided, thrown an {@link NoSuchElementException}. - * - * @param id id - * @return compression algorithm - * @throws NoSuchElementException in case of an unmapped id - */ - @Nonnull - public static CompressionAlgorithm requireFromId(int id) { - CompressionAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No CompressionAlgorithm found for id " + id); - } - return algorithm; - } - - private final int algorithmId; - - CompressionAlgorithm(int id) { - this.algorithmId = id; - } - - /** - * Return the numerical algorithm tag corresponding to this compression algorithm. - * @return id - */ - public int getAlgorithmId() { - return algorithmId; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt new file mode 100644 index 00000000..73179722 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible compression algorithms. + * + * See also [RFC4880 - Compression Algorithm Tags](https://tools.ietf.org/html/rfc4880#section-9.3) + */ +enum class CompressionAlgorithm(val algorithmId: Int) { + + UNCOMPRESSED(0), + ZIP(1), + ZLIB(2), + BZIP2(3), + ; + + companion object { + + /** + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. + * If an invalid id is provided, null is returned. + * + * @param id id + * @return compression algorithm + */ + @JvmStatic + fun fromId(id: Int): CompressionAlgorithm? { + return values().firstOrNull { + c -> c.algorithmId == id + } + } + + /** + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. + * If an invalid id is provided, throw an [NoSuchElementException]. + * + * @param id id + * @return compression algorithm + * @throws NoSuchElementException in case of an unmapped id + */ + @JvmStatic + fun requireFromId(id: Int): CompressionAlgorithm { + return fromId(id) ?: + throw NoSuchElementException("No CompressionAlgorithm found for id $id") + } + } +} \ No newline at end of file From e9dceb4553642c027e3d45336e9de0882b6677f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:02:59 +0200 Subject: [PATCH 045/351] Kotlin conversion: DocumentSignatureType --- .../algorithm/DocumentSignatureType.java | 35 ------------------- .../algorithm/DocumentSignatureType.kt | 20 +++++++++++ 2 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java deleted file mode 100644 index 4dbc58da..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -/** - * Subset of {@link SignatureType}, used for signatures over documents. - */ -public enum DocumentSignatureType { - - /** - * Signature is calculated over the unchanged binary data. - */ - BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), - - /** - * The signature is calculated over the text data with its line endings converted to - *
-     *     {@code <CR><LF>}
-     * 
. - */ - 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/algorithm/DocumentSignatureType.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.kt new file mode 100644 index 00000000..b3cd17ac --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class DocumentSignatureType(val signatureType: SignatureType) { + + /** + * Signature is calculated over the unchanged binary data. + */ + BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), + + /** + * The signature is calculated over the text data with its line endings + * converted to ``. + */ + CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT), + ; +} \ No newline at end of file From eb94aa6063a059be5a26ffd88c3ddc820f0e741b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:03:22 +0200 Subject: [PATCH 046/351] Kotlin conversion: EncryptionPurpose --- .../{EncryptionPurpose.java => EncryptionPurpose.kt} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/algorithm/{EncryptionPurpose.java => EncryptionPurpose.kt} (74%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.kt similarity index 74% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java rename to pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.kt index 5eda30c0..f7e5ce2d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.kt @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; +package org.pgpainless.algorithm -public enum EncryptionPurpose { +enum class EncryptionPurpose { /** * The stream will encrypt communication that goes over the wire. * E.g. EMail, Chat... @@ -19,4 +19,4 @@ public enum EncryptionPurpose { * The stream will use keys with either flags to encrypt the data. */ ANY -} +} \ No newline at end of file From 98e9c0934d608fa33881092161b9dc09ea97826c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:03:43 +0200 Subject: [PATCH 047/351] Kotlin conversion: Feature --- .../org/pgpainless/algorithm/Feature.java | 152 ------------------ .../java/org/pgpainless/algorithm/Feature.kt | 83 ++++++++++ 2 files changed, 83 insertions(+), 152 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java deleted file mode 100644 index d9aa9b90..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.Features; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * An enumeration of features that may be set in the {@link Features} subpacket. - * - * @see RFC4880: Features - */ -public enum Feature { - - /** - * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification - * Detection Code Packets. - * - * @see - * RFC-4880 §5.14: Modification Detection Code Packet - */ - MODIFICATION_DETECTION(Features.FEATURE_MODIFICATION_DETECTION), - - /** - * Support for Authenticated Encryption with Additional Data (AEAD). - * If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets. - * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * NOTE: This value is currently RESERVED. - * - * @see - * AEAD Encrypted Data Packet - */ - GNUPG_AEAD_ENCRYPTED_DATA(Features.FEATURE_AEAD_ENCRYPTED_DATA), - - /** - * If a key announces this feature, it is a version 5 public key. - * The version 5 format is similar to the version 4 format except for the addition of a count for the key material. - * This count helps to parse secret key packets (which are an extension of the public key packet format) in the case - * of an unknown algorithm. - * In addition, fingerprints of version 5 keys are calculated differently from version 4 keys. - * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * NOTE: This value is currently RESERVED. - * - * @see - * Public-Key Packet Formats - */ - GNUPG_VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY), - - /** - * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. - * - * @see - * crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format - */ - MODIFICATION_DETECTION_2((byte) 0x08), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (Feature f : Feature.values()) { - MAP.put(f.featureId, f); - } - } - - /** - * Return the {@link Feature} encoded by the given id. - * If the id does not match any known features, return null. - * - * @param id feature id - * @return feature - */ - @Nullable - public static Feature fromId(byte id) { - return MAP.get(id); - } - - /** - * Return the {@link Feature} encoded by the given id. - * If the id does not match any known features, throw an {@link NoSuchElementException}. - * - * @param id feature id - * @return feature - * @throws NoSuchElementException if an unmatched feature id is encountered - */ - @Nonnull - public static Feature requireFromId(byte id) { - Feature feature = fromId(id); - if (feature == null) { - throw new NoSuchElementException("Unknown feature id encountered: " + id); - } - return feature; - } - - private final byte featureId; - - Feature(byte featureId) { - this.featureId = featureId; - } - - /** - * Return the id of the feature. - * - * @return feature id - */ - public byte getFeatureId() { - return featureId; - } - - /** - * Convert a bitmask into a list of {@link KeyFlag KeyFlags}. - * - * @param bitmask bitmask - * @return list of key flags encoded by the bitmask - */ - @Nonnull - public static List fromBitmask(int bitmask) { - List features = new ArrayList<>(); - for (Feature f : Feature.values()) { - if ((bitmask & f.featureId) != 0) { - features.add(f); - } - } - return features; - } - - /** - * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. - * - * @param features list of flags - * @return bitmask - */ - public static byte toBitmask(Feature... features) { - byte mask = 0; - for (Feature f : features) { - mask |= f.featureId; - } - return mask; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt new file mode 100644 index 00000000..2e0058b5 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * An enumeration of features that may be set in the feature subpacket. + * + * See [RFC4880: Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) + */ +enum class Feature(val featureId: Byte) { + + /** + * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification + * Detection Code Packets. + * + * See [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14) + */ + MODIFICATION_DETECTION(0x01), + + /** + * Support for Authenticated Encryption with Additional Data (AEAD). + * If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets. + * + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! + * NOTE: This value is currently RESERVED. + * + * See [AEAD Encrypted Data Packet](https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-) + */ + GNUPG_AEAD_ENCRYPTED_DATA(0x02), + + /** + * If a key announces this feature, it is a version 5 public key. + * The version 5 format is similar to the version 4 format except for the addition of a count for the key material. + * This count helps to parse secret key packets (which are an extension of the public key packet format) in the case + * of an unknown algorithm. + * In addition, fingerprints of version 5 keys are calculated differently from version 4 keys. + * + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! + * NOTE: This value is currently RESERVED. + * + * See [Public-Key Packet Formats](https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats) + */ + GNUPG_VERSION_5_PUBLIC_KEY(0x04), + + /** + * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. + * + * See [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd) + */ + MODIFICATION_DETECTION_2(0x08), + ; + + companion object { + @JvmStatic + fun fromId(id: Byte): Feature? { + return values().firstOrNull { + f -> f.featureId == id + } + } + + @JvmStatic + fun requireFromId(id: Byte): Feature { + return fromId(id) ?: + throw NoSuchElementException("Unknown feature id encountered: $id") + } + + @JvmStatic + fun fromBitmask(bitmask: Int): List { + return values().filter { + it.featureId.toInt() and bitmask != 0 + } + } + + @JvmStatic + fun toBitmask(vararg features: Feature): Byte { + return features.map { it.featureId.toInt() } + .reduceOrNull { mask, f -> mask or f }?.toByte() + ?: 0 + } + } +} \ No newline at end of file From 2c384907424bad3a9081fb2e6bcac7aad3e20c1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:03:59 +0200 Subject: [PATCH 048/351] Kotlin conversion: HashAlgorithm --- .../pgpainless/algorithm/HashAlgorithm.java | 118 ------------------ .../org/pgpainless/algorithm/HashAlgorithm.kt | 76 +++++++++++ 2 files changed, 76 insertions(+), 118 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java deleted file mode 100644 index 0b9368bb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.java +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -import org.bouncycastle.bcpg.HashAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * An enumeration of different hashing algorithms. - * - * @see RFC4880: Hash Algorithms - */ -public enum HashAlgorithm { - @Deprecated - MD5 (HashAlgorithmTags.MD5, "MD5"), - SHA1 (HashAlgorithmTags.SHA1, "SHA1"), - RIPEMD160 (HashAlgorithmTags.RIPEMD160, "RIPEMD160"), - SHA256 (HashAlgorithmTags.SHA256, "SHA256"), - SHA384 (HashAlgorithmTags.SHA384, "SHA384"), - SHA512 (HashAlgorithmTags.SHA512, "SHA512"), - SHA224 (HashAlgorithmTags.SHA224, "SHA224"), - SHA3_256 (12, "SHA3-256"), - SHA3_512 (14, "SHA3-512"), - ; - - private static final Map ID_MAP = new HashMap<>(); - private static final Map NAME_MAP = new HashMap<>(); - - static { - for (HashAlgorithm h : HashAlgorithm.values()) { - ID_MAP.put(h.algorithmId, h); - NAME_MAP.put(h.name, h); - } - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, null is returned. - * - * @param id numeric id - * @return enum value - */ - @Nullable - public static HashAlgorithm fromId(int id) { - return ID_MAP.get(id); - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, throw a {@link NoSuchElementException}. - * - * @param id algorithm id - * @return enum value - * @throws NoSuchElementException in case of an unknown algorithm id - */ - @Nonnull - public static HashAlgorithm requireFromId(int id) { - HashAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No HashAlgorithm found for id " + id); - } - return algorithm; - } - - /** - * Return the {@link HashAlgorithm} value that corresponds to the provided name. - * If an invalid algorithm name was provided, null is returned. - * - * @see RFC4880: §9.4 Hash Algorithms - * for a list of algorithms and names. - * - * @param name text name - * @return enum value - */ - @Nullable - public static HashAlgorithm fromName(String name) { - String algorithmName = name.toUpperCase(); - HashAlgorithm algorithm = NAME_MAP.get(algorithmName); - if (algorithm == null) { - algorithm = NAME_MAP.get(algorithmName.replace("-", "")); - } - return algorithm; - } - - private final int algorithmId; - private final String name; - - HashAlgorithm(int id, String name) { - this.algorithmId = id; - this.name = name; - } - - /** - * Return the numeric algorithm id of the hash algorithm. - * - * @return numeric id - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return the text name of the hash algorithm. - * - * @return text name - */ - public String getAlgorithmName() { - return name; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt new file mode 100644 index 00000000..9c433efe --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * An enumeration of different hashing algorithms. + * + * See [RFC4880: Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-9.4) + */ +enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { + + @Deprecated("MD5 is deprecated") + MD5 (1, "MD5"), + SHA1 (2, "SHA1"), + RIPEMD160 (3, "RIPEMD160"), + SHA256 (8, "SHA256"), + SHA384 (9, "SHA384"), + SHA512 (10, "SHA512"), + SHA224 (11, "SHA224"), + SHA3_256 (12, "SHA3-256"), + SHA3_512 (14, "SHA3-512"), + ; + + companion object { + /** + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. + * If an invalid algorithm id was provided, null is returned. + * + * @param id numeric id + * @return enum value + */ + @JvmStatic + fun fromId(id: Int): HashAlgorithm? { + return values().firstOrNull { + h -> h.algorithmId == id + } + } + + /** + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. + * If an invalid algorithm id was provided, throw a [NoSuchElementException]. + * + * @param id algorithm id + * @return enum value + * @throws NoSuchElementException in case of an unknown algorithm id + */ + @JvmStatic + fun requireFromId(id: Int): HashAlgorithm { + return fromId(id) ?: + throw NoSuchElementException("No HashAlgorithm found for id $id") + } + + /** + * Return the [HashAlgorithm] value that corresponds to the provided name. + * If an invalid algorithm name was provided, null is returned. + * + * See [RFC4880: §9.4 Hash Algorithms](https://datatracker.ietf.org/doc/html/rfc4880#section-9.4) + * for a list of algorithms and names. + * + * @param name text name + * @return enum value + */ + @JvmStatic + fun fromName(name: String): HashAlgorithm? { + return name.uppercase().let { algoName -> + values().firstOrNull { + it.algorithmName == algoName + } ?: values().firstOrNull { + it.algorithmName == algoName.replace("-", "") + } + } + } + } +} \ No newline at end of file From 294c469a2940974353b0966c47ea1c363dbdb066 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:24:40 +0200 Subject: [PATCH 049/351] Kotlin conversion: KeyFlag --- .../org/pgpainless/algorithm/KeyFlag.java | 121 ------------------ .../java/org/pgpainless/algorithm/KeyFlag.kt | 92 +++++++++++++ 2 files changed, 92 insertions(+), 121 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java deleted file mode 100644 index 149ef9d0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.java +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.sig.KeyFlags; - -/** - * Enumeration of different key flags. - * Key flags denote different capabilities of a key pair. - * - * @see RFC4880: Key Flags - */ -public enum KeyFlag { - - /** - * This key may be used to certify third-party keys. - */ - CERTIFY_OTHER (KeyFlags.CERTIFY_OTHER), - - /** - * This key may be used to sign data. - */ - SIGN_DATA (KeyFlags.SIGN_DATA), - - /** - * This key may be used to encrypt communications. - */ - ENCRYPT_COMMS (KeyFlags.ENCRYPT_COMMS), - - /** - * This key may be used to encrypt storage. - */ - ENCRYPT_STORAGE(KeyFlags.ENCRYPT_STORAGE), - - /** - * The private component of this key may have been split by a secret-sharing mechanism. - */ - SPLIT (KeyFlags.SPLIT), - - /** - * This key may be used for authentication. - */ - AUTHENTICATION (KeyFlags.AUTHENTICATION), - - /** - * The private component of this key may be in the possession of more than one person. - */ - SHARED (KeyFlags.SHARED), - ; - - private final int flag; - - KeyFlag(int flag) { - this.flag = flag; - } - - /** - * Return the numeric id of the {@link KeyFlag}. - * - * @return numeric id - */ - public int getFlag() { - return flag; - } - - /** - * Convert a bitmask into a list of {@link KeyFlag KeyFlags}. - * - * @param bitmask bitmask - * @return list of key flags encoded by the bitmask - */ - public static List fromBitmask(int bitmask) { - List flags = new ArrayList<>(); - for (KeyFlag f : KeyFlag.values()) { - if ((bitmask & f.flag) != 0) { - flags.add(f); - } - } - return flags; - } - - /** - * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. - * - * @param flags list of flags - * @return bitmask - */ - public static int toBitmask(KeyFlag... flags) { - int mask = 0; - for (KeyFlag f : flags) { - mask |= f.getFlag(); - } - return mask; - } - - /** - * Return true if the provided bitmask has the bit for the provided flag set. - * Return false if the mask does not contain the flag. - * - * @param mask bitmask - * @param flag flag to be tested for - * @return true if flag is set, false otherwise - */ - public static boolean hasKeyFlag(int mask, KeyFlag flag) { - return (mask & flag.getFlag()) == flag.getFlag(); - } - - public static boolean containsAny(int mask, KeyFlag... flags) { - for (KeyFlag flag : flags) { - if (hasKeyFlag(mask, flag)) { - return true; - } - } - return false; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt new file mode 100644 index 00000000..d8686cd6 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class KeyFlag(val flag: Int) { + + /** + * This key may be used to certify third-party keys. + */ + CERTIFY_OTHER (1), + + /** + * This key may be used to sign data. + */ + SIGN_DATA (2), + + /** + * This key may be used to encrypt communications. + */ + ENCRYPT_COMMS (4), + + /** + * This key may be used to encrypt storage. + */ + ENCRYPT_STORAGE(8), + + /** + * The private component of this key may have been split by a secret-sharing mechanism. + */ + SPLIT (16), + + /** + * This key may be used for authentication. + */ + AUTHENTICATION (32), + + /** + * The private component of this key may be in the possession of more than one person. + */ + SHARED (128), + ; + + companion object { + + /** + * Convert a bitmask into a list of [KeyFlags][KeyFlag]. + * + * @param bitmask bitmask + * @return list of key flags encoded by the bitmask + */ + @JvmStatic + fun fromBitmask(bitmask: Int): List { + return values().filter { + it.flag and bitmask != 0 + } + } + + /** + * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. + * + * @param flags list of flags + * @return bitmask + */ + @JvmStatic + fun toBitmask(vararg flags: KeyFlag): Int { + return flags.map { it.flag }.reduceOrNull { mask, f -> mask or f } + ?: 0 + } + + /** + * Return true if the provided bitmask has the bit for the provided flag set. + * Return false if the mask does not contain the flag. + * + * @param mask bitmask + * @param flag flag to be tested for + * @return true if flag is set, false otherwise + */ + @JvmStatic + fun hasKeyFlag(mask: Int, flag: KeyFlag): Boolean { + return mask and flag.flag == flag.flag + } + + @JvmStatic + fun containsAny(mask: Int, vararg flags: KeyFlag): Boolean { + return flags.any { + hasKeyFlag(mask, it) + } + } + } +} \ No newline at end of file From eb07b94bcb2fab1cc633d8e412ca5fccde9676c7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:04:38 +0200 Subject: [PATCH 050/351] Kotlin conversion: OpenPgpPacket --- .../pgpainless/algorithm/OpenPgpPacket.java | 71 ------------------- .../org/pgpainless/algorithm/OpenPgpPacket.kt | 46 ++++++++++++ 2 files changed, 46 insertions(+), 71 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java deleted file mode 100644 index 41e3fb08..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.bouncycastle.bcpg.PacketTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; -import java.util.NoSuchElementException; - -public enum OpenPgpPacket { - PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), - SIG(PacketTags.SIGNATURE), - SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), - OPS(PacketTags.ONE_PASS_SIGNATURE), - SK(PacketTags.SECRET_KEY), - PK(PacketTags.PUBLIC_KEY), - SSK(PacketTags.SECRET_SUBKEY), - COMP(PacketTags.COMPRESSED_DATA), - SED(PacketTags.SYMMETRIC_KEY_ENC), - MARKER(PacketTags.MARKER), - LIT(PacketTags.LITERAL_DATA), - TRUST(PacketTags.TRUST), - UID(PacketTags.USER_ID), - PSK(PacketTags.PUBLIC_SUBKEY), - UATTR(PacketTags.USER_ATTRIBUTE), - SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), - MDC(PacketTags.MOD_DETECTION_CODE), - - EXP_1(PacketTags.EXPERIMENTAL_1), - EXP_2(PacketTags.EXPERIMENTAL_2), - EXP_3(PacketTags.EXPERIMENTAL_3), - EXP_4(PacketTags.EXPERIMENTAL_4), - ; - - static final Map MAP = new HashMap<>(); - - static { - for (OpenPgpPacket p : OpenPgpPacket.values()) { - MAP.put(p.getTag(), p); - } - } - - final int tag; - - @Nullable - public static OpenPgpPacket fromTag(int tag) { - return MAP.get(tag); - } - - @Nonnull - public static OpenPgpPacket requireFromTag(int tag) { - OpenPgpPacket p = fromTag(tag); - if (p == null) { - throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); - } - return p; - } - - OpenPgpPacket(int tag) { - this.tag = tag; - } - - int getTag() { - return tag; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt new file mode 100644 index 00000000..f05599bd --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +enum class OpenPgpPacket(val tag: Int) { + PKESK(1), + SIG(2), + SKESK(3), + OPS(4), + SK(5), + PK(6), + SSK(7), + COMP(8), + SED(9), + MARKER(10), + LIT(11), + TRUST(12), + UID(13), + PSK(14), + UATTR(17), + SEIPD(18), + MDC(19), + + EXP_1(60), + EXP_2(61), + EXP_3(62), + EXP_4(63), + ; + + companion object { + @JvmStatic + fun fromTag(tag: Int): OpenPgpPacket? { + return values().firstOrNull { + it.tag == tag + } + } + + @JvmStatic + fun requireFromTag(tag: Int): OpenPgpPacket { + return fromTag(tag) ?: + throw NoSuchElementException("No OpenPGP packet known for tag $tag") + } + } +} \ No newline at end of file From 608edc2378e502a08136214574a432429578ca9c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:04:56 +0200 Subject: [PATCH 051/351] Kotlin conversion: PublicKeyAlgorithm --- .../algorithm/PublicKeyAlgorithm.java | 163 ------------------ .../algorithm/PublicKeyAlgorithm.kt | 96 +++++++++++ 2 files changed, 96 insertions(+), 163 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java deleted file mode 100644 index c7599db9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Enumeration of public key algorithms as defined in RFC4880. - * - * @see RFC4880: Public-Key Algorithms - */ -public enum PublicKeyAlgorithm { - - /** - * RSA capable of encryption and signatures. - */ - RSA_GENERAL (PublicKeyAlgorithmTags.RSA_GENERAL, true, true), - - /** - * RSA with usage encryption. - * - * @deprecated see Deprecation notice - */ - @Deprecated - RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), - - /** - * RSA with usage of creating signatures. - * - * @deprecated see Deprecation notice - */ - @Deprecated - RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), - - /** - * ElGamal with usage encryption. - */ - ELGAMAL_ENCRYPT (PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, false, true), - - /** - * Digital Signature Algorithm. - */ - DSA (PublicKeyAlgorithmTags.DSA, true, false), - - /** - * EC is deprecated. - * @deprecated use {@link #ECDH} instead. - */ - @Deprecated - EC (PublicKeyAlgorithmTags.EC, false, true), - - /** - * Elliptic Curve Diffie-Hellman. - */ - ECDH (PublicKeyAlgorithmTags.ECDH, false, true), - - /** - * Elliptic Curve Digital Signature Algorithm. - */ - ECDSA (PublicKeyAlgorithmTags.ECDSA, true, false), - - /** - * ElGamal General. - * - * @deprecated see Deprecation notice - */ - @Deprecated - ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), - - /** - * Diffie-Hellman key exchange algorithm. - */ - DIFFIE_HELLMAN (PublicKeyAlgorithmTags.DIFFIE_HELLMAN, false, true), - - /** - * Digital Signature Algorithm based on twisted Edwards Curves. - */ - EDDSA (PublicKeyAlgorithmTags.EDDSA, true, false), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (PublicKeyAlgorithm p : PublicKeyAlgorithm.values()) { - MAP.put(p.algorithmId, p); - } - } - - /** - * Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id. - * If an invalid id is provided, null is returned. - * - * @param id numeric algorithm id - * @return algorithm or null - */ - @Nullable - public static PublicKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link PublicKeyAlgorithm} that corresponds to the provided algorithm id. - * If an invalid id is provided, throw a {@link NoSuchElementException}. - * - * @param id numeric algorithm id - * @return algorithm - * @throws NoSuchElementException in case of an unmatched algorithm id - */ - @Nonnull - public static PublicKeyAlgorithm requireFromId(int id) { - PublicKeyAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No PublicKeyAlgorithm found for id " + id); - } - return algorithm; - } - - private final int algorithmId; - private final boolean signingCapable; - private final boolean encryptionCapable; - - PublicKeyAlgorithm(int algorithmId, boolean signingCapable, boolean encryptionCapable) { - this.algorithmId = algorithmId; - this.signingCapable = signingCapable; - this.encryptionCapable = encryptionCapable; - } - - /** - * Return the numeric identifier of the public key algorithm. - * - * @return id - */ - public int getAlgorithmId() { - return algorithmId; - } - - /** - * Return true if this public key algorithm is able to create signatures. - * - * @return true if the algorithm can sign - */ - public boolean isSigningCapable() { - return signingCapable; - } - - /** - * Return true if this public key algorithm can be used as an encryption algorithm. - * - * @return true if the algorithm can encrypt - */ - public boolean isEncryptionCapable() { - return encryptionCapable; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt new file mode 100644 index 00000000..9fad8e7d --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of public key algorithms as defined in RFC4880. + * + * See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1) + */ +enum class PublicKeyAlgorithm( + val algorithmId: Int, + val signingCapable: Boolean, + val encryptionCapable: Boolean) { + + /** + * RSA capable of encryption and signatures. + */ + RSA_GENERAL (1, true, true), + + /** + * RSA with usage encryption. + * + * @deprecated see Deprecation notice + */ + @Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", + ReplaceWith("RSA_GENERAL")) + RSA_ENCRYPT (2, false, true), + + /** + * RSA with usage of creating signatures. + * + * @deprecated see Deprecation notice + */ + @Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", + ReplaceWith("RSA_GENERAL")) + RSA_SIGN (3, true, false), + + /** + * ElGamal with usage encryption. + */ + ELGAMAL_ENCRYPT (16, false, true), + + /** + * Digital Signature Algorithm. + */ + DSA (17, true, false), + + /** + * Elliptic Curve Diffie-Hellman. + */ + ECDH (18, false, true), + + /** + * Elliptic Curve Digital Signature Algorithm. + */ + ECDSA (19, true, false), + + /** + * ElGamal General. + * + * @deprecated see Deprecation notice + */ + @Deprecated("ElGamal is deprecated") + ELGAMAL_GENERAL (20, true, true), + + /** + * Diffie-Hellman key exchange algorithm. + */ + DIFFIE_HELLMAN (21, false, true), + + /** + * Digital Signature Algorithm based on twisted Edwards Curves. + */ + EDDSA (22, true, false), + ; + + fun isSigningCapable(): Boolean = signingCapable + fun isEncryptionCapable(): Boolean = encryptionCapable + + companion object { + @JvmStatic + fun fromId(id: Int): PublicKeyAlgorithm? { + return values().firstOrNull { + it.algorithmId == id + } + } + + @JvmStatic + fun requireFromId(id: Int): PublicKeyAlgorithm { + return fromId(id) ?: + throw NoSuchElementException("No PublicKeyAlgorithm found for id $id") + } + } +} \ No newline at end of file From c8bfcc807be71fc5cd99e29f2b59af4d539a1037 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:05:13 +0200 Subject: [PATCH 052/351] Kotlin conversion: RevocationState --- .../pgpainless/algorithm/RevocationState.java | 131 ------------------ .../pgpainless/algorithm/RevocationState.kt | 87 ++++++++++++ 2 files changed, 87 insertions(+), 131 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java deleted file mode 100644 index 8e4a60d3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.java +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import org.pgpainless.util.DateUtil; - -import javax.annotation.Nonnull; -import java.util.Date; -import java.util.NoSuchElementException; - -public final class RevocationState implements Comparable { - - private final RevocationStateType type; - private final Date date; - - private RevocationState(RevocationStateType type) { - this(type, null); - } - - private RevocationState(RevocationStateType type, Date date) { - this.type = type; - if (type == RevocationStateType.softRevoked && date == null) { - throw new NullPointerException("If type is 'softRevoked' then date cannot be null."); - } - this.date = date; - } - - public static RevocationState notRevoked() { - return new RevocationState(RevocationStateType.notRevoked); - } - - public static RevocationState softRevoked(@Nonnull Date date) { - return new RevocationState(RevocationStateType.softRevoked, date); - } - - public static RevocationState hardRevoked() { - return new RevocationState(RevocationStateType.hardRevoked); - } - - public RevocationStateType getType() { - return type; - } - - public @Nonnull Date getDate() { - if (!isSoftRevocation()) { - throw new NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date."); - } - return date; - } - - public boolean isHardRevocation() { - return getType() == RevocationStateType.hardRevoked; - } - - public boolean isSoftRevocation() { - return getType() == RevocationStateType.softRevoked; - } - - public boolean isNotRevoked() { - return getType() == RevocationStateType.notRevoked; - } - - @Override - public String toString() { - String out = getType().toString(); - if (isSoftRevocation()) { - out = out + " (" + DateUtil.formatUTCDate(date) + ")"; - } - return out; - } - - @Override - public int compareTo(@Nonnull RevocationState o) { - switch (getType()) { - case notRevoked: - if (o.isNotRevoked()) { - return 0; - } else { - return -1; - } - - case softRevoked: - if (o.isNotRevoked()) { - return 1; - } else if (o.isSoftRevocation()) { - // Compare soft dates in reverse - return o.getDate().compareTo(getDate()); - } else { - return -1; - } - - case hardRevoked: - if (o.isHardRevocation()) { - return 0; - } else { - return 1; - } - - default: - throw new AssertionError("Unknown type: " + type); - } - } - - @Override - public int hashCode() { - return type.hashCode() * 31 + (isSoftRevocation() ? getDate().hashCode() : 0); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof RevocationState)) { - return false; - } - RevocationState other = (RevocationState) obj; - if (getType() != other.getType()) { - return false; - } - if (isSoftRevocation()) { - return DateUtil.toSecondsPrecision(getDate()).getTime() == DateUtil.toSecondsPrecision(other.getDate()).getTime(); - } - return true; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt new file mode 100644 index 00000000..d5d95d45 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +import org.pgpainless.util.DateUtil +import java.lang.AssertionError +import java.util.* +import kotlin.NoSuchElementException + +class RevocationState private constructor( + val type: RevocationStateType, + private val _date: Date?): Comparable { + + val date: Date + get() { + if (!isSoftRevocation()) { + throw NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date.") + } + return _date!! + } + + private constructor(type: RevocationStateType): this(type, null) + + fun isSoftRevocation() = type == RevocationStateType.softRevoked + fun isHardRevocation() = type == RevocationStateType.hardRevoked + fun isNotRevoked() = type == RevocationStateType.notRevoked + + companion object { + @JvmStatic + fun notRevoked() = RevocationState(RevocationStateType.notRevoked) + + @JvmStatic + fun softRevoked(date: Date) = RevocationState(RevocationStateType.softRevoked, date) + + @JvmStatic + fun hardRevoked() = RevocationState(RevocationStateType.hardRevoked) + } + + override fun compareTo(other: RevocationState): Int { + return when(type) { + RevocationStateType.notRevoked -> + if (other.isNotRevoked()) 0 + else -1 + RevocationStateType.softRevoked -> + if (other.isNotRevoked()) 1 + // Compare soft dates in reverse + else if (other.isSoftRevocation()) other.date.compareTo(date) + else -1 + RevocationStateType.hardRevoked -> + if (other.isHardRevocation()) 0 + else 1 + else -> throw AssertionError("Unknown type: $type") + } + } + + override fun toString(): String { + return buildString { + append(type) + if (isSoftRevocation()) append(" (${DateUtil.formatUTCDate(date)})") + } + } + + override fun hashCode(): Int { + return type.hashCode() * 31 + if (isSoftRevocation()) date.hashCode() else 0 + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (this === other) { + return true + } + if (other !is RevocationState) { + return false + } + if (type != other.type) { + return false + } + if (isSoftRevocation()) { + return DateUtil.toSecondsPrecision(date).time == DateUtil.toSecondsPrecision(other.date).time + } + return true + } +} \ No newline at end of file From 2fb7e6c3a3fad7ab634ba1df5739b0909c8ce5f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:05:33 +0200 Subject: [PATCH 053/351] Kotlin conversion: RevocationStateType --- .../{RevocationStateType.java => RevocationStateType.kt} | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/algorithm/{RevocationStateType.java => RevocationStateType.kt} (68%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.kt similarity index 68% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java rename to pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.kt index d1757255..bf18e27f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.kt @@ -1,11 +1,10 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; - -public enum RevocationStateType { +package org.pgpainless.algorithm +enum class RevocationStateType { /** * Certificate is not revoked. */ @@ -20,4 +19,4 @@ public enum RevocationStateType { * Certificate is revoked with a hard revocation. */ hardRevoked -} +} \ No newline at end of file From f0082d3fb71be308bb599680fc05d5ac96ca5736 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:05:54 +0200 Subject: [PATCH 054/351] Kotlin conversion: SignatureSubpacket --- ...reSubpacket.java => SignatureSubpacket.kt} | 263 +++++++----------- 1 file changed, 101 insertions(+), 162 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/algorithm/{SignatureSubpacket.java => SignatureSubpacket.kt} (59%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.kt similarity index 59% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java rename to pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.kt index 9429f0c6..fcb0c90a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.kt @@ -1,77 +1,40 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; +package org.pgpainless.algorithm -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ATTESTED_CERTIFICATIONS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.CREATION_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EMBEDDED_SIGNATURE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPIRE_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPORTABLE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.FEATURES; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_FINGERPRINT; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_KEY_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_EXPIRE_TIME; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_FLAGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_SERVER_PREFS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.NOTATION_DATA; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PLACEHOLDER; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.POLICY_URL; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_COMP_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_HASH_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_KEY_SERV; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_SYM_ALGS; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.PRIMARY_USER_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REG_EXP; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCABLE; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNATURE_TARGET; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNER_USER_ID; -import static org.bouncycastle.bcpg.SignatureSubpacketTags.TRUST_SIG; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; +import org.bouncycastle.bcpg.SignatureSubpacketTags.* /** * Enumeration of possible subpackets that might be found in the hashed and unhashed area of an OpenPGP signature. * - * @see RFC4880: Signature Subpacket Specification + * See [RFC4880: Signature Subpacket Specification](https://tools.ietf.org/html/rfc4880#section-5.2.3.1) */ -public enum SignatureSubpacket { - +enum class SignatureSubpacket(val code: Int) { /** * The time the signature was made. * MUST be present in the hashed area of the signature. * - * @see Signature Creation Time + * See [Signature Creation Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.4) */ - signatureCreationTime(CREATION_TIME), + signatureCreationTime(2), /** * The validity period of the signature. This is the number of seconds * after the signature creation time that the signature expires. If * this is not present or has a value of zero, it never expires. * - * @see Signature Expiration Time + * See [Signature Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.10) */ - signatureExpirationTime(EXPIRE_TIME), + signatureExpirationTime(3), /** * Denotes whether the signature is exportable for other users. * - * @see Exportable Certification + * See [Exportable Certification](https://tools.ietf.org/html/rfc4880#section-5.2.3.11) */ - exportableCertification(EXPORTABLE), + exportableCertification(4), /** * Signer asserts that the key is not only valid but also trustworthy at @@ -87,9 +50,9 @@ public enum SignatureSubpacket { * greater indicate complete trust. Implementations SHOULD emit values * of 60 for partial trust and 120 for complete trust. * - * @see Trust Signature + * See [Trust Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.13) */ - trustSignature(TRUST_SIG), + trustSignature(5), /** * Used in conjunction with trust Signature packets (of level greater 0) to @@ -100,9 +63,9 @@ public enum SignatureSubpacket { * "almost public domain" regular expression [REGEX] package. A * description of the syntax is found in Section 8 below. * - * @see Regular Expression + * See [Regular Expression](https://tools.ietf.org/html/rfc4880#section-5.2.3.14) */ - regularExpression(REG_EXP), + regularExpression(6), /** * Signature's revocability status. The packet body contains a Boolean @@ -112,9 +75,9 @@ public enum SignatureSubpacket { * signature for the life of his key. If this packet is not present, * the signature is revocable. * - * @see Revocable + * See [Revocable](https://tools.ietf.org/html/rfc4880#section-5.2.3.12) */ - revocable(REVOCABLE), + revocable(7), /** * The validity period of the key. This is the number of seconds after @@ -122,14 +85,14 @@ public enum SignatureSubpacket { * or has a value of zero, the key never expires. This is found only on * a self-signature. * - * @see Key Expiration Time + * See [Key Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.6) */ - keyExpirationTime(KEY_EXPIRE_TIME), + keyExpirationTime(9), /** * Placeholder for backwards compatibility. */ - placeholder(PLACEHOLDER), + placeholder(10), /** * Symmetric algorithm numbers that indicate which algorithms the keyholder @@ -138,9 +101,9 @@ public enum SignatureSubpacket { * algorithms listed are supported by the recipient's software. * This is only found on a self-signature. * - * @see Preferred Symmetric Algorithms + * See [Preferred Symmetric Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.7) */ - preferredSymmetricAlgorithms(PREFERRED_SYM_ALGS), + preferredSymmetricAlgorithms(11), /** * Authorizes the specified key to issue revocation signatures for this @@ -159,16 +122,16 @@ public enum SignatureSubpacket { * isolate this subpacket within a separate signature so that it is not * combined with other subpackets that need to be exported. * - * @see Revocation Key + * See [Revocation Key](https://tools.ietf.org/html/rfc4880#section-5.2.3.15) */ - revocationKey(REVOCATION_KEY), + revocationKey(12), /** * The OpenPGP Key ID of the key issuing the signature. * - * @see Issuer Key ID + * See [Issuer Key ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.5) */ - issuerKeyId(ISSUER_KEY_ID), + issuerKeyId(16), /** * This subpacket describes a "notation" on the signature that the @@ -178,9 +141,9 @@ public enum SignatureSubpacket { * the signature cares to make. The "flags" field holds four octets of * flags. * - * @see Notation Data + * See [Notation Data](https://tools.ietf.org/html/rfc4880#section-5.2.3.16) */ - notationData(NOTATION_DATA), + notationData(20), /** * Message digest algorithm numbers that indicate which algorithms the @@ -188,9 +151,9 @@ public enum SignatureSubpacket { * algorithms, the list is ordered. * This is only found on a self-signature. * - * @see Preferred Hash Algorithms + * See [Preferred Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.8) */ - preferredHashAlgorithms(PREFERRED_HASH_ALGS), + preferredHashAlgorithms(21), /** * Compression algorithm numbers that indicate which algorithms the @@ -200,9 +163,9 @@ public enum SignatureSubpacket { * software might have no compression software in that implementation. * This is only found on a self-signature. * - * @see Preferred Compressio Algorithms + * See [Preferred Compressio Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.9) */ - preferredCompressionAlgorithms(PREFERRED_COMP_ALGS), + preferredCompressionAlgorithms(22), /** * This is a list of one-bit flags that indicate preferences that the @@ -210,9 +173,9 @@ public enum SignatureSubpacket { * undefined flags MUST be zero. * This is found only on a self-signature. * - * @see Key Server Preferences + * See [Key Server Preferences](https://tools.ietf.org/html/rfc4880#section-5.2.3.17) */ - keyServerPreferences(KEY_SERVER_PREFS), + keyServerPreferences(23), /** * This is a URI of a key server that the keyholder prefers be used for @@ -221,9 +184,9 @@ public enum SignatureSubpacket { * key server can actually be a copy of the key retrieved by ftp, http, * finger, etc. * - * @see Preferred Key Server + * See [Preferred Key Server](https://tools.ietf.org/html/rfc4880#section-5.2.3.18) */ - preferredKeyServers(PREFERRED_KEY_SERV), + preferredKeyServers(24), /** * This is a flag in a User ID's self-signature that states whether this @@ -242,17 +205,17 @@ public enum SignatureSubpacket { * different and independent "primaries" -- one for User IDs, and one * for User Attributes. * - * @see Primary User-ID + * See [Primary User-ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.19) */ - primaryUserId(PRIMARY_USER_ID), + primaryUserId(25), /** * This subpacket contains a URI of a document that describes the policy * under which the signature was issued. * - * @see Policy URL + * See [Policy URL](https://tools.ietf.org/html/rfc4880#section-5.2.3.20) */ - policyUrl(POLICY_URL), + policyUrl(26), /** * This subpacket contains a list of binary flags that hold information @@ -261,9 +224,9 @@ public enum SignatureSubpacket { * list is shorter than an implementation expects, the unstated flags * are considered to be zero. * - * @see Key Flags + * See [Key Flags](https://tools.ietf.org/html/rfc4880#section-5.2.3.21) */ - keyFlags(KEY_FLAGS), + keyFlags(27), /** * This subpacket allows a keyholder to state which User ID is @@ -272,9 +235,9 @@ public enum SignatureSubpacket { * personal communications. This subpacket allows such a keyholder to * state which of their roles is making a signature. * - * @see Signer's User ID + * See [Signer's User ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.22) */ - signerUserId(SIGNER_USER_ID), + signerUserId(28), /** * This subpacket is used only in key revocation and certification @@ -291,9 +254,9 @@ public enum SignatureSubpacket { * 32 - User ID information is no longer valid (cert revocations) * 100-110 - Private Use * - * @see Reason for Revocation + * See [Reason for Revocation](https://tools.ietf.org/html/rfc4880#section-5.2.3.23) */ - revocationReason(REVOCATION_REASON), + revocationReason(29), /** * The Features subpacket denotes which advanced OpenPGP features a @@ -305,9 +268,9 @@ public enum SignatureSubpacket { * This subpacket is similar to a preferences subpacket, and only * appears in a self-signature. * - * @see Features + * See [Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) */ - features(FEATURES), + features(30), /** * This subpacket identifies a specific target signature to which a @@ -321,18 +284,18 @@ public enum SignatureSubpacket { * signature. For example, a target signature with a SHA-1 hash MUST * have 20 octets of hash data. * - * @see Signature Target + * See [Signature Target](https://tools.ietf.org/html/rfc4880#section-5.2.3.25) */ - signatureTarget(SIGNATURE_TARGET), + signatureTarget(31), /** * This subpacket contains a complete Signature packet body as * specified in Section 5.2 above. It is useful when one signature * needs to refer to, or be incorporated in, another signature. * - * @see Embedded Signature + * See [Embedded Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.26) */ - embeddedSignature(EMBEDDED_SIGNATURE), + embeddedSignature(32), /** * The OpenPGP Key fingerprint of the key issuing the signature. This @@ -344,9 +307,9 @@ public enum SignatureSubpacket { * Note that the length N of the fingerprint for a version 4 key is 20 * octets; for a version 5 key N is 32. * - * @see Issuer Fingerprint + * See [Issuer Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28) */ - issuerFingerprint(ISSUER_FINGERPRINT), + issuerFingerprint(33), /** * AEAD algorithm numbers that indicate which AEAD algorithms the @@ -357,9 +320,9 @@ public enum SignatureSubpacket { * Note that support for the AEAD Encrypted Data packet in the general * is indicated by a Feature Flag. * - * @see Preferred AEAD Algorithms + * See [Preferred AEAD Algorithms](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8) */ - preferredAEADAlgorithms(PREFERRED_AEAD_ALGORITHMS), + preferredAEADAlgorithms(39), /** * The OpenPGP Key fingerprint of the intended recipient primary key. @@ -372,9 +335,9 @@ public enum SignatureSubpacket { * Note that the length N of the fingerprint for a version 4 key is 20 * octets; for a version 5 key N is 32. * - * @see Intended Recipient Fingerprint + * See [Intended Recipient Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29) */ - intendedRecipientFingerprint(INTENDED_RECIPIENT_FINGERPRINT), + intendedRecipientFingerprint(35), /** * This subpacket MUST only appear as a hashed subpacket of an @@ -388,75 +351,51 @@ public enum SignatureSubpacket { * Attested Certification subpacket in any generated Attestation Key * Signature. * - * @see Attested Certification + * See [Attested Certification](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30) */ - attestedCertification(ATTESTED_CERTIFICATIONS) + attestedCertification(37) ; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (SignatureSubpacket p : values()) { - MAP.put(p.code, p); - } - } - - private final int code; - - SignatureSubpacket(int code) { - this.code = code; - } - - /** - * Return the numerical identifier of the {@link SignatureSubpacket}. - * @return id - */ - public int getCode() { - return code; - } - - /** - * Return the {@link SignatureSubpacket} that corresponds to the provided id. - * If an unmatched code is presented, return null. - * - * @param code id - * @return signature subpacket - */ - @Nullable - public static SignatureSubpacket fromCode(int code) { - return MAP.get(code); - } - - /** - * Return the {@link SignatureSubpacket} that corresponds to the provided code. - * - * @param code code - * @return signature subpacket - * @throws NoSuchElementException in case of an unmatched subpacket tag - */ - @Nonnull - public static SignatureSubpacket requireFromCode(int code) { - SignatureSubpacket tag = fromCode(code); - if (tag == null) { - throw new NoSuchElementException("No SignatureSubpacket tag found with code " + code); - } - return tag; - } - - /** - * Convert an array of signature subpacket tags into a list of {@link SignatureSubpacket SignatureSubpackets}. - * - * @param codes array of codes - * @return list of subpackets - */ - public static List fromCodes(int[] codes) { - List tags = new ArrayList<>(); - for (int code : codes) { - try { - tags.add(requireFromCode(code)); - } catch (NoSuchElementException e) { - // skip + + companion object { + + /** + * Return the [SignatureSubpacket] that corresponds to the provided id. + * If an unmatched code is presented, return null. + * + * @param code id + * @return signature subpacket + */ + @JvmStatic + fun fromCode(code: Int): SignatureSubpacket? { + return values().firstOrNull { + it.code == code + } + } + + /** + * Return the [SignatureSubpacket] that corresponds to the provided code. + * + * @param code code + * @return signature subpacket + * @throws NoSuchElementException in case of an unmatched subpacket tag + */ + @JvmStatic + fun requireFromCode(code: Int): SignatureSubpacket { + return fromCode(code) ?: + throw NoSuchElementException("No SignatureSubpacket tag found with code $code") + } + + /** + * Convert an array of signature subpacket tags into a list of [SignatureSubpacket SignatureSubpackets]. + * + * @param codes array of codes + * @return list of subpackets + */ + @JvmStatic + fun fromCodes(vararg codes: Int): List { + return codes.toList().mapNotNull { + fromCode(it) } } - return tags; } -} +} \ No newline at end of file From 595b9c73793e075e0dece9b1d31b0498ef925d0a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:06:12 +0200 Subject: [PATCH 055/351] Kotlin conversion: SignatureType --- .../{SignatureType.java => SignatureType.kt} | 187 +++++++++--------- 1 file changed, 95 insertions(+), 92 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/algorithm/{SignatureType.java => SignatureType.kt} (58%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.kt similarity index 58% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java rename to pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.kt index c2f02989..1b708a03 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.kt @@ -1,29 +1,25 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.algorithm; +package org.pgpainless.algorithm -import org.bouncycastle.openpgp.PGPSignature; - -import javax.annotation.Nonnull; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import org.bouncycastle.openpgp.PGPSignature /** * Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1 - * See {@link org.bouncycastle.openpgp.PGPSignature} for comparison. + * See [PGPSignature] for comparison. * - * @see rfc4880 §5.2.1. Signature Types + * See [rfc4880 §5.2.1. Signature Types](https://tools.ietf.org/html/rfc4880#section-5.11) */ -public enum SignatureType { +enum class SignatureType(val code: Int) { /** * Signature of a binary document. * This means the signer owns it, created it, or certifies that it * has not been modified. */ - BINARY_DOCUMENT(PGPSignature.BINARY_DOCUMENT), + BINARY_DOCUMENT(0x00), /** * Signature of a canonical text document. @@ -31,7 +27,7 @@ public enum SignatureType { * has not been modified. The signature is calculated over the text * data with its line endings converted to {@code }. */ - CANONICAL_TEXT_DOCUMENT(PGPSignature.CANONICAL_TEXT_DOCUMENT), + CANONICAL_TEXT_DOCUMENT(0x01), /** * Standalone signature. @@ -40,7 +36,7 @@ public enum SignatureType { * binary document. Note that it doesn't make sense to have a V3 * standalone signature. */ - STANDALONE(PGPSignature.STAND_ALONE), + STANDALONE(0x02), /** * Generic certification of a User ID and Public-Key packet. @@ -48,28 +44,28 @@ public enum SignatureType { * assertion as to how well the certifier has checked that the owner * of the key is in fact the person described by the User ID. */ - GENERIC_CERTIFICATION(PGPSignature.DEFAULT_CERTIFICATION), + GENERIC_CERTIFICATION(0x10), /** * Persona certification of a User ID and Public-Key packet. * The issuer of this certification has not done any verification of * the claim that the owner of this key is the User ID specified. */ - NO_CERTIFICATION(PGPSignature.NO_CERTIFICATION), + NO_CERTIFICATION(0x11), /** * Casual certification of a User ID and Public-Key packet. * The issuer of this certification has done some casual * verification of the claim of identity. */ - CASUAL_CERTIFICATION(PGPSignature.CASUAL_CERTIFICATION), + CASUAL_CERTIFICATION(0x12), /** * Positive certification of a User ID and Public-Key packet. * The issuer of this certification has done substantial * verification of the claim of identity. */ - POSITIVE_CERTIFICATION(PGPSignature.POSITIVE_CERTIFICATION), + POSITIVE_CERTIFICATION(0x13), /** * Subkey Binding Signature. @@ -78,20 +74,20 @@ public enum SignatureType { * directly on the primary key and subkey, and not on any User ID or * other packets. A signature that binds a signing subkey MUST have * an Embedded Signature subpacket in this binding signature that - * contains a {@link #PRIMARYKEY_BINDING} signature made by the + * contains a [#PRIMARYKEY_BINDING] signature made by the * signing subkey on the primary key and subkey. */ - SUBKEY_BINDING(PGPSignature.SUBKEY_BINDING), + SUBKEY_BINDING(0x18), /** * Primary Key Binding Signature * This signature is a statement by a signing subkey, indicating * that it is owned by the primary key and subkey. This signature - * is calculated the same way as a {@link #SUBKEY_BINDING} signature: + * is calculated the same way as a [#SUBKEY_BINDING] signature: * directly on the primary key and subkey, and not on any User ID or * other packets. */ - PRIMARYKEY_BINDING(PGPSignature.PRIMARYKEY_BINDING), + PRIMARYKEY_BINDING(0x19), /** * Signature directly on a key @@ -103,7 +99,7 @@ public enum SignatureType { * about the key itself, rather than the binding between a key and a * name. */ - DIRECT_KEY(PGPSignature.DIRECT_KEY), + DIRECT_KEY(0x1f), /** * Key revocation signature @@ -112,7 +108,7 @@ public enum SignatureType { * key being revoked, or by an authorized revocation key, should be * considered valid revocation signatures. */ - KEY_REVOCATION(PGPSignature.KEY_REVOCATION), + KEY_REVOCATION(0x20), /** * Subkey revocation signature @@ -122,26 +118,26 @@ public enum SignatureType { * by an authorized revocation key, should be considered valid * revocation signatures. */ - SUBKEY_REVOCATION(PGPSignature.SUBKEY_REVOCATION), + SUBKEY_REVOCATION(0x28), /** * Certification revocation signature * This signature revokes an earlier User ID certification signature - * (signature class 0x10 through 0x13) or signature {@link #DIRECT_KEY}. + * (signature class 0x10 through 0x13) or signature [#DIRECT_KEY]. * It should be issued by the same key that issued the * revoked signature or an authorized revocation key. The signature * is computed over the same data as the certificate that it * revokes, and should have a later creation date than that * certificate. */ - CERTIFICATION_REVOCATION(PGPSignature.CERTIFICATION_REVOCATION), + CERTIFICATION_REVOCATION(0x30), /** * Timestamp signature. * This signature is only meaningful for the timestamp contained in * it. */ - TIMESTAMP(PGPSignature.TIMESTAMP), + TIMESTAMP(0x40), /** * Third-Party Confirmation signature. @@ -156,70 +152,77 @@ public enum SignatureType { THIRD_PARTY_CONFIRMATION(0x50) ; - private static final Map map = new ConcurrentHashMap<>(); - static { - for (SignatureType sigType : SignatureType.values()) { - map.put(sigType.getCode(), sigType); + companion object { + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + */ + @JvmStatic + fun fromCode(code: Int): SignatureType? { + return values().firstOrNull { + it.code == code + } + } + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + * @throws NoSuchElementException in case of an unmatched signature type code + */ + @JvmStatic + fun requireFromCode(code: Int): SignatureType { + return fromCode(code) ?: + throw NoSuchElementException("Signature type 0x${Integer.toHexString(code)} appears to be invalid.") + } + + /** + * Convert a numerical id into a [SignatureType]. + * + * @param code numeric id + * @return signature type enum + * @throws IllegalArgumentException in case of an unmatched signature type code + */ + @JvmStatic + @Deprecated("Deprecated in favor of requireFromCode", + ReplaceWith("requireFromCode")) + fun valueOf(code: Int): SignatureType { + try { + return requireFromCode(code) + } catch (e: NoSuchElementException) { + throw IllegalArgumentException(e.message) + } + } + + @JvmStatic + fun isRevocationSignature(signatureType: Int): Boolean { + return isRevocationSignature(valueOf(signatureType)) + } + + @JvmStatic + fun isRevocationSignature(signatureType: SignatureType): Boolean { + return when (signatureType) { + BINARY_DOCUMENT, + CANONICAL_TEXT_DOCUMENT, + STANDALONE, + GENERIC_CERTIFICATION, + NO_CERTIFICATION, + CASUAL_CERTIFICATION, + POSITIVE_CERTIFICATION, + SUBKEY_BINDING, + PRIMARYKEY_BINDING, + DIRECT_KEY, + TIMESTAMP, + THIRD_PARTY_CONFIRMATION -> false + KEY_REVOCATION, + SUBKEY_REVOCATION, + CERTIFICATION_REVOCATION -> true + else -> throw IllegalArgumentException("Unknown signature type: $signatureType") + } } } - - /** - * Convert a numerical id into a {@link SignatureType}. - * - * @param code numeric id - * @return signature type enum - * @throws IllegalArgumentException in case of an unmatched signature type code - */ - @Nonnull - public static SignatureType valueOf(int code) { - SignatureType type = map.get(code); - if (type != null) { - return type; - } - throw new IllegalArgumentException("Signature type 0x" + Integer.toHexString(code) + " appears to be invalid."); - } - - private final int code; - - SignatureType(int code) { - this.code = code; - } - - /** - * Return the numeric id of the signature type enum. - * - * @return numeric id - */ - public int getCode() { - return code; - } - - public static boolean isRevocationSignature(int signatureType) { - return isRevocationSignature(SignatureType.valueOf(signatureType)); - } - - public static boolean isRevocationSignature(SignatureType signatureType) { - switch (signatureType) { - case BINARY_DOCUMENT: - case CANONICAL_TEXT_DOCUMENT: - case STANDALONE: - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - case SUBKEY_BINDING: - case PRIMARYKEY_BINDING: - case DIRECT_KEY: - case TIMESTAMP: - case THIRD_PARTY_CONFIRMATION: - return false; - case KEY_REVOCATION: - case SUBKEY_REVOCATION: - case CERTIFICATION_REVOCATION: - return true; - default: - throw new IllegalArgumentException("Unknown signature type: " + signatureType); - } - } - -} +} \ No newline at end of file From 5da58904b65ab84cb6969fae7c63262e556922be Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:06:29 +0200 Subject: [PATCH 056/351] Kotlin conversion: StreamEncoding --- .../pgpainless/algorithm/StreamEncoding.java | 101 ------------------ .../pgpainless/algorithm/StreamEncoding.kt | 71 ++++++++++++ 2 files changed, 71 insertions(+), 101 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java deleted file mode 100644 index b0617bbb..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.openpgp.PGPLiteralData; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Enumeration of possible encoding formats of the content of the literal data packet. - * - * @see RFC4880: Literal Data Packet - */ -public enum StreamEncoding { - - /** - * The Literal packet contains binary data. - */ - BINARY(PGPLiteralData.BINARY), - - /** - * The Literal packet contains text data, and thus may need line ends converted to local form, or other - * text-mode changes. - */ - TEXT(PGPLiteralData.TEXT), - - /** - * Indication that the implementation believes that the literal data contains UTF-8 text. - */ - UTF8(PGPLiteralData.UTF8), - - /** - * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. - * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). - * Both of these local modes are deprecated. - */ - @Deprecated - LOCAL('l'), - ; - - private final char code; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (StreamEncoding f : StreamEncoding.values()) { - MAP.put(f.code, f); - } - // RFC 1991 [RFC1991] incorrectly stated local mode flag as '1', see doc of LOCAL. - MAP.put('1', LOCAL); - } - - StreamEncoding(char code) { - this.code = code; - } - - /** - * Return the code identifier of the encoding. - * - * @return identifier - */ - public char getCode() { - return code; - } - - /** - * Return the {@link StreamEncoding} corresponding to the provided code identifier. - * If no matching encoding is found, return null. - * - * @param code identifier - * @return encoding enum - */ - @Nullable - public static StreamEncoding fromCode(int code) { - return MAP.get((char) code); - } - - /** - * Return the {@link StreamEncoding} corresponding to the provided code identifier. - * If no matching encoding is found, throw a {@link NoSuchElementException}. - * - * @param code identifier - * @return encoding enum - * - * @throws NoSuchElementException in case of an unmatched identifier - */ - @Nonnull - public static StreamEncoding requireFromCode(int code) { - StreamEncoding encoding = fromCode(code); - if (encoding == null) { - throw new NoSuchElementException("No StreamEncoding found for code " + code); - } - return encoding; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt new file mode 100644 index 00000000..391797b1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible encoding formats of the content of the literal data packet. + * + * See [RFC4880: Literal Data Packet](https://tools.ietf.org/html/rfc4880#section-5.9) + */ +enum class StreamEncoding(val code: Char) { + + /** + * The Literal packet contains binary data. + */ + BINARY('b'), + + /** + * The Literal packet contains text data, and thus may need line ends converted to local form, or other + * text-mode changes. + */ + TEXT('t'), + + /** + * Indication that the implementation believes that the literal data contains UTF-8 text. + */ + UTF8('u'), + + /** + * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. + * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). + * Both of these local modes are deprecated. + */ + @Deprecated("LOCAL is deprecated.") + LOCAL('l'), + ; + + + companion object { + /** + * Return the [StreamEncoding] corresponding to the provided code identifier. + * If no matching encoding is found, return null. + * + * @param code identifier + * @return encoding enum + */ + @JvmStatic + fun fromCode(code: Int): StreamEncoding? { + return values().firstOrNull { + it.code == code.toChar() + } ?: if (code == 1) return LOCAL else null + } + + /** + * Return the [StreamEncoding] corresponding to the provided code identifier. + * If no matching encoding is found, throw a [NoSuchElementException]. + * + * @param code identifier + * @return encoding enum + * + * @throws NoSuchElementException in case of an unmatched identifier + */ + @JvmStatic + fun requireFromCode(code: Int): StreamEncoding { + return fromCode(code) ?: + throw NoSuchElementException("No StreamEncoding found for code $code") + } + } + +} \ No newline at end of file From 3a62b391979abfc050e6311ea54a67afe439bebd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:06:51 +0200 Subject: [PATCH 057/351] Kotlin conversion: SymmetricKeyAlgorithm --- .../algorithm/SymmetricKeyAlgorithm.java | 150 ------------------ .../algorithm/SymmetricKeyAlgorithm.kt | 120 ++++++++++++++ 2 files changed, 120 insertions(+), 150 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java deleted file mode 100644 index e04f21a5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.java +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Enumeration of possible symmetric encryption algorithms. - * - * @see RFC4880: Symmetric-Key Algorithms - */ -public enum SymmetricKeyAlgorithm { - - /** - * Plaintext or unencrypted data. - */ - NULL (SymmetricKeyAlgorithmTags.NULL), - - /** - * IDEA is deprecated. - * @deprecated use a different algorithm. - */ - @Deprecated - IDEA (SymmetricKeyAlgorithmTags.IDEA), - - /** - * TripleDES (DES-EDE - 168 bit key derived from 192). - */ - TRIPLE_DES (SymmetricKeyAlgorithmTags.TRIPLE_DES), - - /** - * CAST5 (128-bit key, as per RFC2144). - */ - CAST5 (SymmetricKeyAlgorithmTags.CAST5), - - /** - * Blowfish (128-bit key, 16 rounds). - */ - BLOWFISH (SymmetricKeyAlgorithmTags.BLOWFISH), - - /** - * Reserved in RFC4880. - * SAFER-SK128 (13 rounds) - */ - SAFER (SymmetricKeyAlgorithmTags.SAFER), - - /** - * Reserved in RFC4880. - * Reserved for DES/SK - */ - DES (SymmetricKeyAlgorithmTags.DES), - - /** - * AES with 128-bit key. - */ - AES_128 (SymmetricKeyAlgorithmTags.AES_128), - - /** - * AES with 192-bit key. - */ - AES_192 (SymmetricKeyAlgorithmTags.AES_192), - - /** - * AES with 256-bit key. - */ - AES_256 (SymmetricKeyAlgorithmTags.AES_256), - - /** - * Twofish with 256-bit key. - */ - TWOFISH (SymmetricKeyAlgorithmTags.TWOFISH), - - /** - * Reserved for Camellia with 128-bit key. - */ - CAMELLIA_128 (SymmetricKeyAlgorithmTags.CAMELLIA_128), - - /** - * Reserved for Camellia with 192-bit key. - */ - CAMELLIA_192 (SymmetricKeyAlgorithmTags.CAMELLIA_192), - - /** - * Reserved for Camellia with 256-bit key. - */ - CAMELLIA_256 (SymmetricKeyAlgorithmTags.CAMELLIA_256), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - - static { - for (SymmetricKeyAlgorithm s : SymmetricKeyAlgorithm.values()) { - MAP.put(s.algorithmId, s); - } - } - - /** - * Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id. - * If an invalid id is provided, null is returned. - * - * @param id numeric algorithm id - * @return symmetric key algorithm enum - */ - @Nullable - public static SymmetricKeyAlgorithm fromId(int id) { - return MAP.get(id); - } - - /** - * Return the {@link SymmetricKeyAlgorithm} enum that corresponds to the provided numeric id. - * If an invalid id is provided, throw a {@link NoSuchElementException}. - * - * @param id numeric algorithm id - * @return symmetric key algorithm enum - * - * @throws NoSuchElementException if an unmatched id is provided - */ - @Nonnull - public static SymmetricKeyAlgorithm requireFromId(int id) { - SymmetricKeyAlgorithm algorithm = fromId(id); - if (algorithm == null) { - throw new NoSuchElementException("No SymmetricKeyAlgorithm found for id " + id); - } - return algorithm; - } - - private final int algorithmId; - - SymmetricKeyAlgorithm(int algorithmId) { - this.algorithmId = algorithmId; - } - - /** - * Return the numeric algorithm id of the enum. - * - * @return numeric id - */ - public int getAlgorithmId() { - return algorithmId; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt new file mode 100644 index 00000000..ae72ff05 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Enumeration of possible symmetric encryption algorithms. + * + * See [RFC4880: Symmetric-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.2) + */ +enum class SymmetricKeyAlgorithm(val algorithmId: Int) { + + /** + * Plaintext or unencrypted data. + */ + NULL (0), + + /** + * IDEA is deprecated. + * @deprecated use a different algorithm. + */ + @Deprecated("IDEA is deprecated.") + IDEA (1), + + /** + * TripleDES (DES-EDE - 168 bit key derived from 192). + */ + TRIPLE_DES (2), + + /** + * CAST5 (128-bit key, as per RFC2144). + */ + CAST5 (3), + + /** + * Blowfish (128-bit key, 16 rounds). + */ + BLOWFISH (4), + + /** + * Reserved in RFC4880. + * SAFER-SK128 (13 rounds) + */ + SAFER (5), + + /** + * Reserved in RFC4880. + * Reserved for DES/SK + */ + DES (6), + + /** + * AES with 128-bit key. + */ + AES_128 (7), + + /** + * AES with 192-bit key. + */ + AES_192 (8), + + /** + * AES with 256-bit key. + */ + AES_256 (9), + + /** + * Twofish with 256-bit key. + */ + TWOFISH (10), + + /** + * Reserved for Camellia with 128-bit key. + */ + CAMELLIA_128 (11), + + /** + * Reserved for Camellia with 192-bit key. + */ + CAMELLIA_192 (12), + + /** + * Reserved for Camellia with 256-bit key. + */ + CAMELLIA_256 (13), + ; + + companion object { + + /** + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. + * If an invalid id is provided, null is returned. + * + * @param id numeric algorithm id + * @return symmetric key algorithm enum + */ + @JvmStatic + fun fromId(id: Int): SymmetricKeyAlgorithm? { + return values().firstOrNull { + it.algorithmId == id + } + } + + /** + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. + * If an invalid id is provided, throw a [NoSuchElementException]. + * + * @param id numeric algorithm id + * @return symmetric key algorithm enum + * + * @throws NoSuchElementException if an unmatched id is provided + */ + @JvmStatic + fun requireFromId(id: Int): SymmetricKeyAlgorithm { + return fromId(id) ?: + throw NoSuchElementException("No SymmetricKeyAlgorithm found for id $id") + } + } +} \ No newline at end of file From 382817dc5fa2f36273771662503e9e303261901f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:07:02 +0200 Subject: [PATCH 058/351] Kotlin conversion: Trustworthiness --- .../pgpainless/algorithm/Trustworthiness.java | 188 ------------------ .../pgpainless/algorithm/Trustworthiness.kt | 140 +++++++++++++ 2 files changed, 140 insertions(+), 188 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java deleted file mode 100644 index 26e66e9c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.java +++ /dev/null @@ -1,188 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -/** - * Facade class for {@link org.bouncycastle.bcpg.sig.TrustSignature}. - * A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act - * as a trusted introducer. - */ -public class Trustworthiness { - - private final int amount; - private final int depth; - - public static final int THRESHOLD_FULLY_CONVINCED = 120; // greater or equal is fully trusted - public static final int MARGINALLY_CONVINCED = 60; // default value for marginally convinced - public static final int NOT_TRUSTED = 0; // 0 is not trusted - - public Trustworthiness(int amount, int depth) { - this.amount = capAmount(amount); - this.depth = capDepth(depth); - } - - /** - * Get the trust amount. - * This value means how confident the issuer of the signature is in validity of the binding. - * - * @return trust amount - */ - public int getAmount() { - return amount; - } - - /** - * Get the depth of the trust signature. - * This value controls, whether the certificate can act as a trusted introducer. - * - * @return depth - */ - public int getDepth() { - return depth; - } - - /** - * Returns true, if the trust amount is equal to 0. - * This means the key is not trusted. - * - * Otherwise return false - * @return true if untrusted - */ - public boolean isNotTrusted() { - return getAmount() == NOT_TRUSTED; - } - - /** - * Return true if the certificate is at least marginally trusted. - * That is the case, if the trust amount is greater than 0. - * - * @return true if the cert is at least marginally trusted - */ - public boolean isMarginallyTrusted() { - return getAmount() > NOT_TRUSTED; - } - - /** - * Return true if the certificate is fully trusted. That is the case if the trust amount is - * greater than or equal to 120. - * - * @return true if the cert is fully trusted - */ - public boolean isFullyTrusted() { - return getAmount() >= THRESHOLD_FULLY_CONVINCED; - } - - /** - * Return true, if the cert is an introducer. That is the case if the depth is greater 0. - * - * @return true if introducer - */ - public boolean isIntroducer() { - return getDepth() >= 1; - } - - /** - * Return true, if the certified cert can introduce certificates with trust depth of
otherDepth
. - * - * @param otherDepth other certifications trust depth - * @return true if the cert can introduce the other - */ - public boolean canIntroduce(int otherDepth) { - return getDepth() > otherDepth; - } - - /** - * Return true, if the certified cert can introduce certificates with the given
other
trust depth. - * - * @param other other certificates trust depth - * @return true if the cert can introduce the other - */ - public boolean canIntroduce(Trustworthiness other) { - return canIntroduce(other.getDepth()); - } - - /** - * This means that we are fully convinced of the trustworthiness of the key. - * - * @return builder - */ - public static Builder fullyTrusted() { - return new Builder(THRESHOLD_FULLY_CONVINCED); - } - - /** - * This means that we are marginally (partially) convinced of the trustworthiness of the key. - * - * @return builder - */ - public static Builder marginallyTrusted() { - return new Builder(MARGINALLY_CONVINCED); - } - - /** - * This means that we do not trust the key. - * Can be used to overwrite previous trust. - * - * @return builder - */ - public static Builder untrusted() { - return new Builder(NOT_TRUSTED); - } - - public static final class Builder { - - private final int amount; - - private Builder(int amount) { - this.amount = amount; - } - - /** - * The key is a trusted introducer (depth 1). - * Certifications made by this key are considered trustworthy. - * - * @return trust - */ - public Trustworthiness introducer() { - return new Trustworthiness(amount, 1); - } - - /** - * The key is a meta introducer (depth 2). - * This key can introduce trusted introducers of depth 1. - * - * @return trust - */ - public Trustworthiness metaIntroducer() { - return new Trustworthiness(amount, 2); - } - - /** - * The key is a meta introducer of depth
n
. - * This key can introduce meta introducers of depth
n - 1
. - * - * @param n depth - * @return trust - */ - public Trustworthiness metaIntroducerOfDepth(int n) { - return new Trustworthiness(amount, n); - } - } - - private static int capAmount(int amount) { - if (amount < 0 || amount > 255) { - throw new IllegalArgumentException("Trust amount MUST be a value between 0 and 255"); - } - return amount; - } - - private static int capDepth(int depth) { - if (depth < 0 || depth > 255) { - throw new IllegalArgumentException("Trust depth MUST be a value between 0 and 255"); - } - return depth; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt new file mode 100644 index 00000000..b1a1b7dd --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm + +/** + * Facade class for [org.bouncycastle.bcpg.sig.TrustSignature]. + * A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act + * as a trusted introducer. + */ +class Trustworthiness(amount: Int, depth: Int) { + val depth = capDepth(depth) + val amount = capAmount(amount) + + /** + * Returns true, if the trust amount is equal to 0. + * This means the key is not trusted. + * + * Otherwise return false + * @return true if untrusted + */ + fun isNotTrusted() = amount == NOT_TRUSTED + + /** + * Return true if the certificate is at least marginally trusted. + * That is the case, if the trust amount is greater than 0. + * + * @return true if the cert is at least marginally trusted + */ + fun isMarginallyTrusted() = amount > NOT_TRUSTED + + /** + * Return true if the certificate is fully trusted. That is the case if the trust amount is + * greater than or equal to 120. + * + * @return true if the cert is fully trusted + */ + fun isFullyTrusted() = amount >= THRESHOLD_FULLY_CONVINCED + + /** + * Return true, if the cert is an introducer. That is the case if the depth is greater 0. + * + * @return true if introducer + */ + fun isIntroducer() = depth >= 1 + + /** + * Return true, if the certified cert can introduce certificates with trust depth of
otherDepth
. + * + * @param otherDepth other certifications trust depth + * @return true if the cert can introduce the other + */ + fun canIntroduce(otherDepth: Int) = depth > otherDepth + + /** + * Return true, if the certified cert can introduce certificates with the given
other
trust depth. + * + * @param other other certificates trust depth + * @return true if the cert can introduce the other + */ + fun canIntroduce(other: Trustworthiness) = canIntroduce(other.depth) + + companion object { + + const val THRESHOLD_FULLY_CONVINCED = 120 // greater or equal is fully trusted + const val MARGINALLY_CONVINCED = 60 // default value for marginally convinced + const val NOT_TRUSTED = 0 // 0 is not trusted + + /** + * This means that we are fully convinced of the trustworthiness of the key. + * + * @return builder + */ + @JvmStatic + fun fullyTrusted() = Builder(THRESHOLD_FULLY_CONVINCED) + + /** + * This means that we are marginally (partially) convinced of the trustworthiness of the key. + * + * @return builder + */ + @JvmStatic + fun marginallyTrusted() = Builder(MARGINALLY_CONVINCED) + + /** + * This means that we do not trust the key. + * Can be used to overwrite previous trust. + * + * @return builder + */ + @JvmStatic + fun untrusted() = Builder(NOT_TRUSTED) + + @JvmStatic + private fun capAmount(amount: Int): Int { + if (amount !in 0..255) { + throw IllegalArgumentException("Trust amount MUST be a value between 0 and 255") + } + return amount + } + + @JvmStatic + private fun capDepth(depth: Int): Int { + if (depth !in 0..255) { + throw IllegalArgumentException("Trust depth MUST be a value between 0 and 255") + } + return depth + } + } + + class Builder(val amount: Int) { + + /** + * The key is a trusted introducer (depth 1). + * Certifications made by this key are considered trustworthy. + * + * @return trust + */ + fun introducer() = Trustworthiness(amount, 1) + + /** + * The key is a meta introducer (depth 2). + * This key can introduce trusted introducers of depth 1. + * + * @return trust + */ + fun metaIntroducer() = Trustworthiness(amount, 2) + + /** + * The key is a meta introducer of depth
n
. + * This key can introduce meta introducers of depth
n - 1
. + * + * @param n depth + * @return trust + */ + fun metaIntroducerOfDepth(d: Int) = Trustworthiness(amount, d) + } + +} \ No newline at end of file From b5d8126e48dfd2a6a9f13ae546637fa28aff212e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:18:19 +0200 Subject: [PATCH 059/351] Kotlin conversion: AlgorithmSuite --- .../pgpainless/algorithm/AlgorithmSuite.java | 63 ------------------- .../pgpainless/algorithm/AlgorithmSuite.kt | 41 ++++++++++++ 2 files changed, 41 insertions(+), 63 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java deleted file mode 100644 index e155e367..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * The {@link AlgorithmSuite} class is consulted when new OpenPGP keys are being generated to set - * preferred algorithms on the key. - */ -public class AlgorithmSuite { - - private static final AlgorithmSuite defaultAlgorithmSuite = new AlgorithmSuite( - Arrays.asList( - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128), - Arrays.asList( - HashAlgorithm.SHA512, - HashAlgorithm.SHA384, - HashAlgorithm.SHA256, - HashAlgorithm.SHA224), - Arrays.asList( - CompressionAlgorithm.ZLIB, - CompressionAlgorithm.BZIP2, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.UNCOMPRESSED) - ); - - private final Set symmetricKeyAlgorithms; - private final Set hashAlgorithms; - private final Set compressionAlgorithms; - - public AlgorithmSuite(List symmetricKeyAlgorithms, - List hashAlgorithms, - List compressionAlgorithms) { - this.symmetricKeyAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(symmetricKeyAlgorithms)); - this.hashAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(hashAlgorithms)); - this.compressionAlgorithms = Collections.unmodifiableSet(new LinkedHashSet<>(compressionAlgorithms)); - } - - public Set getSymmetricKeyAlgorithms() { - return new LinkedHashSet<>(symmetricKeyAlgorithms); - } - - public Set getHashAlgorithms() { - return new LinkedHashSet<>(hashAlgorithms); - } - - public Set getCompressionAlgorithms() { - return new LinkedHashSet<>(compressionAlgorithms); - } - - public static AlgorithmSuite getDefaultAlgorithmSuite() { - return defaultAlgorithmSuite; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt new file mode 100644 index 00000000..9bb2182e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt @@ -0,0 +1,41 @@ +package org.pgpainless.algorithm + +class AlgorithmSuite( + symmetricKeyAlgorithms: List, + hashAlgorithms: List, + compressionAlgorithms: List) { + + val symmetricKeyAlgorithms: Set = symmetricKeyAlgorithms.toSet() + val hashAlgorithms: Set = hashAlgorithms.toSet() + val compressionAlgorithms: Set = compressionAlgorithms.toSet() + + companion object { + + @JvmStatic + val defaultSymmetricKeyAlgorithms = listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128) + + @JvmStatic + val defaultHashAlgorithms = listOf( + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224) + + @JvmStatic + val defaultCompressionAlgorithms = listOf( + CompressionAlgorithm.ZLIB, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.UNCOMPRESSED) + + @JvmStatic + val defaultAlgorithmSuite = AlgorithmSuite( + defaultSymmetricKeyAlgorithms, + defaultHashAlgorithms, + defaultCompressionAlgorithms) + } + +} \ No newline at end of file From fb235d78109e2e0fc2c4ddd6e75efcbd3dd3613e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 17:34:52 +0200 Subject: [PATCH 060/351] Kotlin conversion: HashAlgorithmNegotiator --- .../negotiation/HashAlgorithmNegotiator.java | 70 ------------------ .../negotiation/HashAlgorithmNegotiator.kt | 71 +++++++++++++++++++ 2 files changed, 71 insertions(+), 70 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java deleted file mode 100644 index 18fe53f9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm.negotiation; - -import java.util.Set; - -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.policy.Policy; - -/** - * Interface for a class that negotiates {@link HashAlgorithm HashAlgorithms}. - * - * You can provide your own implementation using custom logic by implementing the - * {@link #negotiateHashAlgorithm(Set)} method. - */ -public interface HashAlgorithmNegotiator { - - /** - * Pick one {@link HashAlgorithm} from the ordered set of acceptable algorithms. - * - * @param orderedHashAlgorithmPreferencesSet hash algorithm preferences - * @return picked algorithms - */ - HashAlgorithm negotiateHashAlgorithm(Set orderedHashAlgorithmPreferencesSet); - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for non-revocation signatures - * based on the given {@link Policy}. - * - * @param policy algorithm policy - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateSignatureHashAlgorithm(Policy policy) { - return negotiateByPolicy(policy.getSignatureHashAlgorithmPolicy()); - } - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} used for revocation signatures - * based on the given {@link Policy}. - * - * @param policy algorithm policy - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateRevocationSignatureAlgorithm(Policy policy) { - return negotiateByPolicy(policy.getRevocationSignatureHashAlgorithmPolicy()); - } - - /** - * Return an instance that negotiates {@link HashAlgorithm HashAlgorithms} based on the given - * {@link Policy.HashAlgorithmPolicy}. - * - * @param hashAlgorithmPolicy algorithm policy for hash algorithms - * @return negotiator - */ - static HashAlgorithmNegotiator negotiateByPolicy(Policy.HashAlgorithmPolicy hashAlgorithmPolicy) { - return new HashAlgorithmNegotiator() { - @Override - public HashAlgorithm negotiateHashAlgorithm(Set orderedPreferencesSet) { - for (HashAlgorithm preference : orderedPreferencesSet) { - if (hashAlgorithmPolicy.isAcceptable(preference)) { - return preference; - } - } - return hashAlgorithmPolicy.defaultHashAlgorithm(); - } - }; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt new file mode 100644 index 00000000..98cbe522 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.policy.Policy + +/** + * Interface for a class that negotiates [HashAlgorithms][HashAlgorithm]. + * + * You can provide your own implementation using custom logic by implementing the + * [negotiateHashAlgorithm(Set)] method. + */ +interface HashAlgorithmNegotiator { + + /** + * Pick one [HashAlgorithm] from the ordered set of acceptable algorithms. + * + * @param orderedPrefs hash algorithm preferences + * @return picked algorithms + */ + fun negotiateHashAlgorithm(orderedPrefs: Set): HashAlgorithm + + companion object { + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for non-revocation signatures + * based on the given [Policy]. + * + * @param policy algorithm policy + * @return negotiator + */ + @JvmStatic + fun negotiateSignatureHashAlgorithm(policy: Policy): HashAlgorithmNegotiator { + return negotiateByPolicy(policy.signatureHashAlgorithmPolicy) + } + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for revocation signatures + * based on the given [Policy]. + * + * @param policy algorithm policy + * @return negotiator + */ + @JvmStatic + fun negotiateRevocationSignatureAlgorithm(policy: Policy): HashAlgorithmNegotiator { + return negotiateByPolicy(policy.revocationSignatureHashAlgorithmPolicy) + } + + /** + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] based on the given + * [Policy.HashAlgorithmPolicy]. + * + * @param hashAlgorithmPolicy algorithm policy for hash algorithms + * @return negotiator + */ + @JvmStatic + fun negotiateByPolicy(hashAlgorithmPolicy: Policy.HashAlgorithmPolicy): HashAlgorithmNegotiator { + return object: HashAlgorithmNegotiator { + override fun negotiateHashAlgorithm(orderedPrefs: Set): HashAlgorithm { + return orderedPrefs.firstOrNull { + hashAlgorithmPolicy.isAcceptable(it) + } ?: hashAlgorithmPolicy.defaultHashAlgorithm() + } + + } + } + } +} \ No newline at end of file From cf7a7f3aca8c2c071c283b26e6dd5e5b1d320d2f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 18:14:09 +0200 Subject: [PATCH 061/351] Kotlin conversion: SymmetricKeyAlgorithmNegotiator --- .../SymmetricKeyAlgorithmNegotiator.java | 105 ------------------ .../SymmetricKeyAlgorithmNegotiator.kt | 77 +++++++++++++ 2 files changed, 77 insertions(+), 105 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt 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 deleted file mode 100644 index de8ced24..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.java +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.algorithm.negotiation; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -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. - * If the override is non-null, it will be returned instead of performing an actual negotiation. - * Otherwise, the list of ordered sets containing the preferences of different recipient keys will be - * used to determine a suitable 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); - - /** - * Return an instance that negotiates a {@link SymmetricKeyAlgorithm} by selecting the most popular acceptable - * algorithm from the list of preferences. - * - * This negotiator has the best chances to select an algorithm which is understood by all recipients. - * - * @return negotiator that selects by popularity - */ - 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; - } - - // Count score (occurrences) of each algorithm - 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); - } - } - } - - // Pivot the score map - Map> byScore = new HashMap<>(); - for (SymmetricKeyAlgorithm algorithm : supportWeight.keySet()) { - int score = supportWeight.get(algorithm); - List withSameScore = byScore.get(score); - if (withSameScore == null) { - withSameScore = new ArrayList<>(); - byScore.put(score, withSameScore); - } - withSameScore.add(algorithm); - } - - List scores = new ArrayList<>(byScore.keySet()); - - // Sort map and iterate from highest to lowest score - Collections.sort(scores); - for (int i = scores.size() - 1; i >= 0; i--) { - int score = scores.get(i); - List withSameScore = byScore.get(score); - // Select best algorithm - SymmetricKeyAlgorithm best = policy.selectBest(withSameScore); - if (best != null) { - return best; - } - } - - // If no algorithm is acceptable, choose fallback - return policy.getDefaultSymmetricKeyAlgorithm(); - } - }; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt new file mode 100644 index 00000000..1a6dfe7f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.policy.Policy +import java.lang.IllegalArgumentException + +interface SymmetricKeyAlgorithmNegotiator { + + /** + * Negotiate a symmetric encryption algorithm. + * If the override is non-null, it will be returned instead of performing an actual negotiation. + * Otherwise, the list of ordered sets containing the preferences of different recipient keys will be + * used to determine a suitable 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 + */ + fun negotiate(policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List>): SymmetricKeyAlgorithm + + companion object { + @JvmStatic + fun byPopularity(): SymmetricKeyAlgorithmNegotiator { + return object: SymmetricKeyAlgorithmNegotiator { + override fun negotiate( + policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List>): + SymmetricKeyAlgorithm { + if (override == SymmetricKeyAlgorithm.NULL) { + throw IllegalArgumentException("Algorithm override cannot be NULL (plaintext).") + } + + if (override != null) { + return override + } + + // algorithm to #occurrences + val supportWeight = buildMap { + keyPreferences.forEach { keyPreference -> + keyPreference.forEach { pref -> + put(pref, getOrDefault(pref, 0) as Int + 1) + } + } + } + + // Pivot map and sort by popularity ascending + // score to list(algo) + val byScore = supportWeight.toList() + .map { e -> e.second to e.first } + .groupBy { e -> e.first } + .map { e -> e.key to e.value.map { it.second }.toList() } + .associate { e -> e } + .toSortedMap() + + // iterate in reverse over algorithms + for (e in byScore.entries.reversed()) { + val best = policy.selectBest(e.value) + if (best != null) { + return best + } + } + + return policy.defaultSymmetricKeyAlgorithm + } + + } + } + } +} \ No newline at end of file From 5a5b604411e123a4b5e7a0ba48a48d58a802fcd8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 18:14:17 +0200 Subject: [PATCH 062/351] Add missing license header --- .../src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt index 9bb2182e..0e4997fc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.algorithm class AlgorithmSuite( From b91e19fc39f8d2ec3f92aa5924740472eca26f04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Aug 2023 18:36:57 +0200 Subject: [PATCH 063/351] Kotlin conversion: PGPainless --- .../main/java/org/pgpainless/PGPainless.java | 238 ------------------ .../main/java/org/pgpainless/PGPainless.kt | 171 +++++++++++++ 2 files changed, 171 insertions(+), 238 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/PGPainless.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java deleted file mode 100644 index 3da54177..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.java +++ /dev/null @@ -1,238 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.decryption_verification.DecryptionBuilder; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.encryption_signing.EncryptionBuilder; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.key.certification.CertifyCertificate; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeyRingTemplates; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.parsing.KeyRingReader; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.util.ArmorUtils; - -public final class PGPainless { - - private PGPainless() { - - } - - /** - * Generate a fresh OpenPGP key ring from predefined templates. - * @return templates - */ - @Nonnull - public static KeyRingTemplates generateKeyRing() { - return new KeyRingTemplates(); - } - - /** - * Build a custom OpenPGP key ring. - * - * @return builder - */ - @Nonnull - public static KeyRingBuilder buildKeyRing() { - return new KeyRingBuilder(); - } - - /** - * Read an existing OpenPGP key ring. - * @return builder - */ - @Nonnull - public static KeyRingReader readKeyRing() { - return new KeyRingReader(); - } - - /** - * Extract a public key certificate from a secret key. - * - * @param secretKey secret key - * @return public key certificate - */ - @Nonnull - public static PGPPublicKeyRing extractCertificate(@Nonnull PGPSecretKeyRing secretKey) { - return KeyRingUtils.publicKeyRingFrom(secretKey); - } - - /** - * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together. - * - * @param originalCopy local, older copy of the cert - * @param updatedCopy updated, newer copy of the cert - * @return merged certificate - * @throws PGPException in case of an error - */ - @Nonnull - public static PGPPublicKeyRing mergeCertificate( - @Nonnull PGPPublicKeyRing originalCopy, - @Nonnull PGPPublicKeyRing updatedCopy) - throws PGPException { - return PGPPublicKeyRing.join(originalCopy, updatedCopy); - } - - /** - * Wrap a key or certificate in ASCII armor. - * - * @param key key or certificate - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String asciiArmor(@Nonnull PGPKeyRing key) - throws IOException { - if (key instanceof PGPSecretKeyRing) { - return ArmorUtils.toAsciiArmoredString((PGPSecretKeyRing) key); - } else { - return ArmorUtils.toAsciiArmoredString((PGPPublicKeyRing) key); - } - } - - /** - * Wrap the detached signature in ASCII armor. - * - * @param signature detached signature - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String asciiArmor(@Nonnull PGPSignature signature) - throws IOException { - return ArmorUtils.toAsciiArmoredString(signature); - } - - /** - * Wrap a key of certificate in ASCII armor and write the result into the given {@link OutputStream}. - * - * @param key key or certificate - * @param outputStream output stream - * - * @throws IOException in case of an error ion the {@link ArmoredOutputStream} - */ - public static void asciiArmor(@Nonnull PGPKeyRing key, @Nonnull OutputStream outputStream) - throws IOException { - ArmoredOutputStream armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream); - key.encode(armorOut); - armorOut.close(); - } - - /** - * Create an {@link EncryptionStream}, which can be used to encrypt and/or sign data using OpenPGP. - * - * @return builder - */ - @Nonnull - public static EncryptionBuilder encryptAndOrSign() { - return new EncryptionBuilder(); - } - - /** - * Create a {@link DecryptionStream}, which can be used to decrypt and/or verify data using OpenPGP. - * - * @return builder - */ - @Nonnull - public static DecryptionBuilder decryptAndOrVerify() { - return new DecryptionBuilder(); - } - - /** - * Make changes to a secret key. - * This method can be used to change key expiration dates and passphrases, or add/revoke subkeys. - *

- * After making the desired changes in the builder, the modified key ring can be extracted using {@link SecretKeyRingEditorInterface#done()}. - * - * @param secretKeys secret key ring - * @return builder - */ - @Nonnull - public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys) { - return modifyKeyRing(secretKeys, new Date()); - } - - /** - * Make changes to a secret key at the given reference time. - * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. - *

- * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. - * - * @param secretKeys secret key ring - * @param referenceTime reference time used as signature creation date - * @return builder - */ - @Nonnull - public static SecretKeyRingEditorInterface modifyKeyRing(@Nonnull PGPSecretKeyRing secretKeys, - @Nonnull Date referenceTime) { - return new SecretKeyRingEditor(secretKeys, referenceTime); - } - - /** - * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. - * This method can be used to determine expiration dates, key flags and other information about a key. - *

- * To evaluate a key at a given date (e.g. to determine if the key was allowed to create a certain signature) - * use {@link #inspectKeyRing(PGPKeyRing, Date)} instead. - * - * @param keyRing key ring - * @return access object - */ - @Nonnull - public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing) { - return new KeyRingInfo(keyRing); - } - - /** - * Quickly access information about a {@link org.bouncycastle.openpgp.PGPPublicKeyRing} / {@link PGPSecretKeyRing}. - * This method can be used to determine expiration dates, key flags and other information about a key at a specific time. - * - * @param keyRing key ring - * @param referenceTime date of inspection - * @return access object - */ - @Nonnull - public static KeyRingInfo inspectKeyRing(@Nonnull PGPKeyRing keyRing, @Nonnull Date referenceTime) { - return new KeyRingInfo(keyRing, referenceTime); - } - - /** - * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. - * - * @return policy - */ - @Nonnull - public static Policy getPolicy() { - return Policy.getInstance(); - } - - /** - * Create different kinds of signatures on other keys. - * - * @return builder - */ - @Nonnull - public static CertifyCertificate certify() { - return new CertifyCertificate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt new file mode 100644 index 00000000..a52b38ae --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt @@ -0,0 +1,171 @@ +package org.pgpainless + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.DecryptionBuilder +import org.pgpainless.encryption_signing.EncryptionBuilder +import org.pgpainless.key.certification.CertifyCertificate +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeyRingTemplates +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor +import org.pgpainless.key.parsing.KeyRingReader +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.util.ArmorUtils +import java.io.OutputStream +import java.util.* + +class PGPainless private constructor() { + + companion object { + + /** + * Generate a fresh OpenPGP key ring from predefined templates. + * @return templates + */ + @JvmStatic + fun generateKeyRing() = KeyRingTemplates() + + /** + * Build a custom OpenPGP key ring. + * + * @return builder + */ + @JvmStatic + fun buildKeyRing() = KeyRingBuilder() + + /** + * Read an existing OpenPGP key ring. + * @return builder + */ + @JvmStatic + fun readKeyRing() = KeyRingReader() + + /** + * Extract a public key certificate from a secret key. + * + * @param secretKey secret key + * @return public key certificate + */ + @JvmStatic + fun extractCertificate(secretKey: PGPSecretKeyRing) = + KeyRingUtils.publicKeyRingFrom(secretKey) + + /** + * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together. + * + * @param originalCopy local, older copy of the cert + * @param updatedCopy updated, newer copy of the cert + * @return merged certificate + * @throws PGPException in case of an error + */ + @JvmStatic + fun mergeCertificate(originalCopy: PGPPublicKeyRing, + updatedCopy: PGPPublicKeyRing) = + PGPPublicKeyRing.join(originalCopy, updatedCopy) + + /** + * Wrap a key or certificate in ASCII armor. + * + * @param key key or certificate + * @return ascii armored string + * + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(key: PGPKeyRing) = + if (key is PGPSecretKeyRing) + ArmorUtils.toAsciiArmoredString(key) + else + ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing) + + /** + * Wrap a key of certificate in ASCII armor and write the result into the given [OutputStream]. + * + * @param key key or certificate + * @param outputStream output stream + * + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(key: PGPKeyRing, outputStream: OutputStream) { + val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) + key.encode(armorOut) + armorOut.close() + } + + /** + * Wrap the detached signature in ASCII armor. + * + * @param signature detached signature + * @return ascii armored string + * + * @throws IOException in case of an error during the armoring process + */ + @JvmStatic + fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature) + + /** + * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using OpenPGP. + * + * @return builder + */ + @JvmStatic + fun encryptAndOrSign() = EncryptionBuilder() + + /** + * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using OpenPGP. + * + * @return builder + */ + @JvmStatic + fun decryptAndOrVerify() = DecryptionBuilder() + + /** + * Make changes to a secret key at the given reference time. + * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. + *

+ * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. + * + * @param secretKeys secret key ring + * @param referenceTime reference time used as signature creation date + * @return builder + */ + @JvmStatic + @JvmOverloads + fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) = + SecretKeyRingEditor(secretKey, referenceTime) + + /** + * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / [PGPSecretKeyRing]. + * This method can be used to determine expiration dates, key flags and other information about a key at a specific time. + * + * @param keyRing key ring + * @param referenceTime date of inspection + * @return access object + */ + @JvmStatic + @JvmOverloads + fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) = + KeyRingInfo(key, referenceTime) + + /** + * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. + * + * @return policy + */ + @JvmStatic + fun getPolicy() = Policy.getInstance() + + /** + * Create different kinds of signatures on other keys. + * + * @return builder + */ + @JvmStatic + fun certify() = CertifyCertificate() + } +} \ No newline at end of file From 075ca4baa3a2997b9519521a68040393e3ecee68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Aug 2023 14:31:50 +0200 Subject: [PATCH 064/351] Kotlin conversion: CertificateAuthenticity --- .../CertificateAuthenticity.java | 139 ------------------ .../authentication/CertificateAuthenticity.kt | 57 +++++++ 2 files changed, 57 insertions(+), 139 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java deleted file mode 100644 index ac059646..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.java +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.authentication; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; - -public class CertificateAuthenticity { - - private final String userId; - private final PGPPublicKeyRing certificate; - private final Map certificationChains = new HashMap<>(); - private final int targetAmount; - - public CertificateAuthenticity(@Nonnull PGPPublicKeyRing certificate, - @Nonnull String userId, - @Nonnull Map certificationChains, - int targetAmount) { - this.userId = userId; - this.certificate = certificate; - this.certificationChains.putAll(certificationChains); - this.targetAmount = targetAmount; - } - - @Nonnull - public String getUserId() { - return userId; - } - - @Nonnull - public PGPPublicKeyRing getCertificate() { - return certificate; - } - - public int getTotalTrustAmount() { - int total = 0; - for (int v : certificationChains.values()) { - total += v; - } - return total; - } - - /** - * Return the degree of authentication of the binding in percent. - * 100% means full authentication. - * Values smaller than 100% mean partial authentication. - * - * @return authenticity in percent - */ - public int getAuthenticityPercentage() { - return targetAmount * 100 / getTotalTrustAmount(); - } - - /** - * Return true, if the binding is authenticated to a sufficient degree. - * - * @return true if total gathered evidence outweighs the target trust amount. - */ - public boolean isAuthenticated() { - return targetAmount <= getTotalTrustAmount(); - } - - /** - * Return a map of {@link CertificationChain CertificationChains} and their respective effective trust amount. - * The effective trust amount of a path might be smaller than its actual trust amount, for example if nodes of a - * path are used multiple times. - * - * @return map of certification chains and their effective trust amounts - */ - @Nonnull - public Map getCertificationChains() { - return Collections.unmodifiableMap(certificationChains); - } - - public static class CertificationChain { - private final int trustAmount; - private final List chainLinks = new ArrayList<>(); - - /** - * A chain of certifications. - * - * @param trustAmount actual trust amount of the chain - * @param chainLinks links of the chain, starting at the trust-root, ending at the target. - */ - public CertificationChain(int trustAmount, @Nonnull List chainLinks) { - this.trustAmount = trustAmount; - this.chainLinks.addAll(chainLinks); - } - - /** - * Actual trust amount of the certification chain. - * @return trust amount - */ - public int getTrustAmount() { - return trustAmount; - } - - /** - * Return all links in the chain, starting at the trust-root and ending at the target. - * @return chain links - */ - @Nonnull - public List getChainLinks() { - return Collections.unmodifiableList(chainLinks); - } - } - - /** - * A chain link contains a node in the trust chain. - */ - public static class ChainLink { - private final PGPPublicKeyRing certificate; - - /** - * Create a chain link. - * @param certificate node in the trust chain - */ - public ChainLink(@Nonnull PGPPublicKeyRing certificate) { - this.certificate = certificate; - } - - /** - * Return the certificate that belongs to the node. - * @return certificate - */ - @Nonnull - public PGPPublicKeyRing getCertificate() { - return certificate; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt new file mode 100644 index 00000000..2024c710 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.authentication + +import org.bouncycastle.openpgp.PGPPublicKeyRing + +class CertificateAuthenticity(val userId: String, + val certificate: PGPPublicKeyRing, + val certificationChains: Map, + val targetAmount: Int) { + + val totalTrustAmount: Int + get() = certificationChains.values.sum() + + + /** + * Return the degree of authentication of the binding in percent. + * 100% means full authentication. + * Values smaller than 100% mean partial authentication. + * + * @return authenticity in percent + */ + val authenticityPercentage: Int + get() = targetAmount * 100 / totalTrustAmount + + /** + * Return true, if the binding is authenticated to a sufficient degree. + * + * @return true if total gathered evidence outweighs the target trust amount. + */ + val authenticated: Boolean + get() = targetAmount <= totalTrustAmount + + fun isAuthenticated() = authenticated +} + +/** + * A chain of certifications. + * + * @param trustAmount actual trust amount of the chain + * @param chainLinks links of the chain, starting at the trust-root, ending at the target. + */ +class CertificationChain( + val trustAmount: Int, + val chainLinks: List) { + +} + +/** + * A chain link contains a node in the trust chain. + */ +class ChainLink( + val certificate: PGPPublicKeyRing) { + +} \ No newline at end of file From 58951ce19a0c6c5993ea5c6e36be0a6bfee1f80a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Aug 2023 14:32:28 +0200 Subject: [PATCH 065/351] Get rid of animalsniffer --- build.gradle | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/build.gradle b/build.gradle index a0f2d0e4..540037a8 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '1.5.3' id 'org.jetbrains.kotlin.jvm' version "1.8.10" } @@ -43,18 +42,6 @@ allprojects { onlyIf { !sourceSets.main.allSource.files.isEmpty() } } - // For library modules, enable android api compatibility check - if (it.name != 'pgpainless-cli') { - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${pgpainlessMinAndroidSdk}:2.3.3_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] - } - } - // checkstyle checkstyle { toolVersion = '10.12.1' From 84e554fc0d5561871ddbc8bf9ed2b6cbb18613f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Aug 2023 14:38:38 +0200 Subject: [PATCH 066/351] Kotlin conversion: CertificateAuthority --- ...Authority.java => CertificateAuthority.kt} | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) rename pgpainless-core/src/main/java/org/pgpainless/authentication/{CertificateAuthority.java => CertificateAuthority.kt} (69%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.kt similarity index 69% rename from pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java rename to pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.kt index 36bf9e5f..e510f48a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.java +++ b/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.kt @@ -4,11 +4,8 @@ package org.pgpainless.authentication; -import org.pgpainless.key.OpenPgpFingerprint; - -import javax.annotation.Nonnull; -import java.util.Date; -import java.util.List; +import org.pgpainless.key.OpenPgpFingerprint +import java.util.* /** * Interface for a CA that can authenticate trust-worthy certificates. @@ -17,7 +14,7 @@ import java.util.List; * @see PGPainless-WOT * @see OpenPGP Web of Trust */ -public interface CertificateAuthority { +interface CertificateAuthority { /** * Determine the authenticity of the binding between the given fingerprint and the userId. @@ -33,11 +30,11 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return information about the authenticity of the binding */ - CertificateAuthenticity authenticateBinding(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); + fun authenticateBinding(fingerprint: OpenPgpFingerprint, + userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int): CertificateAuthenticity; /** * Lookup certificates, which carry a trustworthy binding to the given userId. @@ -50,10 +47,10 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return list of identified bindings */ - List lookupByUserId(@Nonnull String userId, - boolean email, - @Nonnull Date referenceTime, - int targetAmount); + fun lookupByUserId(userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int): List /** * Identify trustworthy bindings for a certificate. @@ -65,7 +62,7 @@ public interface CertificateAuthority { * 60 = partially authenticated...) * @return list of identified bindings */ - List identifyByFingerprint(@Nonnull OpenPgpFingerprint fingerprint, - @Nonnull Date referenceTime, - int targetAmount); + fun identifyByFingerprint(fingerprint: OpenPgpFingerprint, + referenceTime: Date, + targetAmount: Int): List } From 344421a0f48834040e3971ba1ade83b2d3c6efb1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 6 Aug 2023 15:56:44 +0200 Subject: [PATCH 067/351] Kolin conversion: Some OpenPgpFingerprint classes --- .../pgpainless/key/OpenPgpFingerprint.java | 189 ------------------ .../org/pgpainless/key/OpenPgpFingerprint.kt | 158 +++++++++++++++ .../pgpainless/key/OpenPgpV4Fingerprint.java | 149 -------------- .../pgpainless/key/OpenPgpV4Fingerprint.kt | 65 ++++++ .../pgpainless/key/_64DigitFingerprint.java | 119 ----------- .../org/pgpainless/key/_64DigitFingerprint.kt | 74 +++++++ 6 files changed, 297 insertions(+), 457 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java deleted file mode 100644 index 13804061..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.java +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.nio.charset.Charset; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; - -/** - * Abstract super class of different version OpenPGP fingerprints. - * - */ -public abstract class OpenPgpFingerprint implements CharSequence, Comparable { - @SuppressWarnings("CharsetObjectCanBeUsed") - protected static final Charset utf8 = Charset.forName("UTF-8"); - protected final String fingerprint; - - /** - * Return the fingerprint of the given key. - * This method automatically matches key versions to fingerprint implementations. - * - * @param key key - * @return fingerprint - */ - public static OpenPgpFingerprint of(PGPSecretKey key) { - return of(key.getPublicKey()); - } - - /** - * Return the fingerprint of the given key. - * This method automatically matches key versions to fingerprint implementations. - * - * @param key key - * @return fingerprint - */ - public static OpenPgpFingerprint of(PGPPublicKey key) { - if (key.getVersion() == 4) { - return new OpenPgpV4Fingerprint(key); - } - if (key.getVersion() == 5) { - return new OpenPgpV5Fingerprint(key); - } - if (key.getVersion() == 6) { - return new OpenPgpV6Fingerprint(key); - } - throw new IllegalArgumentException("OpenPGP keys of version " + key.getVersion() + " are not supported."); - } - - /** - * Return the fingerprint of the primary key of the given key ring. - * This method automatically matches key versions to fingerprint implementations. - * - * @param ring key ring - * @return fingerprint - */ - public static OpenPgpFingerprint of(PGPKeyRing ring) { - return of(ring.getPublicKey()); - } - - /** - * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. - * If the trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 fingerprint. - * In this case, we return a {@link _64DigitFingerprint}. Since this is ambiguous, it is generally recommended - * to know the version of the key beforehand. - * - * @param fingerprint fingerprint - * @return parsed fingerprint - * @deprecated Use the constructor methods of the versioned fingerprint subclasses instead. - */ - @Deprecated - public static OpenPgpFingerprint parse(String fingerprint) { - String fp = fingerprint.replace(" ", "").trim().toUpperCase(); - if (fp.matches("^[0-9A-F]{40}$")) { - return new OpenPgpV4Fingerprint(fp); - } - if (fp.matches("^[0-9A-F]{64}$")) { - // Might be v5 or v6 :/ - return new _64DigitFingerprint(fp); - } - throw new IllegalArgumentException("Fingerprint does not appear to match any known fingerprint patterns."); - } - - /** - * Parse a binary OpenPGP fingerprint into an {@link OpenPgpFingerprint} object. - * - * @param binaryFingerprint binary representation of the fingerprint - * @return parsed fingerprint - * @deprecated use the parse() methods of the versioned fingerprint subclasses instead. - */ - @Deprecated - public static OpenPgpFingerprint parseFromBinary(byte[] binaryFingerprint) { - String hex = Hex.toHexString(binaryFingerprint).toUpperCase(); - return parse(hex); - } - - public OpenPgpFingerprint(String fingerprint) { - String fp = fingerprint.replace(" ", "").trim().toUpperCase(); - if (!isValid(fp)) { - throw new IllegalArgumentException( - String.format("Fingerprint '%s' does not appear to be a valid OpenPGP V%d fingerprint.", fingerprint, getVersion()) - ); - } - this.fingerprint = fp; - } - - public OpenPgpFingerprint(@Nonnull byte[] bytes) { - this(new String(bytes, utf8)); - } - - public OpenPgpFingerprint(PGPPublicKey key) { - this(Hex.encode(key.getFingerprint())); - if (key.getVersion() != getVersion()) { - throw new IllegalArgumentException(String.format("Key is not a v%d OpenPgp key.", getVersion())); - } - } - - public OpenPgpFingerprint(@Nonnull PGPPublicKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpFingerprint(@Nonnull PGPSecretKeyRing ring) { - this(ring.getPublicKey()); - } - - public OpenPgpFingerprint(@Nonnull PGPKeyRing ring) { - this(ring.getPublicKey()); - } - - /** - * Return the version of the fingerprint. - * - * @return version - */ - public abstract int getVersion(); - - /** - * Check, whether the fingerprint consists of 40 valid hexadecimal characters. - * @param fp fingerprint to check. - * @return true if fingerprint is valid. - */ - protected abstract boolean isValid(@Nonnull String fp); - - /** - * Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to. - * This method can be implemented for V4 and V5 fingerprints. - * V3 key-IDs cannot be derived from the fingerprint, but we don't care, since V3 is deprecated. - * - * @see - * RFC-4880 §12.2: Key IDs and Fingerprints - * @return key id - */ - public abstract long getKeyId(); - - @Override - public int length() { - return fingerprint.length(); - } - - @Override - public char charAt(int i) { - return fingerprint.charAt(i); - } - - @Override - public CharSequence subSequence(int i, int i1) { - return fingerprint.subSequence(i, i1); - } - - @Override - @Nonnull - public String toString() { - return fingerprint; - } - - /** - * Return a pretty printed representation of the fingerprint. - * - * @return pretty printed fingerprint - */ - public abstract String prettyPrint(); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt new file mode 100644 index 00000000..9c7409de --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt @@ -0,0 +1,158 @@ +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex +import java.nio.charset.Charset + +/** + * Abstract super class of different version OpenPGP fingerprints. + * + */ +abstract class OpenPgpFingerprint : CharSequence, Comparable { + val fingerprint: String + + /** + * Return the version of the fingerprint. + * + * @return version + */ + abstract fun getVersion(): Int + + /** + * Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to. + * This method can be implemented for V4 and V5 fingerprints. + * V3 key-IDs cannot be derived from the fingerprint, but we don't care, since V3 is deprecated. + * + * @see + * RFC-4880 §12.2: Key IDs and Fingerprints + * @return key id + */ + abstract val keyId: Long + + constructor(fingerprint: String) { + val prep = fingerprint.replace(" ", "").trim().uppercase() + if (!isValid(prep)) { + throw IllegalArgumentException("Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.") + } + this.fingerprint = prep + } + + constructor(bytes: ByteArray): this(Hex.toHexString(bytes)) + + constructor(key: PGPPublicKey): this(key.fingerprint) { + if (key.version != getVersion()) { + throw IllegalArgumentException("Key is not a v${getVersion()} OpenPgp key.") + } + } + + constructor(key: PGPSecretKey): this(key.publicKey) + + constructor(keys: PGPKeyRing): this(keys.publicKey) + + /** + * Check, whether the fingerprint consists of 40 valid hexadecimal characters. + * @param fp fingerprint to check. + * @return true if fingerprint is valid. + */ + protected abstract fun isValid(fingerprint: String): Boolean + + override val length: Int + get() = fingerprint.length + + override fun get(index: Int) = fingerprint.get(index) + + override fun subSequence(startIndex: Int, endIndex: Int) = fingerprint.subSequence(startIndex, endIndex) + override fun compareTo(other: OpenPgpFingerprint): Int { + return fingerprint.compareTo(other.fingerprint) + } + + override fun equals(other: Any?): Boolean { + return toString() == other.toString() + } + + override fun hashCode(): Int { + return toString().hashCode() + } + + override fun toString(): String = fingerprint + + abstract fun prettyPrint(): String + + companion object { + @JvmStatic + val utf8: Charset = Charset.forName("UTF-8") + + /** + * Return the fingerprint of the given key. + * This method automatically matches key versions to fingerprint implementations. + * + * @param key key + * @return fingerprint + */ + @JvmStatic + fun of(key: PGPSecretKey): OpenPgpFingerprint = of(key.publicKey) + + /** + * Return the fingerprint of the given key. + * This method automatically matches key versions to fingerprint implementations. + * + * @param key key + * @return fingerprint + */ + @JvmStatic + fun of (key: PGPPublicKey): OpenPgpFingerprint = when(key.version) { + 4 -> OpenPgpV4Fingerprint(key) + 5 -> OpenPgpV5Fingerprint(key) + 6 -> OpenPgpV6Fingerprint(key) + else -> throw IllegalArgumentException("OpenPGP keys of version ${key.version} are not supported.") + } + + /** + * Return the fingerprint of the primary key of the given key ring. + * This method automatically matches key versions to fingerprint implementations. + * + * @param ring key ring + * @return fingerprint + */ + @JvmStatic + fun of (keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) + + /** + * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. + * If the trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 fingerprint. + * In this case, we return a {@link _64DigitFingerprint}. Since this is ambiguous, it is generally recommended + * to know the version of the key beforehand. + * + * @param fingerprint fingerprint + * @return parsed fingerprint + * @deprecated Use the constructor methods of the versioned fingerprint subclasses instead. + */ + @JvmStatic + @Deprecated("Use the constructor methods of the versioned fingerprint subclasses instead.") + fun parse(fingerprint: String): OpenPgpFingerprint { + val prep = fingerprint.replace(" ", "").trim().uppercase() + if (prep.matches("^[0-9A-F]{40}$".toRegex())) { + return OpenPgpV4Fingerprint(prep) + } + if (prep.matches("^[0-9A-F]{64}$".toRegex())) { + // Might be v5 or v6 :/ + return _64DigitFingerprint(prep) + } + throw IllegalArgumentException("Fingerprint does not appear to match any known fingerprint pattern.") + } + + /** + * Parse a binary OpenPGP fingerprint into an {@link OpenPgpFingerprint} object. + * + * @param binaryFingerprint binary representation of the fingerprint + * @return parsed fingerprint + * @deprecated use the parse() methods of the versioned fingerprint subclasses instead. + */ + @JvmStatic + @Deprecated("use the parse() methods of the versioned fingerprint subclasses instead.") + fun parseFromBinary(binaryFingerprint: ByteArray): OpenPgpFingerprint = + parse(Hex.toHexString(binaryFingerprint)) + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java deleted file mode 100644 index 13a79201..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; - -/** - * This class represents a hex encoded, uppercase OpenPGP v4 fingerprint. - */ -public class OpenPgpV4Fingerprint extends OpenPgpFingerprint { - - public static final String SCHEME = "openpgp4fpr"; - - /** - * Create an {@link OpenPgpV4Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 40 - */ - public OpenPgpV4Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV4Fingerprint(@Nonnull byte[] bytes) { - super(Hex.encode(bytes)); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV4Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 4; - } - - @Override - protected boolean isValid(@Nonnull String fp) { - return fp.matches("^[0-9A-F]{40}$"); - } - - @Override - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer buf = ByteBuffer.wrap(bytes); - - // The key id is the right-most 8 bytes (conveniently a long) - // We have to cast here in order to be compatible with java 8 - // https://github.com/eclipse/jetty.project/issues/3244 - ((Buffer) buf).position(12); // 20 - 8 bytes = offset 12 - - return buf.getLong(); - } - - @Override - public String prettyPrint() { - String fp = toString(); - StringBuilder pretty = new StringBuilder(); - for (int i = 0; i < 5; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(' '); - for (int i = 5; i < 9; i++) { - pretty.append(fp, i * 4, (i + 1) * 4).append(' '); - } - pretty.append(fp, 36, 40); - return pretty.toString(); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - - if (!(other instanceof CharSequence)) { - return false; - } - - return this.toString().equals(other.toString()); - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - /** - * Return the fingerprint as an openpgp4fpr {@link URI}. - * An example would be 'openpgp4fpr:7F9116FEA90A5983936C7CFAA027DB2F3E1E118A'. - * - * @return openpgp4fpr fingerprint uri - * @see openpgp4fpr URI scheme - */ - public URI toUri() { - try { - return new URI(OpenPgpV4Fingerprint.SCHEME, toString(), null); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } - - /** - * Convert an openpgp4fpr URI to an {@link OpenPgpV4Fingerprint}. - * - * @param uri {@link URI} with scheme 'openpgp4fpr' - * @return fingerprint parsed from the uri - * @see openpgp4fpr URI scheme - */ - public static OpenPgpV4Fingerprint fromUri(URI uri) { - if (!SCHEME.equals(uri.getScheme())) { - throw new IllegalArgumentException("URI scheme MUST equal '" + SCHEME + "'"); - } - return new OpenPgpV4Fingerprint(uri.getSchemeSpecificPart()); - } - - @Override - public int compareTo(@Nonnull OpenPgpFingerprint openPgpFingerprint) { - return toString().compareTo(openPgpFingerprint.toString()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt new file mode 100644 index 00000000..72c31f4e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt @@ -0,0 +1,65 @@ +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex +import java.net.URI +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.charset.Charset + +class OpenPgpV4Fingerprint: OpenPgpFingerprint { + + constructor(fingerprint: String): super(fingerprint) + constructor(bytes: ByteArray): super(bytes) + constructor(key: PGPPublicKey): super(key) + constructor(key: PGPSecretKey): super(key) + constructor(keys: PGPKeyRing): super(keys) + + override fun getVersion() = 4 + + override val keyId: Long + get() { + val bytes = Hex.decode(toString().toByteArray(Charset.forName("UTF-8"))) + val buf = ByteBuffer.wrap(bytes) + + // The key id is the right-most 8 bytes (conveniently a long) + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + (buf as Buffer).position(12) // 20 - 8 bytes = offset 12 + return buf.getLong() + } + + override fun isValid(fingerprint: String): Boolean { + return fingerprint.matches("^[0-9A-F]{40}$".toRegex()) + } + + fun toUri(): URI = URI(SCHEME, toString(), null) + + override fun prettyPrint(): String { + return buildString { + for (i in 0..4) { + append(fingerprint, i * 4, (i + 1) * 4).append(' ') + } + append(' ') + for (i in 5 .. 8) { + append(fingerprint, i * 4, (i + 1) * 4).append(' ') + } + append(fingerprint, 36, 40) + } + } + + companion object { + @JvmStatic + val SCHEME = "openpgp4fpr" + + @JvmStatic + fun fromUri(uri: URI): OpenPgpV4Fingerprint { + if (SCHEME != uri.scheme) { + throw IllegalArgumentException("URI scheme MUST equal '$SCHEME'.") + } + return OpenPgpV4Fingerprint(uri.schemeSpecificPart) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java deleted file mode 100644 index 11f18058..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.nio.Buffer; -import java.nio.ByteBuffer; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; - -/** - * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. - * Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the - * key version. - */ -public class _64DigitFingerprint extends OpenPgpFingerprint { - - /** - * Create an {@link _64DigitFingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - protected _64DigitFingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - protected _64DigitFingerprint(@Nonnull byte[] bytes) { - super(Hex.encode(bytes)); - } - - protected _64DigitFingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - protected _64DigitFingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - protected _64DigitFingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - protected _64DigitFingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - protected _64DigitFingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return -1; // might be v5 or v6 - } - - @Override - protected boolean isValid(@Nonnull String fp) { - return fp.matches("^[0-9A-F]{64}$"); - } - - @Override - public long getKeyId() { - byte[] bytes = Hex.decode(toString().getBytes(utf8)); - ByteBuffer buf = ByteBuffer.wrap(bytes); - - // The key id is the left-most 8 bytes (conveniently a long). - // We have to cast here in order to be compatible with java 8 - // https://github.com/eclipse/jetty.project/issues/3244 - ((Buffer) buf).position(0); - - return buf.getLong(); - } - - @Override - public String prettyPrint() { - String fp = toString(); - StringBuilder pretty = new StringBuilder(); - - for (int i = 0; i < 4; i++) { - pretty.append(fp, i * 8, (i + 1) * 8).append(' '); - } - pretty.append(' '); - for (int i = 4; i < 7; i++) { - pretty.append(fp, i * 8, (i + 1) * 8).append(' '); - } - pretty.append(fp, 56, 64); - return pretty.toString(); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - - if (!(other instanceof CharSequence)) { - return false; - } - - return this.toString().equals(other.toString()); - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - @Override - public int compareTo(OpenPgpFingerprint openPgpFingerprint) { - return toString().compareTo(openPgpFingerprint.toString()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt new file mode 100644 index 00000000..0f7dd705 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import javax.annotation.Nonnull; + +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.encoders.Hex; + +/** + * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. + * Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the + * key version. + */ +open class _64DigitFingerprint: OpenPgpFingerprint { + + /** + * Create an {@link _64DigitFingerprint}. + * + * @param fingerprint uppercase hexadecimal fingerprint of length 64 + */ + constructor(fingerprint: String): super(fingerprint) + constructor(bytes: ByteArray): super(bytes) + constructor(key: PGPPublicKey): super(key) + constructor(key: PGPSecretKey): super(key) + constructor(keys: PGPKeyRing): super(keys) + + override val keyId: Long + get() { + val bytes = Hex.decode(fingerprint.toByteArray(Charset.forName("UTF-8"))) + val buf = ByteBuffer.wrap(bytes); + + // The key id is the left-most 8 bytes (conveniently a long). + // We have to cast here in order to be compatible with java 8 + // https://github.com/eclipse/jetty.project/issues/3244 + (buf as Buffer).position(0); + + return buf.getLong() + } + + override fun getVersion(): Int { + return -1 // might be v5 or v6 + } + + override fun isValid(fingerprint: String): Boolean { + return fingerprint.matches(("^[0-9A-F]{64}$".toRegex())) + } + + override fun toString(): String { + return super.toString() + } + + override fun prettyPrint(): String { + return buildString { + for (i in 0 until 4) { + append(fingerprint, i * 8, (i + 1) * 8).append(' '); + } + append(' '); + for (i in 4 until 7) { + append(fingerprint, i * 8, (i + 1) * 8).append(' '); + } + append(fingerprint, 56, 64); + } + } +} From d36e878bf9824d05c3e41008ccf9c2535649b258 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Aug 2023 15:45:55 +0200 Subject: [PATCH 068/351] Kotlin conversion: OpenPgpFingerprints --- .../pgpainless/key/OpenPgpV5Fingerprint.java | 58 ------------------- .../pgpainless/key/OpenPgpV5Fingerprint.kt | 25 ++++++++ .../pgpainless/key/OpenPgpV6Fingerprint.java | 58 ------------------- .../pgpainless/key/OpenPgpV6Fingerprint.kt | 25 ++++++++ 4 files changed, 50 insertions(+), 116 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java deleted file mode 100644 index a0a3d1f4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; - -/** - * This class represents a hex encoded, upper case OpenPGP v5 fingerprint. - */ -public class OpenPgpV5Fingerprint extends _64DigitFingerprint { - - /** - * Create an {@link OpenPgpV5Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - public OpenPgpV5Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV5Fingerprint(@Nonnull byte[] bytes) { - super(bytes); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV5Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 5; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt new file mode 100644 index 00000000..b82a3482 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey + +/** + * This class represents a hex encoded uppercase OpenPGP v5 fingerprint. + */ +class OpenPgpV5Fingerprint: _64DigitFingerprint { + + constructor(fingerprint: String): super(fingerprint) + constructor(key: PGPPublicKey): super(key) + constructor(key: PGPSecretKey): super(key) + constructor(keys: PGPKeyRing): super(keys) + constructor(bytes: ByteArray): super(bytes) + + override fun getVersion(): Int { + return 5 + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java deleted file mode 100644 index 79cc1715..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; - -/** - * This class represents a hex encoded, upper case OpenPGP v6 fingerprint. - */ -public class OpenPgpV6Fingerprint extends _64DigitFingerprint { - - /** - * Create an {@link OpenPgpV6Fingerprint}. - * - * @param fingerprint uppercase hexadecimal fingerprint of length 64 - */ - public OpenPgpV6Fingerprint(@Nonnull String fingerprint) { - super(fingerprint); - } - - public OpenPgpV6Fingerprint(@Nonnull byte[] bytes) { - super(bytes); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPPublicKey key) { - super(key); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPSecretKey key) { - this(key.getPublicKey()); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPPublicKeyRing ring) { - super(ring); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPSecretKeyRing ring) { - super(ring); - } - - public OpenPgpV6Fingerprint(@Nonnull PGPKeyRing ring) { - super(ring); - } - - @Override - public int getVersion() { - return 6; - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt new file mode 100644 index 00000000..11cf05b0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey + +/** + * This class represents a hex encoded, uppercase OpenPGP v6 fingerprint. + */ +class OpenPgpV6Fingerprint: _64DigitFingerprint { + + constructor(fingerprint: String): super(fingerprint) + constructor(key: PGPPublicKey): super(key) + constructor(key: PGPSecretKey): super(key) + constructor(keys: PGPKeyRing): super(keys) + constructor(bytes: ByteArray): super(bytes) + + override fun getVersion(): Int { + return 6 + } +} \ No newline at end of file From ca6e18d701870fc95272970b3cda320fca22651c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Aug 2023 16:09:18 +0200 Subject: [PATCH 069/351] Kotlin conversion: SubkeyIdentifier --- .../org/pgpainless/key/SubkeyIdentifier.java | 146 ------------------ .../org/pgpainless/key/SubkeyIdentifier.kt | 53 +++++++ 2 files changed, 53 insertions(+), 146 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java deleted file mode 100644 index 8bfd7f7c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.java +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key; - -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; - -/** - * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, - * as well as the subkeys fingerprint. - */ -public class SubkeyIdentifier { - - private final OpenPgpFingerprint primaryKeyFingerprint; - private final OpenPgpFingerprint subkeyFingerprint; - - /** - * Create a {@link SubkeyIdentifier} from a {@link PGPKeyRing}. - * The identifier will point to the primary key of the provided ring. - * - * @param keyRing key ring - */ - public SubkeyIdentifier(PGPKeyRing keyRing) { - this(keyRing, keyRing.getPublicKey().getKeyID()); - } - - /** - * Create a {@link SubkeyIdentifier} from a {@link PGPKeyRing} and the subkeys key id. - * {@link #getPrimaryKeyFingerprint()} will return the {@link OpenPgpFingerprint} of the keyrings primary key, - * while {@link #getSubkeyFingerprint()} will return the subkeys fingerprint. - * - * @param keyRing keyring the subkey belongs to - * @param keyId keyid of the subkey - */ - public SubkeyIdentifier(@Nonnull PGPKeyRing keyRing, long keyId) { - PGPPublicKey subkey = keyRing.getPublicKey(keyId); - if (subkey == null) { - throw new NoSuchElementException("Key ring does not contain subkey with id " + Long.toHexString(keyId)); - } - this.primaryKeyFingerprint = OpenPgpFingerprint.of(keyRing); - this.subkeyFingerprint = OpenPgpFingerprint.of(subkey); - } - - public SubkeyIdentifier(@Nonnull PGPKeyRing keyRing, @Nonnull OpenPgpFingerprint subkeyFingerprint) { - this(OpenPgpFingerprint.of(keyRing), subkeyFingerprint); - } - - /** - * Create a {@link SubkeyIdentifier} that identifies the primary key with the given fingerprint. - * This means, both {@link #getPrimaryKeyFingerprint()} and {@link #getSubkeyFingerprint()} return the same. - * - * @param primaryKeyFingerprint fingerprint of the identified key - */ - public SubkeyIdentifier(@Nonnull OpenPgpFingerprint primaryKeyFingerprint) { - this(primaryKeyFingerprint, primaryKeyFingerprint); - } - - /** - * Create a {@link SubkeyIdentifier} which points to the subkey with the given subkeyFingerprint, - * which has a primary key with the given primaryKeyFingerprint. - * - * @param primaryKeyFingerprint fingerprint of the primary key - * @param subkeyFingerprint fingerprint of the subkey - */ - public SubkeyIdentifier(@Nonnull OpenPgpFingerprint primaryKeyFingerprint, @Nonnull OpenPgpFingerprint subkeyFingerprint) { - this.primaryKeyFingerprint = primaryKeyFingerprint; - this.subkeyFingerprint = subkeyFingerprint; - } - - public @Nonnull OpenPgpFingerprint getFingerprint() { - return getSubkeyFingerprint(); - } - - public long getKeyId() { - return getSubkeyId(); - } - - /** - * Return the {@link OpenPgpFingerprint} of the primary key of the identified key. - * This might be the same as {@link #getSubkeyFingerprint()} if the identified subkey is the primary key. - * - * @return primary key fingerprint - */ - public @Nonnull OpenPgpFingerprint getPrimaryKeyFingerprint() { - 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 OpenPgpFingerprint} of the identified subkey. - * - * @return subkey fingerprint - */ - public @Nonnull OpenPgpFingerprint getSubkeyFingerprint() { - 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(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof SubkeyIdentifier)) { - return false; - } - SubkeyIdentifier other = (SubkeyIdentifier) obj; - return getPrimaryKeyFingerprint().equals(other.getPrimaryKeyFingerprint()) - && getSubkeyFingerprint().equals(other.getSubkeyFingerprint()); - } - - @Override - public String toString() { - return getSubkeyFingerprint() + " " + getPrimaryKeyFingerprint(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt new file mode 100644 index 00000000..61b45874 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.key.util.KeyIdUtil + +/** + * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, + * as well as the subkeys fingerprint. + */ +class SubkeyIdentifier( + val primaryKeyFingerprint: OpenPgpFingerprint, + val subkeyFingerprint: OpenPgpFingerprint) { + + constructor(fingerprint: OpenPgpFingerprint): this(fingerprint, fingerprint) + constructor(keys: PGPKeyRing): this(keys.publicKey) + constructor(key: PGPPublicKey): this(OpenPgpFingerprint.of(key)) + constructor(keys: PGPKeyRing, keyId: Long): this( + OpenPgpFingerprint.of(keys.publicKey), + OpenPgpFingerprint.of(keys.getPublicKey(keyId) ?: + throw NoSuchElementException("OpenPGP key does not contain subkey ${KeyIdUtil.formatKeyId(keyId)}"))) + constructor(keys: PGPKeyRing, subkeyFingerprint: OpenPgpFingerprint): this(OpenPgpFingerprint.of(keys), subkeyFingerprint) + + val keyId = subkeyFingerprint.keyId + val fingerprint = subkeyFingerprint + + val subkeyId = subkeyFingerprint.keyId + val primaryKeyId = primaryKeyFingerprint.keyId + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (this === other) { + return true + } + if (other !is SubkeyIdentifier) { + return false + } + + return primaryKeyFingerprint.equals(other.primaryKeyFingerprint) && subkeyFingerprint.equals(other.subkeyFingerprint) + } + + override fun hashCode(): Int { + return primaryKeyFingerprint.hashCode() + 31 * subkeyFingerprint.hashCode() + } + + override fun toString(): String = "$subkeyFingerprint $primaryKeyFingerprint" +} \ No newline at end of file From b68061373dcae8e032a3a7b9e8e28254ca906231 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Aug 2023 11:45:49 +0200 Subject: [PATCH 070/351] Wip: Kolin conversion of Policy --- .../java/org/pgpainless/policy/Policy2.kt | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt new file mode 100644 index 00000000..e17b9653 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt @@ -0,0 +1,132 @@ +package org.pgpainless.policy + +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.util.DateUtil +import java.util.* + +class Policy2 { + + /** + * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the + * given map, if the queried usage date is BEFORE the respective termination date. + * A termination date value of

null
means no termination, resulting in the algorithm being + * acceptable, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm + * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + */ + class HashAlgorithmPolicy( + val defaultHashAlgorithm: HashAlgorithm, + val acceptableHashAlgorithmsAndTerminationDates: Map) { + + /** + * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed in + * the given list, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) + * @param acceptableHashAlgorithms list of acceptable hash algorithms + */ + constructor(defaultHashAlgorithm: HashAlgorithm, acceptableHashAlgorithms: List) : + this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) + + fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date()) + + /** + * Return true, if the given algorithm is acceptable for the given usage date. + * + * @param hashAlgorithm algorithm + * @param referenceTime usage date (e.g. signature creation time) + * + * @return acceptance + */ + fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean { + if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) + return false + val terminationDate = acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] + if (terminationDate == null) { + return true + } + return terminationDate > referenceTime + } + + fun isAcceptable(algorithmId: Int, referenceTime: Date): Boolean { + HashAlgorithm.fromId(algorithmId).let { + if (it == null) { + return false + } + return isAcceptable(it, referenceTime) + } + } + + fun isAcceptable(algorithmId: Int) = isAcceptable(algorithmId, Date()) + + companion object { + @JvmStatic + val smartSignatureHashAlgorithmPolicy = HashAlgorithmPolicy(HashAlgorithm.SHA512, buildMap { + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA224, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA3_512, null) + }) + + /** + * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are acceptable + * according to 2022 standards. + * + * Particularly this policy only accepts algorithms from the SHA2 and SHA3 families. + * + * @return static signature algorithm policy + */ + @JvmStatic + val static2022SignatureHashAlgorithmPolicy = + HashAlgorithmPolicy(HashAlgorithm.SHA512, listOf( + HashAlgorithm.SHA224, + HashAlgorithm.SHA256, + HashAlgorithm.SHA384, + HashAlgorithm.SHA512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA3_512)) + + /** + * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. + * + * @return static revocation signature hash algorithm policy + */ + @JvmStatic + val static2022RevocationSignatureHashAlgorithmPolicy = + HashAlgorithmPolicy(HashAlgorithm.SHA512, listOf( + HashAlgorithm.RIPEMD160, + HashAlgorithm.SHA1, + HashAlgorithm.SHA224, + HashAlgorithm.SHA256, + HashAlgorithm.SHA384, + HashAlgorithm.SHA512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA3_512)) + } + } + + class CompressionAlgorithmPolicy( + val defaultCompressionAlgorithm: CompressionAlgorithm, + val acceptableCompressionAlgorithms: List) { + + fun isAcceptable(algorithm: CompressionAlgorithm) = acceptableCompressionAlgorithms.contains(algorithm) + fun isAcceptable(algorithmId: Int) = + } + + companion object { + private var INSTANCE: Policy2? = null + fun getInstance(): Policy2 { + if (INSTANCE == null) { + INSTANCE = Policy2() + } + return INSTANCE!! + } + } +} \ No newline at end of file From e57e74163c8c860493553b3d110b4261db383cb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Aug 2023 13:15:00 +0200 Subject: [PATCH 071/351] Finish Policy conversion and move kotlin classes to src/kotlin/ --- .../algorithm/negotiation/package-info.java | 8 - .../pgpainless/algorithm/package-info.java | 8 - .../authentication/package-info.java | 8 - .../java/org/pgpainless/policy/Policy.java | 732 ------------------ .../java/org/pgpainless/policy/Policy2.kt | 132 ---- .../org/pgpainless/policy/package-info.java | 8 - .../org/pgpainless/PGPainless.kt | 4 + .../org/pgpainless/algorithm/AEADAlgorithm.kt | 0 .../pgpainless/algorithm/AlgorithmSuite.kt | 0 .../pgpainless/algorithm/CertificationType.kt | 0 .../algorithm/CompressionAlgorithm.kt | 0 .../algorithm/DocumentSignatureType.kt | 0 .../pgpainless/algorithm/EncryptionPurpose.kt | 0 .../org/pgpainless/algorithm/Feature.kt | 0 .../org/pgpainless/algorithm/HashAlgorithm.kt | 0 .../org/pgpainless/algorithm/KeyFlag.kt | 0 .../org/pgpainless/algorithm/OpenPgpPacket.kt | 0 .../algorithm/PublicKeyAlgorithm.kt | 0 .../pgpainless/algorithm/RevocationState.kt | 0 .../algorithm/RevocationStateType.kt | 0 .../algorithm/SignatureSubpacket.kt | 0 .../org/pgpainless/algorithm/SignatureType.kt | 0 .../pgpainless/algorithm/StreamEncoding.kt | 0 .../algorithm/SymmetricKeyAlgorithm.kt | 0 .../pgpainless/algorithm/Trustworthiness.kt | 0 .../negotiation/HashAlgorithmNegotiator.kt | 0 .../SymmetricKeyAlgorithmNegotiator.kt | 0 .../authentication/CertificateAuthenticity.kt | 0 .../authentication/CertificateAuthority.kt | 0 .../org/pgpainless/key/OpenPgpFingerprint.kt | 4 + .../pgpainless/key/OpenPgpV4Fingerprint.kt | 4 + .../pgpainless/key/OpenPgpV5Fingerprint.kt | 0 .../pgpainless/key/OpenPgpV6Fingerprint.kt | 0 .../org/pgpainless/key/SubkeyIdentifier.kt | 0 .../org/pgpainless/key/_64DigitFingerprint.kt | 0 .../kotlin/org/pgpainless/policy/Policy.kt | 359 +++++++++ 36 files changed, 371 insertions(+), 896 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/PGPainless.kt (98%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/AEADAlgorithm.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/AlgorithmSuite.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/CertificationType.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/CompressionAlgorithm.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/DocumentSignatureType.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/EncryptionPurpose.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/Feature.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/HashAlgorithm.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/KeyFlag.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/OpenPgpPacket.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/PublicKeyAlgorithm.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/RevocationState.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/RevocationStateType.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/SignatureSubpacket.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/SignatureType.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/StreamEncoding.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/Trustworthiness.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/authentication/CertificateAuthenticity.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/authentication/CertificateAuthority.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/OpenPgpFingerprint.kt (98%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/OpenPgpV4Fingerprint.kt (94%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/OpenPgpV5Fingerprint.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/OpenPgpV6Fingerprint.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/SubkeyIdentifier.kt (100%) rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/key/_64DigitFingerprint.kt (100%) create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt 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 deleted file mode 100644 index 3969d8df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to algorithm negotiation. - */ -package org.pgpainless.algorithm.negotiation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java deleted file mode 100644 index c2f5f5ff..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Enums which map to OpenPGP's algorithm IDs. - */ -package org.pgpainless.algorithm; diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java deleted file mode 100644 index 495ab1f7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/authentication/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes and interfaces related to certificate authenticity. - */ -package org.pgpainless.authentication; diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java deleted file mode 100644 index 35787138..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ /dev/null @@ -1,732 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.policy; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.DateUtil; -import org.pgpainless.util.NotationRegistry; - -/** - * Policy class used to configure acceptable algorithm suites etc. - */ -public final class Policy { - - private static Policy INSTANCE; - - private HashAlgorithmPolicy signatureHashAlgorithmPolicy = - HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(); - private HashAlgorithmPolicy revocationSignatureHashAlgorithmPolicy = - HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(); - private SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(); - private SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionAlgorithmPolicy = - SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(); - private CompressionAlgorithmPolicy compressionAlgorithmPolicy = - CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(); - private PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy = - PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(); - private final NotationRegistry notationRegistry = new NotationRegistry(); - - private AlgorithmSuite keyGenerationAlgorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - - // Signers User-ID is soon to be deprecated. - private SignerUserIdValidationLevel signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED; - - private boolean enableKeyParameterValidation = false; - - public enum SignerUserIdValidationLevel { - /** - * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. - * This means, that signatures with Signer's User-ID subpackets containing a value that does not match the signer key's - * user-id exactly, will be rejected. - * E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not - * match exactly and is therefore rejected. - */ - STRICT, - - /** - * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature. - */ - DISABLED - } - - Policy() { - } - - /** - * Return the singleton instance of PGPainless' policy. - * - * @return singleton instance - */ - public static Policy getInstance() { - if (INSTANCE == null) { - INSTANCE = new Policy(); - } - return INSTANCE; - } - - /** - * Return the hash algorithm policy for signatures. - * @return hash algorithm policy - */ - public HashAlgorithmPolicy getSignatureHashAlgorithmPolicy() { - return signatureHashAlgorithmPolicy; - } - - /** - * Set a custom hash algorithm policy for signatures. - * - * @param policy custom policy - */ - public void setSignatureHashAlgorithmPolicy(HashAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.signatureHashAlgorithmPolicy = policy; - } - - /** - * Return the hash algorithm policy for revocations. - * This policy is separate from {@link #getSignatureHashAlgorithmPolicy()}, as PGPainless by default uses a - * less strict policy when it comes to acceptable algorithms. - * - * @return revocation signature hash algorithm policy - */ - public HashAlgorithmPolicy getRevocationSignatureHashAlgorithmPolicy() { - return revocationSignatureHashAlgorithmPolicy; - } - - /** - * Set a custom hash algorithm policy for revocations. - * - * @param policy custom policy - */ - public void setRevocationSignatureHashAlgorithmPolicy(HashAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.revocationSignatureHashAlgorithmPolicy = policy; - } - - /** - * Return the symmetric encryption algorithm policy for encryption. - * This policy defines which symmetric algorithms are acceptable when producing encrypted messages. - * - * @return symmetric algorithm policy for encryption - */ - public SymmetricKeyAlgorithmPolicy getSymmetricKeyEncryptionAlgorithmPolicy() { - return symmetricKeyEncryptionAlgorithmPolicy; - } - - /** - * Return the symmetric encryption algorithm policy for decryption. - * This policy defines which symmetric algorithms are acceptable when decrypting encrypted messages. - * - * @return symmetric algorithm policy for decryption - */ - public SymmetricKeyAlgorithmPolicy getSymmetricKeyDecryptionAlgorithmPolicy() { - return symmetricKeyDecryptionAlgorithmPolicy; - } - - /** - * Set a custom symmetric encryption algorithm policy for encrypting messages. - * - * @param policy custom policy - */ - public void setSymmetricKeyEncryptionAlgorithmPolicy(SymmetricKeyAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - this.symmetricKeyEncryptionAlgorithmPolicy = policy; - } - - /** - * Set a custom symmetric encryption algorithm policy for decrypting messages. - * - * @param policy custom policy - */ - public void setSymmetricKeyDecryptionAlgorithmPolicy(SymmetricKeyAlgorithmPolicy policy) { - if (policy == null) { - throw new NullPointerException("Policy cannot be null."); - } - 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; - } - - /** - * Return the current public key algorithm policy. - * - * @return public key algorithm policy - */ - public PublicKeyAlgorithmPolicy getPublicKeyAlgorithmPolicy() { - return publicKeyAlgorithmPolicy; - } - - /** - * Set a custom public key algorithm policy. - * - * @param publicKeyAlgorithmPolicy custom policy - */ - public void setPublicKeyAlgorithmPolicy(PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy) { - if (publicKeyAlgorithmPolicy == null) { - throw new NullPointerException("Public key algorithm policy cannot be null."); - } - this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy; - } - - public static final class SymmetricKeyAlgorithmPolicy { - - private final SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm; - private final List acceptableSymmetricKeyAlgorithms; - - public SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm, List acceptableSymmetricKeyAlgorithms) { - this.defaultSymmetricKeyAlgorithm = defaultSymmetricKeyAlgorithm; - this.acceptableSymmetricKeyAlgorithms = Collections.unmodifiableList(acceptableSymmetricKeyAlgorithms); - } - - /** - * Return the default symmetric key algorithm. - * This algorithm is used as a fallback when no consensus about symmetric algorithms can be reached. - * - * @return default symmetric encryption algorithm - */ - public SymmetricKeyAlgorithm getDefaultSymmetricKeyAlgorithm() { - return defaultSymmetricKeyAlgorithm; - } - - /** - * Return true if the given symmetric encryption algorithm is acceptable by this policy. - * - * @param algorithm algorithm - * @return true if algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(SymmetricKeyAlgorithm algorithm) { - return acceptableSymmetricKeyAlgorithms.contains(algorithm); - } - - /** - * Return true if the given symmetric encryption algorithm is acceptable by this policy. - * - * @param algorithmId algorithm - * @return true if algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(int algorithmId) { - try { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * The default symmetric encryption algorithm policy of PGPainless. - * - * @return default symmetric encryption algorithm policy - * @deprecated not expressive - will be removed in a future release - */ - @Deprecated - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyEncryptionAlgorithmPolicy() { - return symmetricKeyEncryptionPolicy2022(); - } - - /** - * Policy for symmetric encryption algorithms in the context of message production (encryption). - * This suite contains algorithms that are deemed safe to use in 2022. - * - * @return 2022 symmetric key encryption algorithm policy - */ - public static SymmetricKeyAlgorithmPolicy symmetricKeyEncryptionPolicy2022() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_128, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128 - )); - } - - /** - * The default symmetric decryption algorithm policy of PGPainless. - * - * @return default symmetric decryption algorithm policy - * @deprecated not expressive - will be removed in a future update - */ - @Deprecated - public static SymmetricKeyAlgorithmPolicy defaultSymmetricKeyDecryptionAlgorithmPolicy() { - return symmetricKeyDecryptionPolicy2022(); - } - - /** - * Policy for symmetric key encryption algorithms in the context of message consumption (decryption). - * This suite contains algorithms that are deemed safe to use in 2022. - * - * @return 2022 symmetric key decryption algorithm policy - */ - public static SymmetricKeyAlgorithmPolicy symmetricKeyDecryptionPolicy2022() { - return new SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_128, Arrays.asList( - // Reject: Unencrypted, IDEA, TripleDES, Blowfish - SymmetricKeyAlgorithm.CAST5, - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128 - )); - } - - /** - * Select the best acceptable algorithm from the options list. - * The best algorithm is the first algorithm we encounter in our list of acceptable algorithms that - * is also contained in the list of options. - * - * - * @param options list of algorithm options - * @return best - */ - public SymmetricKeyAlgorithm selectBest(List options) { - for (SymmetricKeyAlgorithm acceptable : acceptableSymmetricKeyAlgorithms) { - if (options.contains(acceptable)) { - return acceptable; - } - } - return null; - } - } - - public static final class HashAlgorithmPolicy { - - private final HashAlgorithm defaultHashAlgorithm; - private final Map acceptableHashAlgorithmsAndTerminationDates; - - /** - * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} from the - * given map, if the queried usage date is BEFORE the respective termination date. - * A termination date value of
null
means no termination, resulting in the algorithm being - * acceptable, regardless of usage date. - * - * @param defaultHashAlgorithm default hash algorithm - * @param algorithmTerminationDates map of acceptable algorithms and their termination dates - */ - public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull Map algorithmTerminationDates) { - this.defaultHashAlgorithm = defaultHashAlgorithm; - this.acceptableHashAlgorithmsAndTerminationDates = algorithmTerminationDates; - } - - /** - * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} listed in - * the given list, regardless of usage date. - * - * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) - * @param acceptableHashAlgorithms list of acceptable hash algorithms - */ - public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull List acceptableHashAlgorithms) { - this(defaultHashAlgorithm, Collections.unmodifiableMap(listToMap(acceptableHashAlgorithms))); - } - - private static Map listToMap(@Nonnull List algorithms) { - Map algorithmsAndTerminationDates = new HashMap<>(); - for (HashAlgorithm algorithm : algorithms) { - algorithmsAndTerminationDates.put(algorithm, null); - } - return algorithmsAndTerminationDates; - } - - /** - * Return the default hash algorithm. - * This algorithm is used as a fallback when no consensus about hash algorithms can be reached. - * - * @return default hash algorithm - */ - public HashAlgorithm defaultHashAlgorithm() { - return defaultHashAlgorithm; - } - - /** - * Return true if the given hash algorithm is currently acceptable by this policy. - * - * @param hashAlgorithm hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm) { - return isAcceptable(hashAlgorithm, new Date()); - } - - /** - * Return true if the given hash algorithm is currently acceptable by this policy. - * - * @param algorithmId hash algorithm - * @return true if the hash algorithm is acceptable, false otherwise - */ - public boolean isAcceptable(int algorithmId) { - try { - HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * Return true, if the given algorithm is acceptable for the given usage date. - * - * @param hashAlgorithm algorithm - * @param usageDate usage date (e.g. signature creation time) - * - * @return acceptance - */ - public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm, @Nonnull Date usageDate) { - if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) { - return false; - } - - // Check termination date - Date terminationDate = acceptableHashAlgorithmsAndTerminationDates.get(hashAlgorithm); - if (terminationDate == null) { - return true; - } - - // Reject if usage date is past termination date - return terminationDate.after(usageDate); - } - - public boolean isAcceptable(int algorithmId, @Nonnull Date usageDate) { - try { - HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm, usageDate); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - /** - * The default signature hash algorithm policy of PGPainless. - * Note that this policy is only used for non-revocation signatures. - * For revocation signatures {@link #defaultRevocationSignatureHashAlgorithmPolicy()} is used instead. - * - * @return default signature hash algorithm policy - * @deprecated not expressive - will be removed in an upcoming release - */ - @Deprecated - public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { - return smartSignatureHashAlgorithmPolicy(); - } - - /** - * {@link HashAlgorithmPolicy} which takes the date of the algorithm usage into consideration. - * If the policy has a termination date for a given algorithm, and the usage date is after that termination - * date, the algorithm is rejected. - * - * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. - * - * @see Sequoia-PGP's Collision Resistant Algorithm Policy - * - * @return smart signature algorithm policy - */ - public static HashAlgorithmPolicy smartSignatureHashAlgorithmPolicy() { - Map algorithmDateMap = new HashMap<>(); - - algorithmDateMap.put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - algorithmDateMap.put(HashAlgorithm.SHA224, null); - algorithmDateMap.put(HashAlgorithm.SHA256, null); - algorithmDateMap.put(HashAlgorithm.SHA384, null); - algorithmDateMap.put(HashAlgorithm.SHA512, null); - - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); - } - - /** - * {@link HashAlgorithmPolicy} which only accepts signatures made using algorithms which are acceptable - * according to 2022 standards. - * - * Particularly this policy only accepts algorithms from the SHA2 family. - * - * @return static signature algorithm policy - */ - public static HashAlgorithmPolicy static2022SignatureHashAlgorithmPolicy() { - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512 - )); - } - - /** - * The default revocation signature hash algorithm policy of PGPainless. - * - * @return default revocation signature hash algorithm policy - * @deprecated not expressive - will be removed in an upcoming release - */ - @Deprecated - public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { - return smartSignatureHashAlgorithmPolicy(); - } - - /** - * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. - * - * @return static revocation signature hash algorithm policy - */ - public static HashAlgorithmPolicy static2022RevocationSignatureHashAlgorithmPolicy() { - return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( - HashAlgorithm.RIPEMD160, - HashAlgorithm.SHA1, - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512 - )); - } - } - - 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) { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(compressionAlgorithmTag); - return isAcceptable(compressionAlgorithm); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - public boolean isAcceptable(CompressionAlgorithm compressionAlgorithm) { - return acceptableCompressionAlgorithms.contains(compressionAlgorithm); - } - - /** - * Default {@link CompressionAlgorithmPolicy} of PGPainless. - * The default compression algorithm policy accepts any compression algorithm. - * - * @return default algorithm policy - * @deprecated not expressive - might be removed in a future release - */ - @Deprecated - public static CompressionAlgorithmPolicy defaultCompressionAlgorithmPolicy() { - return anyCompressionAlgorithmPolicy(); - } - - /** - * Policy that accepts any known compression algorithm and offers {@link CompressionAlgorithm#ZIP} as - * default algorithm. - * - * @return compression algorithm policy - */ - public static CompressionAlgorithmPolicy anyCompressionAlgorithmPolicy() { - return new CompressionAlgorithmPolicy(CompressionAlgorithm.ZIP, Arrays.asList( - CompressionAlgorithm.UNCOMPRESSED, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.BZIP2, - CompressionAlgorithm.ZLIB - )); - } - } - - public static final class PublicKeyAlgorithmPolicy { - - private final Map algorithmStrengths = new EnumMap<>(PublicKeyAlgorithm.class); - - public PublicKeyAlgorithmPolicy(Map minimalAlgorithmBitStrengths) { - this.algorithmStrengths.putAll(minimalAlgorithmBitStrengths); - } - - public boolean isAcceptable(int algorithmId, int bitStrength) { - try { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(algorithmId); - return isAcceptable(algorithm, bitStrength); - } catch (NoSuchElementException e) { - // Unknown algorithm is not acceptable - return false; - } - } - - public boolean isAcceptable(PublicKeyAlgorithm algorithm, int bitStrength) { - if (!algorithmStrengths.containsKey(algorithm)) { - return false; - } - - int minStrength = algorithmStrengths.get(algorithm); - return bitStrength >= minStrength; - } - - /** - * Return PGPainless' default public key algorithm policy. - * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). - * - * @return default algorithm policy - * @deprecated not expressive - might be removed in a future release - */ - @Deprecated - public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { - return bsi2021PublicKeyAlgorithmPolicy(); - } - - /** - * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). - * - * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, - * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. - * - * @see BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) - * @see BlueKrypt | Cryptographic Key Length Recommendation - * - * @return default algorithm policy - */ - public static PublicKeyAlgorithmPolicy bsi2021PublicKeyAlgorithmPolicy() { - Map minimalBitStrengths = new EnumMap<>(PublicKeyAlgorithm.class); - // §5.4.1 - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_SIGN, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000); - // Note: ElGamal is not mentioned in the BSI document. - // We assume that the requirements are similar to other DH algorithms - minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000); - minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000); - // §5.4.2 - minimalBitStrengths.put(PublicKeyAlgorithm.DSA, 2000); - // §5.4.3 - minimalBitStrengths.put(PublicKeyAlgorithm.ECDSA, 250); - // Note: EdDSA is not mentioned in the BSI document. - // We assume that the requirements are similar to other EC algorithms. - minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA, 250); - // §7.2.1 - minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); - // §7.2.2 - minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); - - return new PublicKeyAlgorithmPolicy(minimalBitStrengths); - } - } - - /** - * Return the {@link NotationRegistry} of PGPainless. - * The notation registry is used to decide, whether a Notation is known or not. - * Background: Critical unknown notations render signatures invalid. - * - * @return Notation registry - */ - public NotationRegistry getNotationRegistry() { - return notationRegistry; - } - - /** - * Return the current {@link AlgorithmSuite} which defines preferred algorithms used during key generation. - * @return current algorithm suite - */ - public @Nonnull AlgorithmSuite getKeyGenerationAlgorithmSuite() { - return keyGenerationAlgorithmSuite; - } - - /** - * Set a custom {@link AlgorithmSuite} which defines preferred algorithms used during key generation. - * - * @param algorithmSuite custom algorithm suite - */ - public void setKeyGenerationAlgorithmSuite(@Nonnull AlgorithmSuite algorithmSuite) { - this.keyGenerationAlgorithmSuite = algorithmSuite; - } - - /** - * Return the level of validation PGPainless shall do on {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. - * By default, this value is {@link SignerUserIdValidationLevel#DISABLED}. - * - * @return the level of validation - */ - public SignerUserIdValidationLevel getSignerUserIdValidationLevel() { - return signerUserIdValidationLevel; - } - - /** - * Specify, how {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signatures shall be validated. - * - * @param signerUserIdValidationLevel level of verification PGPainless shall do on - * {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets. - * @return policy instance - */ - public Policy setSignerUserIdValidationLevel(SignerUserIdValidationLevel signerUserIdValidationLevel) { - if (signerUserIdValidationLevel == null) { - throw new NullPointerException("SignerUserIdValidationLevel cannot be null."); - } - this.signerUserIdValidationLevel = signerUserIdValidationLevel; - return this; - } - - /** - * Enable or disable validation of public key parameters when unlocking private keys. - * Disabled by default. - * When enabled, PGPainless will validate, whether public key parameters have been tampered with. - * This is a countermeasure against possible attacks described in the paper - * "Victory by KO: Attacking OpenPGP Using Key Overwriting" by Lara Bruseghini, Daniel Huigens, and Kenneth G. Paterson. - * Since these attacks are only possible in very special conditions (attacker has access to the encrypted private key), - * and the countermeasures are very costly, they are disabled by default, but can be enabled using this method. - * - * @see KOpenPGP.com - * @param enable boolean - * @return this - */ - public Policy setEnableKeyParameterValidation(boolean enable) { - this.enableKeyParameterValidation = enable; - return this; - } - - /** - * Return true, if countermeasures against the KOpenPGP attacks are enabled, false otherwise. - * - * @return true if countermeasures are enabled, false otherwise. - */ - public boolean isEnableKeyParameterValidation() { - return enableKeyParameterValidation; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt deleted file mode 100644 index e17b9653..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy2.kt +++ /dev/null @@ -1,132 +0,0 @@ -package org.pgpainless.policy - -import org.pgpainless.algorithm.CompressionAlgorithm -import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.util.DateUtil -import java.util.* - -class Policy2 { - - /** - * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the - * given map, if the queried usage date is BEFORE the respective termination date. - * A termination date value of
null
means no termination, resulting in the algorithm being - * acceptable, regardless of usage date. - * - * @param defaultHashAlgorithm default hash algorithm - * @param algorithmTerminationDates map of acceptable algorithms and their termination dates - */ - class HashAlgorithmPolicy( - val defaultHashAlgorithm: HashAlgorithm, - val acceptableHashAlgorithmsAndTerminationDates: Map) { - - /** - * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed in - * the given list, regardless of usage date. - * - * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) - * @param acceptableHashAlgorithms list of acceptable hash algorithms - */ - constructor(defaultHashAlgorithm: HashAlgorithm, acceptableHashAlgorithms: List) : - this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) - - fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date()) - - /** - * Return true, if the given algorithm is acceptable for the given usage date. - * - * @param hashAlgorithm algorithm - * @param referenceTime usage date (e.g. signature creation time) - * - * @return acceptance - */ - fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean { - if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) - return false - val terminationDate = acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] - if (terminationDate == null) { - return true - } - return terminationDate > referenceTime - } - - fun isAcceptable(algorithmId: Int, referenceTime: Date): Boolean { - HashAlgorithm.fromId(algorithmId).let { - if (it == null) { - return false - } - return isAcceptable(it, referenceTime) - } - } - - fun isAcceptable(algorithmId: Int) = isAcceptable(algorithmId, Date()) - - companion object { - @JvmStatic - val smartSignatureHashAlgorithmPolicy = HashAlgorithmPolicy(HashAlgorithm.SHA512, buildMap { - put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) - put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) - put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) - put(HashAlgorithm.SHA224, null) - put(HashAlgorithm.SHA256, null) - put(HashAlgorithm.SHA384, null) - put(HashAlgorithm.SHA512, null) - put(HashAlgorithm.SHA3_256, null) - put(HashAlgorithm.SHA3_512, null) - }) - - /** - * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are acceptable - * according to 2022 standards. - * - * Particularly this policy only accepts algorithms from the SHA2 and SHA3 families. - * - * @return static signature algorithm policy - */ - @JvmStatic - val static2022SignatureHashAlgorithmPolicy = - HashAlgorithmPolicy(HashAlgorithm.SHA512, listOf( - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512, - HashAlgorithm.SHA3_256, - HashAlgorithm.SHA3_512)) - - /** - * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. - * - * @return static revocation signature hash algorithm policy - */ - @JvmStatic - val static2022RevocationSignatureHashAlgorithmPolicy = - HashAlgorithmPolicy(HashAlgorithm.SHA512, listOf( - HashAlgorithm.RIPEMD160, - HashAlgorithm.SHA1, - HashAlgorithm.SHA224, - HashAlgorithm.SHA256, - HashAlgorithm.SHA384, - HashAlgorithm.SHA512, - HashAlgorithm.SHA3_256, - HashAlgorithm.SHA3_512)) - } - } - - class CompressionAlgorithmPolicy( - val defaultCompressionAlgorithm: CompressionAlgorithm, - val acceptableCompressionAlgorithms: List) { - - fun isAcceptable(algorithm: CompressionAlgorithm) = acceptableCompressionAlgorithms.contains(algorithm) - fun isAcceptable(algorithmId: Int) = - } - - companion object { - private var INSTANCE: Policy2? = null - fun getInstance(): Policy2 { - if (INSTANCE == null) { - INSTANCE = Policy2() - } - return INSTANCE!! - } - } -} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java deleted file mode 100644 index dd248151..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Policy regarding used algorithms. - */ -package org.pgpainless.policy; diff --git a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt index a52b38ae..6aa9799e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/PGPainless.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless import org.bouncycastle.openpgp.PGPKeyRing diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/AEADAlgorithm.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/AlgorithmSuite.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/CertificationType.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/CompressionAlgorithm.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/DocumentSignatureType.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/EncryptionPurpose.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/HashAlgorithm.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/KeyFlag.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationState.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/RevocationStateType.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureType.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/StreamEncoding.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/Trustworthiness.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthenticity.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/authentication/CertificateAuthority.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt similarity index 98% rename from pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt index 9c7409de..c5a2a24f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.key import org.bouncycastle.openpgp.PGPKeyRing diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt similarity index 94% rename from pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt index 72c31f4e..fe0c4364 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV4Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.key import org.bouncycastle.openpgp.PGPKeyRing diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV5Fingerprint.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/key/OpenPgpV6Fingerprint.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/key/SubkeyIdentifier.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/key/_64DigitFingerprint.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt new file mode 100644 index 00000000..b7c9f39e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -0,0 +1,359 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.policy + +import org.pgpainless.algorithm.* +import org.pgpainless.util.DateUtil +import org.pgpainless.util.NotationRegistry +import java.util.* + +class Policy( + var signatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, + var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, + var notationRegistry: NotationRegistry) { + + constructor(): this( + HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), + SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(), + SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(), + CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(), + PublicKeyAlgorithmPolicy.bsi2021PublicKeyAlgorithmPolicy(), + NotationRegistry()) + + var keyGenerationAlgorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + var signerUserIdValidationLevel = SignerUserIdValidationLevel.DISABLED + var enableKeyParameterValidation = false + + fun isEnableKeyParameterValidation() = enableKeyParameterValidation + + + /** + * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the + * given map, if the queried usage date is BEFORE the respective termination date. + * A termination date value of
null
means no termination, resulting in the algorithm being + * acceptable, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm + * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + */ + class HashAlgorithmPolicy( + val defaultHashAlgorithm: HashAlgorithm, + val acceptableHashAlgorithmsAndTerminationDates: Map) { + + /** + * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed in + * the given list, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) + * @param acceptableHashAlgorithms list of acceptable hash algorithms + */ + constructor(defaultHashAlgorithm: HashAlgorithm, acceptableHashAlgorithms: List) : + this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) + + fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date()) + + /** + * Return true, if the given algorithm is acceptable for the given usage date. + * + * @param hashAlgorithm algorithm + * @param referenceTime usage date (e.g. signature creation time) + * + * @return acceptance + */ + fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean { + if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) + return false + val terminationDate = acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] ?: return true + return terminationDate > referenceTime + } + + fun isAcceptable(algorithmId: Int) = isAcceptable(algorithmId, Date()) + + fun isAcceptable(algorithmId: Int, referenceTime: Date): Boolean { + val algorithm = HashAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm, referenceTime) + } + + fun defaultHashAlgorithm() = defaultHashAlgorithm + + companion object { + @JvmStatic + fun smartSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( + HashAlgorithm.SHA512, buildMap { + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA224, null) + put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + }) + + /** + * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are acceptable + * according to 2022 standards. + * + * Particularly this policy only accepts algorithms from the SHA2 and SHA3 families. + * + * @return static signature algorithm policy + */ + @JvmStatic + fun static2022SignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( + HashAlgorithm.SHA512, + listOf( + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224) + ) + + /** + * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. + * + * @return static revocation signature hash algorithm policy + */ + @JvmStatic + fun static2022RevocationSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( + HashAlgorithm.SHA512, + listOf( + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224, + HashAlgorithm.SHA1, + HashAlgorithm.RIPEMD160 + ) + ) + } + } + + class SymmetricKeyAlgorithmPolicy( + val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm, + val acceptableSymmetricKeyAlgorithms: List) { + + fun isAcceptable(algorithm: SymmetricKeyAlgorithm) = acceptableSymmetricKeyAlgorithms.contains(algorithm) + fun isAcceptable(algorithmId: Int): Boolean { + val algorithm = SymmetricKeyAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm) + } + + fun selectBest(options: List): SymmetricKeyAlgorithm? { + for (acceptable in acceptableSymmetricKeyAlgorithms) { + if (options.contains(acceptable)) { + return acceptable + } + } + return null + } + + companion object { + + /** + * The default symmetric encryption algorithm policy of PGPainless. + * + * @return default symmetric encryption algorithm policy + * @deprecated not expressive - will be removed in a future release + */ + @JvmStatic + @Deprecated( + "Not expressive - will be removed in a future release", + ReplaceWith("symmetricKeyEncryptionPolicy2022")) + fun defaultSymmetricKeyEncryptionAlgorithmPolicy() = symmetricKeyEncryptionPolicy2022() + + /** + * Policy for symmetric encryption algorithms in the context of message production (encryption). + * This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key encryption algorithm policy + */ + @JvmStatic + fun symmetricKeyEncryptionPolicy2022() = SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_128, + // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish + listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128 + )) + + /** + * The default symmetric decryption algorithm policy of PGPainless. + * + * @return default symmetric decryption algorithm policy + * @deprecated not expressive - will be removed in a future update + */ + @JvmStatic + @Deprecated("not expressive - will be removed in a future update", + ReplaceWith("symmetricKeyDecryptionPolicy2022()")) + fun defaultSymmetricKeyDecryptionAlgorithmPolicy() = symmetricKeyDecryptionPolicy2022() + + /** + * Policy for symmetric key encryption algorithms in the context of message consumption (decryption). + * This suite contains algorithms that are deemed safe to use in 2022. + * + * @return 2022 symmetric key decryption algorithm policy + */ + @JvmStatic + fun symmetricKeyDecryptionPolicy2022() = SymmetricKeyAlgorithmPolicy( + SymmetricKeyAlgorithm.AES_128, + // Reject: Unencrypted, IDEA, TripleDES, Blowfish + listOf( + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128, + SymmetricKeyAlgorithm.CAST5 + )) + } + } + + class CompressionAlgorithmPolicy( + val defaultCompressionAlgorithm: CompressionAlgorithm, + val acceptableCompressionAlgorithms: List) { + + fun isAcceptable(algorithm: CompressionAlgorithm) = acceptableCompressionAlgorithms.contains(algorithm) + fun isAcceptable(algorithmId: Int): Boolean { + val algorithm = CompressionAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm) + } + + fun defaultCompressionAlgorithm() = defaultCompressionAlgorithm + + companion object { + + /** + * Default {@link CompressionAlgorithmPolicy} of PGPainless. + * The default compression algorithm policy accepts any compression algorithm. + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @JvmStatic + @Deprecated("not expressive - might be removed in a future release", + ReplaceWith("anyCompressionAlgorithmPolicy()")) + fun defaultCompressionAlgorithmPolicy() = anyCompressionAlgorithmPolicy() + + /** + * Policy that accepts any known compression algorithm and offers [CompressionAlgorithm.ZIP] as + * default algorithm. + * + * @return compression algorithm policy + */ + @JvmStatic + fun anyCompressionAlgorithmPolicy() = CompressionAlgorithmPolicy( + CompressionAlgorithm.ZIP, + listOf(CompressionAlgorithm.UNCOMPRESSED, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZLIB)) + } + } + + class PublicKeyAlgorithmPolicy(private val algorithmStrengths: Map) { + + fun isAcceptable(algorithm: PublicKeyAlgorithm, bitStrength: Int): Boolean { + return bitStrength >= (algorithmStrengths[algorithm] ?: return false) + } + + fun isAcceptable(algorithmId: Int, bitStrength: Int): Boolean { + val algorithm = PublicKeyAlgorithm.fromId(algorithmId) ?: return false + return isAcceptable(algorithm, bitStrength) + } + + companion object { + + /** + * Return PGPainless' default public key algorithm policy. + * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * + * @return default algorithm policy + * @deprecated not expressive - might be removed in a future release + */ + @JvmStatic + @Deprecated("not expressive - might be removed in a future release", + ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()")) + fun defaultPublicKeyAlgorithmPolicy() = bsi2021PublicKeyAlgorithmPolicy() + + /** + * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * + * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, + * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. + * + * @see BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) + * @see BlueKrypt | Cryptographic Key Length Recommendation + * + * @return default algorithm policy + */ + @JvmStatic + fun bsi2021PublicKeyAlgorithmPolicy() = PublicKeyAlgorithmPolicy(buildMap { + // §5.4.1 + put(PublicKeyAlgorithm.RSA_GENERAL, 2000) + put(PublicKeyAlgorithm.RSA_SIGN, 2000) + put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000) + // Note: ElGamal is not mentioned in the BSI document. + // We assume that the requirements are similar to other DH algorithms + put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000) + put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000) + // §5.4.2 + put(PublicKeyAlgorithm.DSA, 2000) + // §5.4.3 + put(PublicKeyAlgorithm.ECDSA, 250) + // Note: EdDSA is not mentioned in the BSI document. + // We assume that the requirements are similar to other EC algorithms. + put(PublicKeyAlgorithm.EDDSA, 250) + // §7.2.1 + put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) + // §7.2.2 + put(PublicKeyAlgorithm.ECDH, 250) + }) + } + } + + enum class SignerUserIdValidationLevel { + /** + * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. + * This means, that signatures with Signer's User-ID subpackets containing a value that does not match the signer key's + * user-id exactly, will be rejected. + * E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not + * match exactly and is therefore rejected. + */ + STRICT, + + /** + * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature. + */ + DISABLED + } + + companion object { + + @Volatile + private var INSTANCE: Policy? = null + + @JvmStatic + fun getInstance() = INSTANCE ?: synchronized(this) { + INSTANCE ?: Policy().also { INSTANCE = it } + } + } +} \ No newline at end of file From 39c5d12096cf27258ba7bd31092d821fe09b6f4a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Aug 2023 13:28:38 +0200 Subject: [PATCH 072/351] Use IntRange for Trustworthiness range check --- .../kotlin/org/pgpainless/algorithm/Trustworthiness.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt index b1a1b7dd..695632a2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt @@ -62,11 +62,13 @@ class Trustworthiness(amount: Int, depth: Int) { fun canIntroduce(other: Trustworthiness) = canIntroduce(other.depth) companion object { - const val THRESHOLD_FULLY_CONVINCED = 120 // greater or equal is fully trusted const val MARGINALLY_CONVINCED = 60 // default value for marginally convinced const val NOT_TRUSTED = 0 // 0 is not trusted + @JvmStatic + private val validRange = 0..255 + /** * This means that we are fully convinced of the trustworthiness of the key. * @@ -94,7 +96,7 @@ class Trustworthiness(amount: Int, depth: Int) { @JvmStatic private fun capAmount(amount: Int): Int { - if (amount !in 0..255) { + if (amount !in validRange) { throw IllegalArgumentException("Trust amount MUST be a value between 0 and 255") } return amount @@ -102,7 +104,7 @@ class Trustworthiness(amount: Int, depth: Int) { @JvmStatic private fun capDepth(depth: Int): Int { - if (depth !in 0..255) { + if (depth !in validRange) { throw IllegalArgumentException("Trust depth MUST be a value between 0 and 255") } return depth From de73b3b00be4e0e3ddc67c5255b8de333f685cbb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:29:27 +0200 Subject: [PATCH 073/351] Kotlin conversion: CertifyCertificate --- .../key/certification/CertifyCertificate.java | 298 ------------------ .../key/certification/package-info.java | 8 - .../key/certification/CertifyCertificate.kt | 228 ++++++++++++++ 3 files changed, 228 insertions(+), 306 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java deleted file mode 100644 index 5b6c6fe2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/CertifyCertificate.java +++ /dev/null @@ -1,298 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.certification; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -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.CertificationType; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.Trustworthiness; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder; -import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder; -import org.pgpainless.signature.subpackets.CertificationSubpackets; -import org.pgpainless.util.DateUtil; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Date; - -/** - * API for creating certifications and delegations (Signatures) on keys. - * This API can be used to sign another persons OpenPGP key. - * - * A certification over a user-id is thereby used to attest, that the user believes that the user-id really belongs - * to the owner of the certificate. - * A delegation over a key can be used to delegate trust by marking the certificate as a trusted introducer. - */ -public class CertifyCertificate { - - /** - * Create a certification over a User-Id. - * By default, this method will use {@link CertificationType#GENERIC} to create the signature. - * If you need to create another type of certification, use - * {@link #userIdOnCertificate(String, PGPPublicKeyRing, CertificationType)} instead. - * - * @param userId user-id to certify - * @param certificate certificate - * @return API - */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userId, - @Nonnull PGPPublicKeyRing certificate) { - return userIdOnCertificate(userId, certificate, CertificationType.GENERIC); - } - - /** - * Create a certification of the given {@link CertificationType} over a User-Id. - * - * @param userid user-id to certify - * @param certificate certificate - * @param certificationType type of signature - * @return API - */ - public CertificationOnUserId userIdOnCertificate(@Nonnull String userid, - @Nonnull PGPPublicKeyRing certificate, - @Nonnull CertificationType certificationType) { - return new CertificationOnUserId(userid, certificate, certificationType); - } - - /** - * Create a delegation (direct key signature) over a certificate. - * This can be used to mark a certificate as a trusted introducer - * (see {@link #certificate(PGPPublicKeyRing, Trustworthiness)}). - * - * @param certificate certificate - * @return API - */ - public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate) { - return certificate(certificate, null); - } - - /** - * Create a delegation (direct key signature) containing a {@link org.bouncycastle.bcpg.sig.TrustSignature} packet - * over a certificate. - * This can be used to mark a certificate as a trusted introducer. - * - * @param certificate certificate - * @param trustworthiness trustworthiness of the certificate - * @return API - */ - public DelegationOnCertificate certificate(@Nonnull PGPPublicKeyRing certificate, - @Nullable Trustworthiness trustworthiness) { - return new DelegationOnCertificate(certificate, trustworthiness); - } - - public static class CertificationOnUserId { - - private final PGPPublicKeyRing certificate; - private final String userId; - private final CertificationType certificationType; - - CertificationOnUserId(@Nonnull String userId, - @Nonnull PGPPublicKeyRing certificate, - @Nonnull CertificationType certificationType) { - this.userId = userId; - this.certificate = certificate; - this.certificationType = certificationType; - } - - /** - * Create the certification using the given key. - * - * @param certificationKey key used to create the certification - * @param protector protector to unlock the certification key - * @return API - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationOnUserIdWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); - - ThirdPartyCertificationSignatureBuilder sigBuilder = new ThirdPartyCertificationSignatureBuilder( - certificationType.asSignatureType(), secretKey, protector); - - return new CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder); - } - } - - public static class CertificationOnUserIdWithSubpackets { - - private final PGPPublicKeyRing certificate; - private final String userId; - private final ThirdPartyCertificationSignatureBuilder sigBuilder; - - CertificationOnUserIdWithSubpackets(@Nonnull PGPPublicKeyRing certificate, - @Nonnull String userId, - @Nonnull ThirdPartyCertificationSignatureBuilder sigBuilder) { - this.certificate = certificate; - this.userId = userId; - this.sigBuilder = sigBuilder; - } - - /** - * Apply the given signature subpackets and build the certification. - * - * @param subpacketCallback callback to modify the signatures subpackets - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketCallback) - throws PGPException { - sigBuilder.applyCallback(subpacketCallback); - return build(); - } - - /** - * Build the certification signature. - * - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult build() throws PGPException { - PGPSignature signature = sigBuilder.build(certificate, userId); - PGPPublicKeyRing certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature); - return new CertificationResult(certifiedCertificate, signature); - } - } - - public static class DelegationOnCertificate { - - private final PGPPublicKeyRing certificate; - private final Trustworthiness trustworthiness; - - DelegationOnCertificate(@Nonnull PGPPublicKeyRing certificate, - @Nullable Trustworthiness trustworthiness) { - this.certificate = certificate; - this.trustworthiness = trustworthiness; - } - - /** - * Build the delegation using the given certification key. - * - * @param certificationKey key to create the certification with - * @param protector protector to unlock the certification key - * @return API - * @throws PGPException in case of an OpenPGP related error - */ - public DelegationOnCertificateWithSubpackets withKey(@Nonnull PGPSecretKeyRing certificationKey, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - PGPSecretKey secretKey = getCertifyingSecretKey(certificationKey); - - ThirdPartyDirectKeySignatureBuilder sigBuilder = new ThirdPartyDirectKeySignatureBuilder(secretKey, protector); - if (trustworthiness != null) { - sigBuilder.getHashedSubpackets().setTrust(true, trustworthiness.getDepth(), trustworthiness.getAmount()); - } - return new DelegationOnCertificateWithSubpackets(certificate, sigBuilder); - } - } - - public static class DelegationOnCertificateWithSubpackets { - - private final PGPPublicKeyRing certificate; - private final ThirdPartyDirectKeySignatureBuilder sigBuilder; - - DelegationOnCertificateWithSubpackets(@Nonnull PGPPublicKeyRing certificate, - @Nonnull ThirdPartyDirectKeySignatureBuilder sigBuilder) { - this.certificate = certificate; - this.sigBuilder = sigBuilder; - } - - /** - * Apply the given signature subpackets and build the delegation signature. - * - * @param subpacketsCallback callback to modify the signatures subpackets - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult buildWithSubpackets(@Nonnull CertificationSubpackets.Callback subpacketsCallback) - throws PGPException { - sigBuilder.applyCallback(subpacketsCallback); - return build(); - } - - /** - * Build the delegation signature. - * - * @return result - * @throws PGPException in case of an OpenPGP related error - */ - public CertificationResult build() throws PGPException { - PGPPublicKey delegatedKey = certificate.getPublicKey(); - PGPSignature delegation = sigBuilder.build(delegatedKey); - PGPPublicKeyRing delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation); - return new CertificationResult(delegatedCertificate, delegation); - } - } - - public static class CertificationResult { - - private final PGPPublicKeyRing certificate; - private final PGPSignature certification; - - CertificationResult(@Nonnull PGPPublicKeyRing certificate, @Nonnull PGPSignature certification) { - this.certificate = certificate; - this.certification = certification; - } - - /** - * Return the signature. - * - * @return signature - */ - @Nonnull - public PGPSignature getCertification() { - return certification; - } - - /** - * Return the certificate, which now contains the signature. - * - * @return certificate + signature - */ - @Nonnull - public PGPPublicKeyRing getCertifiedCertificate() { - return certificate; - } - } - - private static PGPSecretKey getCertifyingSecretKey(PGPSecretKeyRing certificationKey) { - Date now = DateUtil.now(); - KeyRingInfo info = PGPainless.inspectKeyRing(certificationKey, now); - - // We only support certification-capable primary keys - OpenPgpFingerprint fingerprint = info.getFingerprint(); - PGPPublicKey certificationPubKey = info.getPublicKey(fingerprint); - assert (certificationPubKey != null); - if (!info.isKeyValidlyBound(certificationPubKey.getKeyID())) { - throw new KeyException.RevokedKeyException(fingerprint); - } - - if (!info.isUsableForThirdPartyCertification()) { - throw new KeyException.UnacceptableThirdPartyCertificationKeyException(fingerprint); - } - - Date expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER); - if (expirationDate != null && expirationDate.before(now)) { - throw new KeyException.ExpiredKeyException(fingerprint, expirationDate); - } - - PGPSecretKey secretKey = certificationKey.getSecretKey(certificationPubKey.getKeyID()); - if (secretKey == null) { - throw new KeyException.MissingSecretKeyException(fingerprint, certificationPubKey.getKeyID()); - } - return secretKey; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java deleted file mode 100644 index db2f4857..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/certification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * API for key certifications. - */ -package org.pgpainless.key.certification; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt new file mode 100644 index 00000000..a924e0f3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.certification + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CertificationType +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.Trustworthiness +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.ExpiredKeyException +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.RevokedKeyException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder +import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder +import org.pgpainless.signature.subpackets.CertificationSubpackets +import java.util.* + +/** + * API for creating certifications and delegations (Signatures) on keys. + * This API can be used to sign another persons OpenPGP key. + * + * A certification over a user-id is thereby used to attest, that the user believes that the user-id really belongs + * to the owner of the certificate. + * A delegation over a key can be used to delegate trust by marking the certificate as a trusted introducer. + */ +class CertifyCertificate { + + /** + * Create a certification over a User-Id. + * By default, this method will use [CertificationType.GENERIC] to create the signature. + * + * @param userId user-id to certify + * @param certificate certificate + * @return API + */ + fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing): CertificationOnUserId = + userIdOnCertificate(userId, certificate, CertificationType.GENERIC) + + /** + * Create a certification of the given [CertificationType] over a User-Id. + * + * @param userid user-id to certify + * @param certificate certificate + * @param certificationType type of signature + * @return API + */ + fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing, certificationType: CertificationType) = + CertificationOnUserId(userId, certificate, certificationType) + + /** + * Create a delegation (direct key signature) over a certificate. + * This can be used to mark a certificate as a trusted introducer + * (see [certificate] method with [Trustworthiness] argument). + * + * @param certificate certificate + * @return API + */ + fun certificate(certificate: PGPPublicKeyRing): DelegationOnCertificate = + certificate(certificate, null) + + /** + * Create a delegation (direct key signature) containing a [org.bouncycastle.bcpg.sig.TrustSignature] packet + * over a certificate. + * This can be used to mark a certificate as a trusted introducer. + * + * @param certificate certificate + * @param trustworthiness trustworthiness of the certificate + * @return API + */ + fun certificate(certificate: PGPPublicKeyRing, trustworthiness: Trustworthiness?) = + DelegationOnCertificate(certificate, trustworthiness) + + class CertificationOnUserId( + val userId: String, + val certificate: PGPPublicKeyRing, + val certificationType: CertificationType) { + + /** + * Create the certification using the given key. + * + * @param certificationKey key used to create the certification + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ + fun withKey(certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector): CertificationOnUserIdWithSubpackets { + + val secretKey = getCertifyingSecretKey(certificationKey) + val sigBuilder = ThirdPartyCertificationSignatureBuilder( + certificationType.asSignatureType(), secretKey, protector) + + return CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder) + } + } + + class CertificationOnUserIdWithSubpackets( + val certificate: PGPPublicKeyRing, + val userId: String, + val sigBuilder: ThirdPartyCertificationSignatureBuilder + ) { + + /** + * Apply the given signature subpackets and build the certification. + * + * @param subpacketCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets(subpacketCallback: CertificationSubpackets.Callback): CertificationResult { + sigBuilder.applyCallback(subpacketCallback) + return build() + } + + /** + * Build the certification signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val signature = sigBuilder.build(certificate, userId) + val certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature) + return CertificationResult(certifiedCertificate, signature) + } + } + + class DelegationOnCertificate( + val certificate: PGPPublicKeyRing, + val trustworthiness: Trustworthiness?) { + + /** + * Build the delegation using the given certification key. + * + * @param certificationKey key to create the certification with + * @param protector protector to unlock the certification key + * @return API + * @throws PGPException in case of an OpenPGP related error + */ + fun withKey(certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector): DelegationOnCertificateWithSubpackets { + val secretKey = getCertifyingSecretKey(certificationKey) + val sigBuilder = ThirdPartyDirectKeySignatureBuilder(secretKey, protector) + if (trustworthiness != null) { + sigBuilder.hashedSubpackets.setTrust(true, trustworthiness.depth, trustworthiness.amount) + } + return DelegationOnCertificateWithSubpackets(certificate, sigBuilder) + } + } + + class DelegationOnCertificateWithSubpackets( + val certificate: PGPPublicKeyRing, + val sigBuilder: ThirdPartyDirectKeySignatureBuilder) { + + /** + * Apply the given signature subpackets and build the delegation signature. + * + * @param subpacketsCallback callback to modify the signatures subpackets + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun buildWithSubpackets(subpacketsCallback: CertificationSubpackets.Callback): CertificationResult { + sigBuilder.applyCallback(subpacketsCallback) + return build() + } + + /** + * Build the delegation signature. + * + * @return result + * @throws PGPException in case of an OpenPGP related error + */ + fun build(): CertificationResult { + val delegatedKey = certificate.publicKey + val delegation = sigBuilder.build(delegatedKey) + val delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation) + return CertificationResult(delegatedCertificate, delegation) + } + } + + /** + * Result of a certification operation. + * + * @param certifiedCertificate certificate which now contains the newly created signature + * @param certification the newly created signature + */ + data class CertificationResult( + val certifiedCertificate: PGPPublicKeyRing, + val certification: PGPSignature) + + companion object { + @JvmStatic + private fun getCertifyingSecretKey(certificationKey: PGPSecretKeyRing): PGPSecretKey { + val now = Date() + val info = PGPainless.inspectKeyRing(certificationKey, now) + + val fingerprint = info.fingerprint + val certificationPubKey = info.getPublicKey(fingerprint) + requireNotNull(certificationPubKey) { + "Primary key cannot be null." + } + if (!info.isKeyValidlyBound(certificationPubKey.keyID)) { + throw RevokedKeyException(fingerprint) + } + + if (!info.isUsableForThirdPartyCertification) { + throw KeyException.UnacceptableThirdPartyCertificationKeyException(fingerprint) + } + + val expirationDate = info.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER) + if (expirationDate != null && expirationDate < now) { + throw ExpiredKeyException(fingerprint, expirationDate) + } + + return certificationKey.getSecretKey(certificationPubKey.keyID) + ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyID) + } + } +} \ No newline at end of file From a1a090028d9c5f9ff1ca16b087c511752df0c85c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Aug 2023 16:09:15 +0200 Subject: [PATCH 074/351] Kotlin conversion: PDA --- .../syntax_check/PDA.java | 156 ------------------ .../syntax_check/PDA.kt | 120 ++++++++++++++ 2 files changed, 120 insertions(+), 156 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java deleted file mode 100644 index 7d7cf973..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Stack; - -import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; -import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; - -/** - * Pushdown Automaton for validating context-free languages. - * In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax. - * - * @see OpenPGP Message Syntax - */ -public class PDA { - - private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); - - // right now we implement what rfc4880 specifies. - // TODO: Consider implementing what we proposed here: - // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ - private final Syntax syntax; - private final Stack stack = new Stack<>(); - private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting - private State state; - - /** - * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. - */ - public PDA() { - this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); - } - - /** - * Construct a PDA with a custom {@link Syntax}, initial {@link State} and initial {@link StackSymbol StackSymbols}. - * - * @param syntax syntax - * @param initialState initial state - * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) - */ - public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { - this.syntax = syntax; - this.state = initialState; - for (StackSymbol symbol : initialStack) { - pushStack(symbol); - } - } - - /** - * Process the next {@link InputSymbol}. - * This will either leave the PDA in the next state, or throw a {@link MalformedOpenPgpMessageException} if the - * input symbol is rejected. - * - * @param input input symbol - * @throws MalformedOpenPgpMessageException if the input symbol is rejected - */ - public void next(@Nonnull InputSymbol input) - throws MalformedOpenPgpMessageException { - StackSymbol stackSymbol = popStack(); - try { - Transition transition = syntax.transition(state, input, stackSymbol); - state = transition.getNewState(); - for (StackSymbol item : transition.getPushedItems()) { - pushStack(item); - } - inputs.add(input); - } catch (MalformedOpenPgpMessageException e) { - MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( - "Malformed message: After reading packet sequence " + Arrays.toString(inputs.toArray()) + - ", token '" + input + "' is not allowed." + - "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + - (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); - LOGGER.debug("Invalid input '" + input + "'", wrapped); - throw wrapped; - } - } - - /** - * Return the current state of the PDA. - * - * @return state - */ - public @Nonnull State getState() { - return state; - } - - /** - * Peek at the stack, returning the topmost stack item without changing the stack. - * - * @return topmost stack item, or null if stack is empty - */ - public @Nullable StackSymbol peekStack() { - if (stack.isEmpty()) { - return null; - } - return stack.peek(); - } - - /** - * Return true, if the PDA is in a valid state (the OpenPGP message is valid). - * - * @return true if valid, false otherwise - */ - public boolean isValid() { - return getState() == State.Valid && stack.isEmpty(); - } - - /** - * Throw a {@link MalformedOpenPgpMessageException} if the pda is not in a valid state right now. - * - * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state - */ - public void assertValid() throws MalformedOpenPgpMessageException { - if (!isValid()) { - throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); - } - } - - /** - * Pop an item from the stack. - * - * @return stack item - */ - private StackSymbol popStack() { - if (stack.isEmpty()) { - return null; - } - return stack.pop(); - } - - /** - * Push an item onto the stack. - * - * @param item item - */ - private void pushStack(StackSymbol item) { - stack.push(item); - } - - @Override - public String toString() { - return "State: " + state + " Stack: " + stack; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt new file mode 100644 index 00000000..ae79909f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.slf4j.LoggerFactory + +/** + * Pushdown Automaton for validating context-free languages. + * In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax. + * + * See [OpenPGP Message Syntax](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) + */ +class PDA constructor( + private val syntax: Syntax, + private val stack: ArrayDeque, + private val inputs: MutableList, + private var state: State +) { + + /** + * Construct a PDA with a custom [Syntax], initial [State] and initial [StackSymbols][StackSymbol]. + * + * @param syntax syntax + * @param initialState initial state + * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) + */ + constructor(syntax: Syntax, initialState: State, vararg initialStack: StackSymbol): this ( + syntax, ArrayDeque(initialStack.toList().reversed()), mutableListOf(), initialState) + + /** + * Default constructor which initializes the PDA to work with the [OpenPgpMessageSyntax]. + */ + constructor(): this(OpenPgpMessageSyntax(), State.OpenPgpMessage, StackSymbol.terminus, StackSymbol.msg) + + /** + * Process the next [InputSymbol]. + * This will either leave the PDA in the next state, or throw a [MalformedOpenPgpMessageException] if the + * input symbol is rejected. + * + * @param input input symbol + * @throws MalformedOpenPgpMessageException if the input symbol is rejected + */ + fun next(input: InputSymbol) { + val stackSymbol = popStack() + try { + val transition = syntax.transition(state, input, stackSymbol) + state = transition.newState + for (item in transition.pushedItems) { + pushStack(item) + } + inputs.add(input) + } catch (e: MalformedOpenPgpMessageException) { + val stackFormat = if (stackSymbol != null) { + "${stack.joinToString()}||$stackSymbol" + } else { + stack.joinToString() + } + val wrapped = MalformedOpenPgpMessageException( + "Malformed message: After reading packet sequence ${inputs.joinToString()}, token '$input' is not allowed.\n" + + "No transition from state '$state' with stack $stackFormat", e) + LOGGER.debug("Invalid input '$input'", wrapped) + throw wrapped + } + } + + /** + * Peek at the stack, returning the topmost stack item without changing the stack. + * + * @return topmost stack item, or null if stack is empty + */ + fun peekStack(): StackSymbol? = stack.firstOrNull() + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + fun isValid(): Boolean = state == State.Valid && stack.isEmpty() + + /** + * Throw a [MalformedOpenPgpMessageException] if the pda is not in a valid state right now. + * + * @throws MalformedOpenPgpMessageException if the pda is not in an acceptable state + */ + fun assertValid() { + if (!isValid()) { + throw MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: ${toString()}") + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private fun popStack(): StackSymbol? { + return stack.removeFirstOrNull() + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private fun pushStack(item: StackSymbol) { + stack.addFirst(item) + } + + override fun toString(): String { + return "State: $state Stack: ${stack.joinToString()}" + } + + companion object { + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(PDA::class.java) + } +} \ No newline at end of file From 603f07d014dc7574ec26a1f1e06b113a55141dbe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Aug 2023 16:33:06 +0200 Subject: [PATCH 075/351] Kotlin conversion: Syntax checking --- .../syntax_check/OpenPgpMessageSyntax.java | 142 ------------------ .../syntax_check/Syntax.java | 33 ---- .../syntax_check/Transition.java | 48 ------ .../syntax_check/package-info.java | 8 - .../syntax_check/InputSymbol.kt} | 24 ++- .../syntax_check/OpenPgpMessageSyntax.kt | 88 +++++++++++ .../syntax_check/StackSymbol.kt} | 8 +- .../syntax_check/State.kt} | 8 +- .../syntax_check/Syntax.kt | 30 ++++ .../syntax_check/Transition.kt | 22 +++ 10 files changed, 157 insertions(+), 254 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java rename pgpainless-core/src/main/{java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java => kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt} (56%) create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt rename pgpainless-core/src/main/{java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java => kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt} (65%) rename pgpainless-core/src/main/{java/org/pgpainless/decryption_verification/syntax_check/State.java => kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt} (55%) create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java deleted file mode 100644 index 9d20e0a8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * This class describes the syntax for OpenPGP messages as specified by rfc4880. - * - * @see - * rfc4880 - §11.3. OpenPGP Messages - * @see - * Blog post about theoretic background and translation of grammar to PDA syntax - * @see - * Blog post about practically implementing the PDA for packet syntax validation - */ -public class OpenPgpMessageSyntax implements Syntax { - - @Override - public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (from) { - case OpenPgpMessage: - return fromOpenPgpMessage(input, stackItem); - case LiteralMessage: - return fromLiteralMessage(input, stackItem); - case CompressedMessage: - return fromCompressedMessage(input, stackItem); - case EncryptedMessage: - return fromEncryptedMessage(input, stackItem); - case Valid: - return fromValid(input, stackItem); - } - - throw new MalformedOpenPgpMessageException(from, input, stackItem); - } - - @Nonnull - Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - if (stackItem != StackSymbol.msg) { - throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); - } - - switch (input) { - case LiteralData: - return new Transition(State.LiteralMessage); - - case Signature: - return new Transition(State.OpenPgpMessage, StackSymbol.msg); - - case OnePassSignature: - return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); - - case CompressedData: - return new Transition(State.CompressedMessage); - - case EncryptedData: - return new Transition(State.EncryptedMessage); - - case EndOfSequence: - default: - throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); - } - } - - @Nonnull - Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.LiteralMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); - } - - @Nonnull - Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.CompressedMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); - } - - @Nonnull - Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - switch (input) { - case Signature: - if (stackItem == StackSymbol.ops) { - return new Transition(State.EncryptedMessage); - } - break; - - case EndOfSequence: - if (stackItem == StackSymbol.terminus) { - return new Transition(State.Valid); - } - break; - } - - throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); - } - - @Nonnull - Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException { - if (input == InputSymbol.EndOfSequence) { - // allow subsequent read() calls. - return new Transition(State.Valid); - } - // There is no applicable transition rule out of Valid - throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java deleted file mode 100644 index 2f3d0a57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import org.pgpainless.exception.MalformedOpenPgpMessageException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * This interface can be used to define a custom syntax for the {@link PDA}. - */ -public interface Syntax { - - /** - * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
- * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. - * The resulting {@link Transition} contains the new {@link State}, as well as a list of - * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. - * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case - * the {@link InputSymbol} must be considered illegal. - * - * @param from current state of the PDA - * @param input input symbol - * @param stackItem item that got popped from the top of the stack - * @return applicable transition rule containing the new state and pushed stack symbols - * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) - */ - @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) - throws MalformedOpenPgpMessageException; -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java deleted file mode 100644 index ab0db5ef..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.syntax_check; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Result of applying a transition rule. - * Transition rules can be described by implementing the {@link Syntax} interface. - */ -public class Transition { - - private final List pushedItems = new ArrayList<>(); - private final State newState; - - public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { - this.newState = newState; - this.pushedItems.addAll(Arrays.asList(pushedItems)); - } - - /** - * Return the new {@link State} that is reached by applying the transition. - * - * @return new state - */ - @Nonnull - public State getNewState() { - return newState; - } - - /** - * Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack - * by applying the transition. - * The list contains items in the order in which they are pushed onto the stack. - * The list may be empty. - * - * @return list of items to be pushed onto the stack - */ - @Nonnull - public List getPushedItems() { - return new ArrayList<>(pushedItems); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java deleted file mode 100644 index 4df6af5a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. - */ -package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt similarity index 56% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt index 854c3305..f189c89f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt @@ -1,36 +1,30 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.syntax_check; +package org.pgpainless.decryption_verification.syntax_check -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignatureList; - -public enum InputSymbol { +enum class InputSymbol { /** - * A {@link PGPLiteralData} packet. + * A [PGPLiteralData] packet. */ LiteralData, /** - * A {@link PGPSignatureList} object. + * A [PGPSignatureList] object. */ Signature, /** - * A {@link PGPOnePassSignatureList} object. + * A [PGPOnePassSignatureList] object. */ OnePassSignature, /** - * A {@link PGPCompressedData} packet. + * A [PGPCompressedData] packet. * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify * its nested packet sequence. */ CompressedData, /** - * A {@link PGPEncryptedDataList} object. + * A [PGPEncryptedDataList] object. * This object combines multiple ESKs and the corresponding Symmetrically Encrypted * (possibly Integrity Protected) Data packet. */ @@ -42,4 +36,4 @@ public enum InputSymbol { * (e.g. the end of a Compressed Data packet). */ EndOfSequence -} +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt new file mode 100644 index 00000000..c25476b2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException + +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * See [rfc4880 - §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) + * See [Blog post about theoretic background and translation of grammar to PDA syntax](https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/) + * See [Blog post about practically implementing the PDA for packet syntax validation](https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/) + */ +class OpenPgpMessageSyntax : Syntax { + + override fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition { + return when (from) { + State.OpenPgpMessage -> fromOpenPgpMessage(input, stackItem) + State.LiteralMessage -> fromLiteralMessage(input, stackItem) + State.CompressedMessage -> fromCompressedMessage(input, stackItem) + State.EncryptedMessage -> fromEncryptedMessage(input, stackItem) + State.Valid -> fromValid(input, stackItem) + else -> throw MalformedOpenPgpMessageException(from, input, stackItem) + } + } + + fun fromOpenPgpMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (stackItem !== StackSymbol.msg) { + throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) + } + return when (input) { + InputSymbol.LiteralData -> Transition(State.LiteralMessage) + InputSymbol.Signature -> Transition(State.OpenPgpMessage, StackSymbol.msg) + InputSymbol.OnePassSignature -> Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg) + InputSymbol.CompressedData -> Transition(State.CompressedMessage) + InputSymbol.EncryptedData -> Transition(State.EncryptedMessage) + InputSymbol.EndOfSequence -> throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) + else -> throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) + } + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromLiteralMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { + return Transition(State.LiteralMessage) + } + if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { + return Transition(State.Valid) + } + + throw MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromCompressedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { + return Transition(State.CompressedMessage) + } + if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { + return Transition(State.Valid) + } + + throw MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromEncryptedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { + return Transition(State.EncryptedMessage) + } + if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { + return Transition(State.Valid) + } + + throw MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem) + } + + @Throws(MalformedOpenPgpMessageException::class) + fun fromValid(input: InputSymbol, stackItem: StackSymbol?): Transition { + if (input == InputSymbol.EndOfSequence) { + // allow subsequent read() calls. + return Transition(State.Valid) + } + throw MalformedOpenPgpMessageException(State.Valid, input, stackItem) + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt similarity index 65% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt index 120458e5..009a86d4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.syntax_check; +package org.pgpainless.decryption_verification.syntax_check -public enum StackSymbol { +enum class StackSymbol { /** * OpenPGP Message. */ @@ -17,4 +17,4 @@ public enum StackSymbol { * Special symbol representing the end of the message. */ terminus -} +} \ No newline at end of file diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt similarity index 55% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt index 9dee9af1..5c3e4906 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt @@ -1,16 +1,16 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification.syntax_check; +package org.pgpainless.decryption_verification.syntax_check /** * Set of states of the automaton. */ -public enum State { +enum class State { OpenPgpMessage, LiteralMessage, CompressedMessage, EncryptedMessage, Valid -} +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt new file mode 100644 index 00000000..16f0445b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +import org.pgpainless.exception.MalformedOpenPgpMessageException + +/** + * This interface can be used to define a custom syntax for the [PDA]. + */ +interface Syntax { + + /** + * Describe a transition rule from [State]
from
for [InputSymbol]
input
+ * with [StackSymbol]
stackItem
from the top of the [PDAs][PDA] stack. + * The resulting [Transition] contains the new [State], as well as a list of + * [StackSymbols][StackSymbol] that get pushed onto the stack by the transition rule. + * If there is no applicable rule, a [MalformedOpenPgpMessageException] is thrown, since in this case + * the [InputSymbol] must be considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + */ + @Throws(MalformedOpenPgpMessageException::class) + fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt new file mode 100644 index 00000000..5bc6dfc6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check + +/** + * Result of applying a transition rule. + * Transition rules can be described by implementing the [Syntax] interface. + * + * @param newState new [State] that is reached by applying the transition. + * @param pushedItems list of [StackSymbol] that are pushed onto the stack by applying the transition. + * The list contains items in the order in which they are pushed onto the stack. + * The list may be empty. + */ +class Transition private constructor( + val pushedItems: List, + val newState: State +) { + + constructor(newState: State, vararg pushedItems: StackSymbol): this(pushedItems.toList(), newState) +} \ No newline at end of file From 1ab5377f708c5fa048e32de65a4c98c6d11a5aa6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Aug 2023 16:35:40 +0200 Subject: [PATCH 076/351] Rename syntax checker enums to screaming snake case --- .../OpenPgpMessageInputStream.java | 18 ++-- .../syntax_check/InputSymbol.kt | 12 +-- .../syntax_check/OpenPgpMessageSyntax.kt | 64 +++++------ .../syntax_check/PDA.kt | 4 +- .../syntax_check/StackSymbol.kt | 6 +- .../syntax_check/State.kt | 10 +- .../syntax_check/PDATest.java | 102 +++++++++--------- 7 files changed, 108 insertions(+), 108 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java index b9ead7e7..66ad1edd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -342,7 +342,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processLiteralData() throws IOException { LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.LiteralData); + syntaxVerifier.next(InputSymbol.LITERAL_DATA); PGPLiteralData literalData = packetInputStream.readLiteralData(); // Extract Metadata this.metadata.setChild(new MessageMetadata.LiteralData( @@ -354,7 +354,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputSymbol.CompressedData); + syntaxVerifier.next(InputSymbol.COMPRESSED_DATA); signatures.enterNesting(); PGPCompressedData compressedData = packetInputStream.readCompressedData(); // Extract Metadata @@ -368,7 +368,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputSymbol.OnePassSignature); + syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE); PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + " at depth " + metadata.depth + " encountered"); @@ -377,8 +377,8 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private void processSignature() throws PGPException, IOException { // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; - syntaxVerifier.next(InputSymbol.Signature); + boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.OPS; + syntaxVerifier.next(InputSymbol.SIGNATURE); PGPSignature signature; try { signature = packetInputStream.readSignature(); @@ -404,7 +404,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { private boolean processEncryptedData() throws IOException, PGPException { LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.EncryptedData); + syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA); PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); if (!encDataList.isIntegrityProtected()) { @@ -749,7 +749,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { throws IOException { if (nestedInputStream == null) { if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.EndOfSequence); + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); syntaxVerifier.assertValid(); } return -1; @@ -780,7 +780,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { super.close(); if (closed) { if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.EndOfSequence); + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); syntaxVerifier.assertValid(); } return; @@ -799,7 +799,7 @@ public class OpenPgpMessageInputStream extends DecryptionStream { } if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.EndOfSequence); + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); syntaxVerifier.assertValid(); packetInputStream.close(); } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt index f189c89f..133bfcb3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt @@ -8,32 +8,32 @@ enum class InputSymbol { /** * A [PGPLiteralData] packet. */ - LiteralData, + LITERAL_DATA, /** * A [PGPSignatureList] object. */ - Signature, + SIGNATURE, /** * A [PGPOnePassSignatureList] object. */ - OnePassSignature, + ONE_PASS_SIGNATURE, /** * A [PGPCompressedData] packet. * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify * its nested packet sequence. */ - CompressedData, + COMPRESSED_DATA, /** * A [PGPEncryptedDataList] object. * This object combines multiple ESKs and the corresponding Symmetrically Encrypted * (possibly Integrity Protected) Data packet. */ - EncryptedData, + ENCRYPTED_DATA, /** * Marks the end of a (sub-) sequence. * This input is given if the end of an OpenPGP message is reached. * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents * (e.g. the end of a Compressed Data packet). */ - EndOfSequence + END_OF_SEQUENCE } \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt index c25476b2..56bd9a77 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt @@ -17,72 +17,72 @@ class OpenPgpMessageSyntax : Syntax { override fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition { return when (from) { - State.OpenPgpMessage -> fromOpenPgpMessage(input, stackItem) - State.LiteralMessage -> fromLiteralMessage(input, stackItem) - State.CompressedMessage -> fromCompressedMessage(input, stackItem) - State.EncryptedMessage -> fromEncryptedMessage(input, stackItem) - State.Valid -> fromValid(input, stackItem) + State.OPENPGP_MESSAGE -> fromOpenPgpMessage(input, stackItem) + State.LITERAL_MESSAGE -> fromLiteralMessage(input, stackItem) + State.COMPRESSED_MESSAGE -> fromCompressedMessage(input, stackItem) + State.ENCRYPTED_MESSAGE -> fromEncryptedMessage(input, stackItem) + State.VALID -> fromValid(input, stackItem) else -> throw MalformedOpenPgpMessageException(from, input, stackItem) } } fun fromOpenPgpMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { - if (stackItem !== StackSymbol.msg) { - throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) + if (stackItem !== StackSymbol.MSG) { + throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) } return when (input) { - InputSymbol.LiteralData -> Transition(State.LiteralMessage) - InputSymbol.Signature -> Transition(State.OpenPgpMessage, StackSymbol.msg) - InputSymbol.OnePassSignature -> Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg) - InputSymbol.CompressedData -> Transition(State.CompressedMessage) - InputSymbol.EncryptedData -> Transition(State.EncryptedMessage) - InputSymbol.EndOfSequence -> throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) - else -> throw MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem) + InputSymbol.LITERAL_DATA -> Transition(State.LITERAL_MESSAGE) + InputSymbol.SIGNATURE -> Transition(State.OPENPGP_MESSAGE, StackSymbol.MSG) + InputSymbol.ONE_PASS_SIGNATURE -> Transition(State.OPENPGP_MESSAGE, StackSymbol.OPS, StackSymbol.MSG) + InputSymbol.COMPRESSED_DATA -> Transition(State.COMPRESSED_MESSAGE) + InputSymbol.ENCRYPTED_DATA -> Transition(State.ENCRYPTED_MESSAGE) + InputSymbol.END_OF_SEQUENCE -> throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) + else -> throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) } } @Throws(MalformedOpenPgpMessageException::class) fun fromLiteralMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { - if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { - return Transition(State.LiteralMessage) + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.LITERAL_MESSAGE) } - if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { - return Transition(State.Valid) + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) } - throw MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem) + throw MalformedOpenPgpMessageException(State.LITERAL_MESSAGE, input, stackItem) } @Throws(MalformedOpenPgpMessageException::class) fun fromCompressedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { - if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { - return Transition(State.CompressedMessage) + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.COMPRESSED_MESSAGE) } - if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { - return Transition(State.Valid) + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) } - throw MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem) + throw MalformedOpenPgpMessageException(State.COMPRESSED_MESSAGE, input, stackItem) } @Throws(MalformedOpenPgpMessageException::class) fun fromEncryptedMessage(input: InputSymbol, stackItem: StackSymbol?): Transition { - if (input == InputSymbol.Signature && stackItem == StackSymbol.ops) { - return Transition(State.EncryptedMessage) + if (input == InputSymbol.SIGNATURE && stackItem == StackSymbol.OPS) { + return Transition(State.ENCRYPTED_MESSAGE) } - if (input == InputSymbol.EndOfSequence && stackItem == StackSymbol.terminus) { - return Transition(State.Valid) + if (input == InputSymbol.END_OF_SEQUENCE && stackItem == StackSymbol.TERMINUS) { + return Transition(State.VALID) } - throw MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem) + throw MalformedOpenPgpMessageException(State.ENCRYPTED_MESSAGE, input, stackItem) } @Throws(MalformedOpenPgpMessageException::class) fun fromValid(input: InputSymbol, stackItem: StackSymbol?): Transition { - if (input == InputSymbol.EndOfSequence) { + if (input == InputSymbol.END_OF_SEQUENCE) { // allow subsequent read() calls. - return Transition(State.Valid) + return Transition(State.VALID) } - throw MalformedOpenPgpMessageException(State.Valid, input, stackItem) + throw MalformedOpenPgpMessageException(State.VALID, input, stackItem) } } \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt index ae79909f..b1949917 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt @@ -33,7 +33,7 @@ class PDA constructor( /** * Default constructor which initializes the PDA to work with the [OpenPgpMessageSyntax]. */ - constructor(): this(OpenPgpMessageSyntax(), State.OpenPgpMessage, StackSymbol.terminus, StackSymbol.msg) + constructor(): this(OpenPgpMessageSyntax(), State.OPENPGP_MESSAGE, StackSymbol.TERMINUS, StackSymbol.MSG) /** * Process the next [InputSymbol]. @@ -78,7 +78,7 @@ class PDA constructor( * * @return true if valid, false otherwise */ - fun isValid(): Boolean = state == State.Valid && stack.isEmpty() + fun isValid(): Boolean = state == State.VALID && stack.isEmpty() /** * Throw a [MalformedOpenPgpMessageException] if the pda is not in a valid state right now. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt index 009a86d4..960e5eba 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt @@ -8,13 +8,13 @@ enum class StackSymbol { /** * OpenPGP Message. */ - msg, + MSG, /** * OnePassSignature (in case of BC this represents a OnePassSignatureList). */ - ops, + OPS, /** * Special symbol representing the end of the message. */ - terminus + TERMINUS } \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt index 5c3e4906..8e1f682c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt @@ -8,9 +8,9 @@ package org.pgpainless.decryption_verification.syntax_check * Set of states of the automaton. */ enum class State { - OpenPgpMessage, - LiteralMessage, - CompressedMessage, - EncryptedMessage, - Valid + OPENPGP_MESSAGE, + LITERAL_MESSAGE, + COMPRESSED_MESSAGE, + ENCRYPTED_MESSAGE, + VALID } \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java index d0486a3e..10f8dceb 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -21,8 +21,8 @@ public class PDATest { @Test public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -35,10 +35,10 @@ public class PDATest { @Test public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -52,9 +52,9 @@ public class PDATest { @Test public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.Signature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -68,11 +68,11 @@ public class PDATest { @Test public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.COMPRESSED_DATA); // Here would be a nested PDA for the LiteralData packet - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -80,105 +80,105 @@ public class PDATest { @Test public void testOPSSignedEncryptedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.EncryptedData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.ENCRYPTED_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @Test public void anyInputAfterEOSIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.END_OF_SEQUENCE); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.ENCRYPTED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.ENCRYPTED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testTwoLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LiteralData)); + () -> check.next(InputSymbol.LITERAL_DATA)); } @Test public void testTrailingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSAloneIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.ONE_PASS_SIGNATURE); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testOPSLitWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.Signature)); + () -> check.next(InputSymbol.SIGNATURE)); } @Test public void testOPSCompressedDataWithMissingSigIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.EndOfSequence)); + () -> check.next(InputSymbol.END_OF_SEQUENCE)); } @Test public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { PDA check = new PDA(); - check.next(InputSymbol.CompressedData); + check.next(InputSymbol.COMPRESSED_DATA); assertThrows(MalformedOpenPgpMessageException.class, - () -> check.next(InputSymbol.LiteralData)); + () -> check.next(InputSymbol.LITERAL_DATA)); } @Test public void testOPSWithPrependedSigIsValid() { PDA check = new PDA(); - check.next(InputSymbol.Signature); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } @@ -186,11 +186,11 @@ public class PDATest { @Test public void testPrependedSigInsideOPSSignedMessageIsValid() { PDA check = new PDA(); - check.next(InputSymbol.OnePassSignature); - check.next(InputSymbol.Signature); - check.next(InputSymbol.LiteralData); - check.next(InputSymbol.Signature); - check.next(InputSymbol.EndOfSequence); + check.next(InputSymbol.ONE_PASS_SIGNATURE); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.LITERAL_DATA); + check.next(InputSymbol.SIGNATURE); + check.next(InputSymbol.END_OF_SEQUENCE); assertTrue(check.isValid()); } From fca5c88d0956d17621e90a81661a03314df61e87 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Aug 2023 13:37:23 +0200 Subject: [PATCH 077/351] Kotlin conversion: OpenPgpMessageInputStream --- .../OpenPgpMessageInputStream.java | 1139 ----------------- .../OpenPgpMessageInputStream.kt | 903 +++++++++++++ 2 files changed, 903 insertions(+), 1139 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java deleted file mode 100644 index 66ad1edd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java +++ /dev/null @@ -1,1139 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.Stack; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.UnsupportedPacketVersionException; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSessionKeyEncryptedData; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.util.io.TeeInputStream; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.OpenPgpPacket; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.decryption_verification.syntax_check.InputSymbol; -import org.pgpainless.decryption_verification.syntax_check.PDA; -import org.pgpainless.decryption_verification.syntax_check.StackSymbol; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -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.key.protection.UnlockSecretKey; -import org.pgpainless.key.util.KeyIdUtil; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.signature.consumer.SignatureCheck; -import org.pgpainless.signature.consumer.SignatureValidator; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class OpenPgpMessageInputStream extends DecryptionStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); - - // Options to consume the data - protected final ConsumerOptions options; - - private final Policy policy; - // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message - protected final PDA syntaxVerifier = new PDA(); - // InputStream of OpenPGP packets - protected TeeBCPGInputStream packetInputStream; - // InputStream of a data packet containing nested data - protected InputStream nestedInputStream; - - private boolean closed = false; - - private final Signatures signatures; - private final MessageMetadata.Layer metadata; - - /** - * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of - * OpenPGP messages and signatures. - * This constructor will use the global PGPainless {@link Policy}. - * - * @param inputStream underlying input stream - * @param options options for consuming the stream - * @return input stream that consumes OpenPGP messages - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - */ - public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws IOException, PGPException { - return create(inputStream, options, PGPainless.getPolicy()); - } - - /** - * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of - * OpenPGP messages and signatures. - * This factory method takes a custom {@link Policy} instead of using the global policy object. - * - * @param inputStream underlying input stream containing the OpenPGP message - * @param options options for consuming the message - * @param policy policy for acceptable algorithms etc. - * @return input stream that consumes OpenPGP messages - * - * @throws PGPException in case of an OpenPGP error - * @throws IOException in case of an IO error - */ - public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull Policy policy) - throws PGPException, IOException { - return create(inputStream, options, new MessageMetadata.Message(), policy); - } - - protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) - throws IOException, PGPException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - openPgpIn.reset(); - - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - return new OpenPgpMessageInputStream(Type.non_openpgp, - openPgpIn, options, metadata, policy); - } - - if (openPgpIn.isBinaryOpenPgp()) { - // Simply consume OpenPGP message - return new OpenPgpMessageInputStream(Type.standard, - openPgpIn, options, metadata, policy); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - if (armorIn.isClearText()) { - ((MessageMetadata.Message) metadata).cleartextSigned = true; - return new OpenPgpMessageInputStream(Type.cleartext_signed, - armorIn, options, metadata, policy); - } else { - // Simply consume dearmored OpenPGP message - return new OpenPgpMessageInputStream(Type.standard, - armorIn, options, metadata, policy); - } - } else { - throw new AssertionError("Cannot deduce type of data."); - } - } - - protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) - throws PGPException, IOException { - super(); - - this.policy = policy; - this.options = options; - this.metadata = metadata; - this.signatures = new Signatures(options); - - // Add detached signatures only on the outermost OpenPgpMessageInputStream - if (metadata instanceof MessageMetadata.Message) { - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - } - - // tee out packet bytes for signature verification - packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); - - // *omnomnom* - consumePackets(); - } - - enum Type { - standard, - cleartext_signed, - non_openpgp - } - - protected OpenPgpMessageInputStream(@Nonnull Type type, - @Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options, - @Nonnull MessageMetadata.Layer metadata, - @Nonnull Policy policy) throws PGPException, IOException { - super(); - this.policy = policy; - this.options = options; - this.metadata = metadata; - this.signatures = new Signatures(options); - - if (metadata instanceof MessageMetadata.Message) { - this.signatures.addDetachedSignatures(options.getDetachedSignatures()); - } - - switch (type) { - - // Binary OpenPGP Message - case standard: - // tee out packet bytes for signature verification - packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); - - // *omnomnom* - consumePackets(); - break; - - // Cleartext Signature Framework (probably signed message) - case cleartext_signed: - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList detachedSignatures = ClearsignedMessageUtil - .detachSignaturesFromInbandClearsignedMessage( - inputStream, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : detachedSignatures) { - signatures.addDetachedSignature(signature); - } - - options.forceNonOpenPgpData(); - nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); - break; - - // Non-OpenPGP Data (e.g. detached signature verification) - case non_openpgp: - packetInputStream = null; - nestedInputStream = new TeeInputStream(inputStream, this.signatures); - break; - } - } - - /** - * Consume OpenPGP packets from the current {@link BCPGInputStream}. - * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, - * set
nestedInputStream
to the nested stream and breaks the loop. - * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another - * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. - * Once the nested data is processed, this method is called again to consume the remainder - * of packets following the nested data packet. - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted - * due to missing decryption methods (no key, no password, no sessionkey) - * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which - * does not follow the packet syntax of RFC4880. - */ - private void consumePackets() - throws IOException, PGPException { - OpenPgpPacket nextPacket; - if (packetInputStream == null) { - return; - } - - loop: // we break this when we enter nested packets and later resume - while ((nextPacket = packetInputStream.nextPacketTag()) != null) { - signatures.nextPacket(nextPacket); - switch (nextPacket) { - - // Literal Data - the literal data content is the new input stream - case LIT: - processLiteralData(); - break loop; - - // Compressed Data - the content contains another OpenPGP message - case COMP: - processCompressedData(); - break loop; - - // One Pass Signature - case OPS: - processOnePassSignature(); - break; - - // Signature - either prepended to the message, or corresponding to a One Pass Signature - case SIG: - processSignature(); - break; - - // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) - case PKESK: - case SKESK: - case SED: - case SEIPD: - if (processEncryptedData()) { - // Successfully decrypted, enter nested content - break loop; - } - - throw new MissingDecryptionMethodException("No working decryption method found."); - - // Marker Packets need to be skipped and ignored - case MARKER: - LOGGER.debug("Skipping Marker Packet"); - packetInputStream.readMarker(); - break; - - // Key Packets are illegal in this context - case SK: - case PK: - case SSK: - case PSK: - case TRUST: - case UID: - case UATTR: - throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); - - // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this - // packet out of order - case MDC: - throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); - - // Experimental Packets are not supported - case EXP_1: - case EXP_2: - case EXP_3: - case EXP_4: - throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); - } - } - } - - private void processLiteralData() throws IOException { - LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.LITERAL_DATA); - PGPLiteralData literalData = packetInputStream.readLiteralData(); - // Extract Metadata - this.metadata.setChild(new MessageMetadata.LiteralData( - literalData.getFileName(), - literalData.getModificationTime(), - StreamEncoding.requireFromCode(literalData.getFormat()))); - - nestedInputStream = literalData.getDataStream(); - } - - private void processCompressedData() throws IOException, PGPException { - syntaxVerifier.next(InputSymbol.COMPRESSED_DATA); - signatures.enterNesting(); - PGPCompressedData compressedData = packetInputStream.readCompressedData(); - // Extract Metadata - MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( - CompressionAlgorithm.requireFromId(compressedData.getAlgorithm()), - metadata.depth + 1); - - LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); - InputStream decompressed = compressedData.getDataStream(); - nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); - } - - private void processOnePassSignature() throws PGPException, IOException { - syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE); - PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); - LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + - " at depth " + metadata.depth + " encountered"); - signatures.addOnePassSignature(onePassSignature); - } - - private void processSignature() throws PGPException, IOException { - // true if Signature corresponds to OnePassSignature - boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.OPS; - syntaxVerifier.next(InputSymbol.SIGNATURE); - PGPSignature signature; - try { - signature = packetInputStream.readSignature(); - } catch (UnsupportedPacketVersionException e) { - LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); - return; - } - - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (isSigForOPS) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + - KeyIdUtil.formatKeyId(keyId) + - " at depth " + metadata.depth + " encountered"); - signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata, policy); - } else { - LOGGER.debug("Prepended Signature Packet by key " + - KeyIdUtil.formatKeyId(keyId) + - " at depth " + metadata.depth + " encountered"); - signatures.addPrependedSignature(signature); - } - } - - private boolean processEncryptedData() throws IOException, PGPException { - LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); - syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA); - PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); - - if (!encDataList.isIntegrityProtected()) { - LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected."); - if (!options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - } - - SortedESKs esks = new SortedESKs(encDataList); - LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has " + - esks.skesks.size() + " SKESK(s) and " + - (esks.pkesks.size() + esks.anonPkesks.size()) + " PKESK(s) from which " + - esks.anonPkesks.size() + " PKESK(s) have an anonymous recipient"); - - // Try custom decryptor factories - for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { - LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); - PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - // find matching PKESK - if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { - continue; - } - - // attempt decryption - if (decryptPKESKAndStream(esks, subkeyIdentifier, decryptorFactory, pkesk)) { - return true; - } - } - } - - // Try provided session key - if (options.getSessionKey() != null) { - LOGGER.debug("Attempt decryption with provided session key"); - SessionKey sessionKey = options.getSessionKey(); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - - PGPSessionKeyEncryptedData sessionKeyEncryptedData = encDataList.extractSessionKeyEncryptedData(); - try { - InputStream decrypted = sessionKeyEncryptedData.getDataStream(decryptorFactory); - encryptedData.sessionKey = sessionKey; - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, sessionKeyEncryptedData, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - LOGGER.debug("Successfully decrypted data with provided session key"); - return true; - } catch (PGPException e) { - // Session key mismatch? - LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); - } - } - - // Try passwords - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - for (PGPPBEEncryptedData skesk : esks.skesks) { - LOGGER.debug("Attempt decryption with provided passphrase"); - SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); - try { - throwIfUnacceptable(encapsulationAlgorithm); - } catch (UnacceptableAlgorithmException e) { - LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); - continue; - } - - PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { - return true; - } - } - } - - List> postponedDueToMissingPassphrase = new ArrayList<>(); - - // Try (known) secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - long keyId = pkesk.getKeyID(); - LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); - PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); - if (decryptionKeys == null) { - LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); - continue; - } - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); - - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); - // Postpone keys with missing passphrase - if (!protector.hasPassphraseFor(keyId)) { - LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); - postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); - continue; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true; - } - } - - // try anonymous secret keys - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { - PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); - PGPSecretKey secretKey = decryptionKeyCandidate.getB(); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); - if (!protector.hasPassphraseFor(secretKey.getKeyID())) { - LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); - postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); - continue; - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true; - } - } - } - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - PGPSecretKey key = k.getA(); - PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); - assert (keys != null); - keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { - PGPSecretKey secretKey = missingPassphrases.getA(); - long keyId = secretKey.getKeyID(); - PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); - assert (decryptionKey != null); - SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue; - } - - LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); - SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true; - } - } - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - // we did not yet succeed in decrypting any session key :/ - - LOGGER.debug("Failed to decrypt encrypted data packet"); - return false; - } - - private boolean decryptWithPrivateKey(SortedESKs esks, - PGPPrivateKey privateKey, - SubkeyIdentifier decryptionKeyId, - PGPPublicKeyEncryptedData pkesk) - throws PGPException, IOException { - PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey); - return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk); - } - - private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { - S2K s2K = secretKey.getS2K(); - if (s2K != null) { - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); - return true; - } - } - return false; - } - - private boolean decryptSKESKAndStream(SortedESKs esks, - PGPPBEEncryptedData symEsk, - PBEDataDecryptorFactory decryptorFactory) - throws IOException, UnacceptableAlgorithmException { - try { - InputStream decrypted = symEsk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(symEsk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - sessionKey.getAlgorithm(), metadata.depth + 1); - encryptedData.sessionKey = sessionKey; - encryptedData.recipients = new ArrayList<>(); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - encryptedData.recipients.add(pkesk.getKeyID()); - } - LOGGER.debug("Successfully decrypted data with passphrase"); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, symEsk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); - } - return false; - } - - private boolean decryptPKESKAndStream(SortedESKs esks, - SubkeyIdentifier decryptionKeyId, - PublicKeyDataDecryptorFactory decryptorFactory, - PGPPublicKeyEncryptedData asymEsk) - throws IOException, UnacceptableAlgorithmException { - try { - InputStream decrypted = asymEsk.getDataStream(decryptorFactory); - SessionKey sessionKey = new SessionKey(asymEsk.getSessionKey(decryptorFactory)); - throwIfUnacceptable(sessionKey.getAlgorithm()); - - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( - SymmetricKeyAlgorithm.requireFromId(asymEsk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1); - encryptedData.decryptionKey = decryptionKeyId; - encryptedData.sessionKey = sessionKey; - encryptedData.recipients = new ArrayList<>(); - for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { - encryptedData.recipients.add(pkesk.getKeyID()); - } - for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { - encryptedData.recipients.add(pkesk.getKeyID()); - } - - LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); - IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, asymEsk, options); - nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); - return true; - } catch (UnacceptableAlgorithmException e) { - throw e; - } catch (PGPException e) { - LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); - } - return false; - } - - private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); - } - } - - private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { - int algorithm = pkesk.getAlgorithm(); - List> decryptionKeyCandidates = new ArrayList<>(); - - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - for (PGPPublicKey publicKey : info.getDecryptionSubkeys()) { - if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { - PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); - decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); - } - } - } - return decryptionKeyCandidates; - } - - private PGPSecretKeyRing getDecryptionKey(long keyID) { - for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { - PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); - if (decryptionKey == null) { - continue; - } - - KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); - List encryptionKeys = info.getDecryptionSubkeys(); - for (PGPPublicKey key : encryptionKeys) { - if (key.getKeyID() == keyID) { - return secretKeys; - } - } - - LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); - } - return null; - } - - @Override - public int read() throws IOException { - if (nestedInputStream == null) { - if (packetInputStream != null) { - syntaxVerifier.assertValid(); - } - return -1; - } - - int r; - try { - r = nestedInputStream.read(); - } catch (IOException e) { - r = -1; - } - boolean eos = r == -1; - if (!eos) { - byte b = (byte) r; - signatures.updateLiteral(b); - } else { - nestedInputStream.close(); - collectMetadata(); - nestedInputStream = null; - - if (packetInputStream != null) { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - signatures.finish(metadata, policy); - } - return r; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) - throws IOException { - if (nestedInputStream == null) { - if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); - syntaxVerifier.assertValid(); - } - return -1; - } - - int r = nestedInputStream.read(b, off, len); - if (r != -1) { - signatures.updateLiteral(b, off, r); - } else { - nestedInputStream.close(); - collectMetadata(); - nestedInputStream = null; - - if (packetInputStream != null) { - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - signatures.finish(metadata, policy); - } - return r; - } - - @Override - public void close() throws IOException { - super.close(); - if (closed) { - if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); - syntaxVerifier.assertValid(); - } - return; - } - - if (nestedInputStream != null) { - nestedInputStream.close(); - collectMetadata(); - nestedInputStream = null; - } - - try { - consumePackets(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - - if (packetInputStream != null) { - syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE); - syntaxVerifier.assertValid(); - packetInputStream.close(); - } - closed = true; - } - - private void collectMetadata() { - if (nestedInputStream instanceof OpenPgpMessageInputStream) { - OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; - this.metadata.setChild((MessageMetadata.Nested) child.metadata); - } - } - - public MessageMetadata getMetadata() { - if (!closed) { - throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); - } - - return new MessageMetadata((MessageMetadata.Message) metadata); - } - - private static class SortedESKs { - - private final List skesks = new ArrayList<>(); - private final List pkesks = new ArrayList<>(); - private final List anonPkesks = new ArrayList<>(); - - SortedESKs(PGPEncryptedDataList esks) { - for (PGPEncryptedData esk : esks) { - if (esk instanceof PGPPBEEncryptedData) { - skesks.add((PGPPBEEncryptedData) esk); - } - else if (esk instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; - if (pkesk.getKeyID() != 0) { - pkesks.add(pkesk); - } else { - anonPkesks.add(pkesk); - } - } else { - throw new IllegalArgumentException("Unknown ESK class type."); - } - } - } - - public List all() { - List esks = new ArrayList<>(); - esks.addAll(skesks); - esks.addAll(pkesks); - esks.addAll(anonPkesks); - return esks; - } - } - - // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" - // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. - // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. - // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! - private static final class Signatures extends OutputStream { - final ConsumerOptions options; - final List detachedSignatures; - final List prependedSignatures; - final List onePassSignatures; - final Stack> opsUpdateStack; - List literalOPS = new ArrayList<>(); - final List correspondingSignatures; - final List prependedSignaturesWithMissingCert = new ArrayList<>(); - final List inbandSignaturesWithMissingCert = new ArrayList<>(); - final List detachedSignaturesWithMissingCert = new ArrayList<>(); - boolean isLiteral = true; - - private Signatures(ConsumerOptions options) { - this.options = options; - this.detachedSignatures = new ArrayList<>(); - this.prependedSignatures = new ArrayList<>(); - this.onePassSignatures = new ArrayList<>(); - this.opsUpdateStack = new Stack<>(); - this.correspondingSignatures = new ArrayList<>(); - } - - void addDetachedSignatures(Collection signatures) { - for (PGPSignature signature : signatures) { - addDetachedSignature(signature); - } - } - - void addDetachedSignature(PGPSignature signature) { - SignatureCheck check = initializeSignature(signature); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (check != null) { - detachedSignatures.add(check); - } else { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key") - )); - } - } - - void addPrependedSignature(PGPSignature signature) { - SignatureCheck check = initializeSignature(signature); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - if (check != null) { - this.prependedSignatures.add(check); - } else { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key") - )); - } - } - - SignatureCheck initializeSignature(PGPSignature signature) { - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing certificate = findCertificate(keyId); - if (certificate == null) { - return null; - } - - SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); - initialize(signature, certificate, keyId); - return new SignatureCheck(signature, certificate, verifierKey); - } - - void addOnePassSignature(PGPOnePassSignature signature) { - PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); - - if (certificate != null) { - OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); - initialize(signature, certificate); - onePassSignatures.add(ops); - - literalOPS.add(ops); - } - if (signature.isContaining()) { - enterNesting(); - } - } - - void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { - boolean found = false; - long keyId = SignatureUtils.determineIssuerKeyId(signature); - for (int i = onePassSignatures.size() - 1; i >= 0; i--) { - OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); - if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { - continue; - } - found = true; - - if (onePassSignature.getSignature() != null) { - continue; - } - - onePassSignature.setSignature(signature); - SignatureVerification verification = new SignatureVerification(signature, - new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); - - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedOnePassSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); - } - break; - } - - if (!found) { - LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); - inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( - new SignatureVerification(signature, null), - new SignatureValidationException("Missing verification key"))); - } - } - - void enterNesting() { - opsUpdateStack.push(literalOPS); - literalOPS = new ArrayList<>(); - } - - void leaveNesting() { - if (opsUpdateStack.isEmpty()) { - return; - } - opsUpdateStack.pop(); - } - - private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { - PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(); - try { - signature.init(verifierProvider, certificate.getPublicKey(keyId)); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { - PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(); - try { - ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private PGPPublicKeyRing findCertificate(long keyId) { - PGPPublicKeyRing cert = options.getCertificateSource().getCertificate(keyId); - if (cert != null) { - return cert; - } - - if (options.getMissingCertificateCallback() != null) { - return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - return null; // TODO: Missing cert for sig - } - - public void updateLiteral(byte b) { - for (OnePassSignatureCheck ops : literalOPS) { - ops.getOnePassSignature().update(b); - } - - for (SignatureCheck detached : detachedSignatures) { - detached.getSignature().update(b); - } - - for (SignatureCheck prepended : prependedSignatures) { - prepended.getSignature().update(b); - } - } - - public void updateLiteral(byte[] b, int off, int len) { - for (OnePassSignatureCheck ops : literalOPS) { - ops.getOnePassSignature().update(b, off, len); - } - - for (SignatureCheck detached : detachedSignatures) { - detached.getSignature().update(b, off, len); - } - - for (SignatureCheck prepended : prependedSignatures) { - prepended.getSignature().update(b, off, len); - } - } - - public void updatePacket(byte b) { - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignatureCheck ops : nestedOPSs) { - ops.getOnePassSignature().update(b); - } - } - } - - public void updatePacket(byte[] buf, int off, int len) { - for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { - List nestedOPSs = opsUpdateStack.get(i); - for (OnePassSignatureCheck ops : nestedOPSs) { - ops.getOnePassSignature().update(buf, off, len); - } - } - } - - public void finish(MessageMetadata.Layer layer, Policy policy) { - for (SignatureCheck detached : detachedSignatures) { - SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(detached.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedDetachedSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); - } - } - - for (SignatureCheck prepended : prependedSignatures) { - SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); - try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(prepended.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature( - prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); - LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); - layer.addVerifiedPrependedSignature(verification); - } catch (SignatureValidationException e) { - LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); - layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); - } - } - - for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { - layer.addRejectedOnePassSignature(rejected); - } - - for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { - layer.addRejectedPrependedSignature(rejected); - } - - for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { - layer.addRejectedDetachedSignature(rejected); - } - } - - @Override - public void write(int b) { - updatePacket((byte) b); - } - - @Override - public void write(@Nonnull byte[] b, int off, int len) { - updatePacket(b, off, len); - } - - public void nextPacket(OpenPgpPacket nextPacket) { - if (nextPacket == OpenPgpPacket.LIT) { - isLiteral = true; - if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { - literalOPS = opsUpdateStack.pop(); - } - } else { - isLiteral = false; - } - } - - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt new file mode 100644 index 00000000..25b29100 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -0,0 +1,903 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.UnsupportedPacketVersionException +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.bouncycastle.util.io.TeeInputStream +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.* +import org.pgpainless.decryption_verification.MessageMetadata.* +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil +import org.pgpainless.decryption_verification.syntax_check.InputSymbol +import org.pgpainless.decryption_verification.syntax_check.PDA +import org.pgpainless.decryption_verification.syntax_check.StackSymbol +import org.pgpainless.exception.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.key.util.KeyIdUtil +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.signature.consumer.CertificateValidator +import org.pgpainless.signature.consumer.OnePassSignatureCheck +import org.pgpainless.signature.consumer.SignatureCheck +import org.pgpainless.signature.consumer.SignatureValidator +import org.pgpainless.util.ArmoredInputStreamFactory +import org.pgpainless.util.SessionKey +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class OpenPgpMessageInputStream( + type: Type, + inputStream: InputStream, + private val options: ConsumerOptions, + private val metadata: Layer, + private val policy: Policy) : DecryptionStream() { + + private val signatures: Signatures = Signatures(options) + private var packetInputStream: TeeBCPGInputStream? = null + private var nestedInputStream: InputStream? = null + private val syntaxVerifier = PDA() + private var closed = false + + init { + + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (metadata is Message) { + signatures.addDetachedSignatures(options.detachedSignatures) + } + + when(type) { + Type.standard -> { + + // tee out packet bytes for signature verification + packetInputStream = TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures) + + // *omnomnom* + consumePackets() + } + + Type.cleartext_signed -> { + val multiPassStrategy = options.multiPassStrategy + val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.messageOutputStream) + + for (signature in detachedSignatures) { + signatures.addDetachedSignature(signature) + } + + options.forceNonOpenPgpData() + nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) + } + + Type.non_openpgp -> { + packetInputStream = null + nestedInputStream = TeeInputStream(inputStream, this.signatures) + } + } + } + + enum class Type { + standard, cleartext_signed, non_openpgp + } + + constructor(inputStream: InputStream, options: ConsumerOptions, metadata: Layer, policy: Policy): + this(Type.standard, inputStream, options, metadata, policy) + + private fun consumePackets() { + val pIn = packetInputStream ?: return + + var packet: OpenPgpPacket? + + // Comsume packets, potentially stepping into nested layers + layer@ while (run { + packet = pIn.nextPacketTag() + packet + } != null) { + + signatures.nextPacket(packet!!) + // Consume packets in a layer + when(packet) { + + OpenPgpPacket.LIT -> { + processLiteralData() + break@layer // nest down + } + + OpenPgpPacket.COMP -> { + processCompressedData() + break@layer // nest down + } + + OpenPgpPacket.OPS -> { + processOnePassSignature() // OPS is on the same layer, no nest down + } + + OpenPgpPacket.SIG -> { + processSignature() // SIG is on the same layer, no nest down + } + + OpenPgpPacket.PKESK, OpenPgpPacket.SKESK, OpenPgpPacket.SED, OpenPgpPacket.SEIPD -> { + if (processEncryptedData()) { + break@layer + } + throw MissingDecryptionMethodException("No working decryption method found.") + } + + OpenPgpPacket.MARKER -> { + LOGGER.debug("Skipping Marker Packet") + pIn.readMarker() + } + + OpenPgpPacket.SK, OpenPgpPacket.PK, OpenPgpPacket.SSK, OpenPgpPacket.PSK, OpenPgpPacket.TRUST, OpenPgpPacket.UID, OpenPgpPacket.UATTR -> + throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") + + OpenPgpPacket.EXP_1, OpenPgpPacket.EXP_2, OpenPgpPacket.EXP_3, OpenPgpPacket.EXP_4 -> + throw MalformedOpenPgpMessageException("Unsupported Packet in Stream: $packet") + + else -> + throw MalformedOpenPgpMessageException("Unexpected Packet in Stream: $packet") + } + } + } + + private fun processLiteralData() { + LOGGER.debug("Literal Data Packet at depth ${metadata.depth} encountered.") + syntaxVerifier.next(InputSymbol.LITERAL_DATA) + val literalData = packetInputStream!!.readLiteralData() + + // Extract Metadata + metadata.setChild(LiteralData( + literalData.fileName, literalData.modificationTime, + StreamEncoding.requireFromCode(literalData.format))) + + nestedInputStream = literalData.inputStream + } + + private fun processCompressedData() { + syntaxVerifier.next(InputSymbol.COMPRESSED_DATA) + signatures.enterNesting() + val compressedData = packetInputStream!!.readCompressedData() + + // Extract Metadata + val compressionLayer = CompressedData( + CompressionAlgorithm.requireFromId(compressedData.algorithm), + metadata.depth + 1) + + LOGGER.debug("Compressed Data Packet (${compressionLayer.algorithm}) at depth ${metadata.depth} encountered.") + nestedInputStream = OpenPgpMessageInputStream(compressedData.dataStream, options, compressionLayer, policy) + } + + private fun processOnePassSignature() { + syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) + val ops = packetInputStream!!.readOnePassSignature() + LOGGER.debug("One-Pass-Signature Packet by key ${KeyIdUtil.formatKeyId(ops.keyID)} at depth ${metadata.depth} encountered.") + signatures.addOnePassSignature(ops) + } + + private fun processSignature() { + // true if signature corresponds to OPS + val isSigForOps = syntaxVerifier.peekStack() == StackSymbol.OPS + syntaxVerifier.next(InputSymbol.SIGNATURE) + val signature = try { + packetInputStream!!.readSignature() + } catch (e : UnsupportedPacketVersionException) { + LOGGER.debug("Unsupported Signature at depth ${metadata.depth} encountered.", e) + return + } + + val keyId = SignatureUtils.determineIssuerKeyId(signature) + if (isSigForOps) { + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${metadata.depth} encountered.") + signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + signatures.addCorrespondingOnePassSignature(signature, metadata, policy) + } else { + LOGGER.debug("Prepended Signature Packet by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${metadata.depth} encountered.") + signatures.addPrependedSignature(signature) + } + } + + private fun processEncryptedData(): Boolean { + LOGGER.debug("Symmetrically Encrypted Data Packet at depth ${metadata.depth} encountered.") + syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) + val encDataList = packetInputStream!!.readEncryptedDataList() + if (!encDataList.isIntegrityProtected) { + LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") + if (!options.isIgnoreMDCErrors) { + throw MessageNotIntegrityProtectedException() + } + } + + val esks = SortedESKs(encDataList) + LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" + + " ${esks.pkesks.size + esks.anonPkesks.size} PKESK(s) from which ${esks.anonPkesks.size} PKESK(s)" + + " have an anonymous recipient.") + + // try custom decryptor factories + for ((key, decryptorFactory) in options.customDecryptorFactories) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.") + esks.pkesks.filter { + // find matching PKESK + it.keyID == key.subkeyId + }.forEach { + // attempt decryption + if (decryptPKESKAndStream(esks, key, decryptorFactory, it)) { + return true + } + } + } + + // try provided session key + if (options.sessionKey != null) { + val sk = options.sessionKey!! + LOGGER.debug("Attempt decryption with provided session key.") + throwIfUnacceptable(sk.algorithm) + + val decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(sk) + val layer = EncryptedData(sk.algorithm, metadata.depth + 1) + val skEncData = encDataList.extractSessionKeyEncryptedData() + try { + val decrypted = skEncData.getDataStream(decryptorFactory) + layer.sessionKey = sk + val integrityProtected = IntegrityProtectedInputStream(decrypted, skEncData, options) + nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, layer, policy) + LOGGER.debug("Successfully decrypted data using provided session key") + return true + } catch (e : PGPException) { + // Session key mismatch? + LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e) + } + } + + // try passwords + for (passphrase in options.decryptionPassphrases) { + for (skesk in esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase") + val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) + if (!isAcceptable(algorithm)) { + LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm $algorithm") + continue + } + + val decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase) + if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { + return true + } + } + } + + val postponedDueToMissingPassphrase = mutableListOf>() + + // try (known) secret keys + for (pkesk in esks.pkesks) { + val keyId = pkesk.keyID + LOGGER.debug("Encountered PKESK for recipient ${KeyIdUtil.formatKeyId(keyId)}") + val decryptionKeys = getDecryptionKey(keyId) + if (decryptionKeys == null) { + LOGGER.debug("Skipping PKESK because no matching key ${KeyIdUtil.formatKeyId(keyId)} was provided.") + continue + } + val secretKey = decryptionKeys.getSecretKey(keyId) + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") + val protector = options.getSecretKeyProtector(decryptionKeys) + if (!protector.hasPassphraseFor(keyId)) { + LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(secretKey to pkesk) + continue + } + + val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + + // try anonymous secret keys + for (pkesk in esks.anonPkesks) { + for ((decryptionKeys, secretKey) in findPotentialDecryptionKeys(pkesk)) { + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") + val protector = options.getSecretKeyProtector(decryptionKeys) + + if (!protector.hasPassphraseFor(secretKey.keyID)) { + LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(secretKey to pkesk) + continue + } + + val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + } + + if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + postponedDueToMissingPassphrase.map { + SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) + }.also { + if (it.isNotEmpty()) + throw MissingPassphraseException(it.toSet()) + } + } else if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.INTERACTIVE) { + for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { + val keyId = secretKey.keyID + val decryptionKeys = getDecryptionKey(keyId)!! + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } + + LOGGER.debug("Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") + val protector = options.getSecretKeyProtector(decryptionKeys) + val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } + } + } else { + throw IllegalStateException("Invalid PostponedKeysStrategy set in consumer options.") + } + + // We did not yet succeed in decrypting any session key :/ + LOGGER.debug("Failed to decrypt encrypted data packet.") + return false + } + + private fun decryptWithPrivateKey(esks: SortedESKs, + privateKey: PGPPrivateKey, + decryptionKeyId: SubkeyIdentifier, + pkesk: PGPPublicKeyEncryptedData): Boolean { + val decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey) + return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk) + } + + private fun hasUnsupportedS2KSpecifier(secretKey: PGPSecretKey, decryptionKeyId: SubkeyIdentifier): Boolean { + val s2k = secretKey.s2K + if (s2k != null) { + if (s2k.type in 100..110) { + LOGGER.debug("Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}") + return true + } + } + return false + } + + private fun decryptSKESKAndStream(esks: SortedESKs, + skesk: PGPPBEEncryptedData, + decryptorFactory: PBEDataDecryptorFactory): Boolean { + try { + val decrypted = skesk.getDataStream(decryptorFactory) + val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) + throwIfUnacceptable(sessionKey.algorithm) + val encryptedData = EncryptedData(sessionKey.algorithm, metadata.depth + 1) + encryptedData.sessionKey = sessionKey + encryptedData.recipients = esks.pkesks.map { it.keyID } + LOGGER.debug("Successfully decrypted data with passphrase") + val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) + nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + return true + } catch (e : UnacceptableAlgorithmException) { + throw e + } catch (e : PGPException) { + LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e) + } + return false + } + + private fun decryptPKESKAndStream(esks: SortedESKs, + decryptionKeyId: SubkeyIdentifier, + decryptorFactory: PublicKeyDataDecryptorFactory, + pkesk: PGPPublicKeyEncryptedData): Boolean { + try { + val decrypted = pkesk.getDataStream(decryptorFactory) + val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) + throwIfUnacceptable(sessionKey.algorithm) + + val encryptedData = EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1) + encryptedData.decryptionKey = decryptionKeyId + encryptedData.sessionKey = sessionKey + encryptedData.recipients = esks.pkesks.plus(esks.anonPkesks).map { it.keyID } + LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") + val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) + nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + return true + } catch (e : UnacceptableAlgorithmException) { + throw e + } catch (e : PGPException) { + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e) + } + return false + } + + override fun read(): Int { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.assertValid() + } + return -1 + } + + val r: Int = try { + nestedInputStream!!.read() + } catch (e: IOException) { + -1 + } + if (r != -1) { + signatures.updateLiteral(r.toByte()) + } else { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + if (packetInputStream != null) { + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + signatures.finish(metadata, policy) + } + return r + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + } + return -1 + } + val r = nestedInputStream!!.read(b, off, len) + if (r != -1) { + signatures.updateLiteral(b, off, r) + } else { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + if (packetInputStream != null) { + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + signatures.finish(metadata, policy) + } + return r + } + + override fun close() { + super.close() + if (closed) { + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + } + return + } + if (nestedInputStream != null) { + nestedInputStream!!.close() + collectMetadata() + nestedInputStream = null + } + try { + consumePackets() + } catch (e: PGPException) { + throw RuntimeException(e) + } + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) + syntaxVerifier.assertValid() + packetInputStream!!.close() + } + closed = true + } + + private fun collectMetadata() { + if (nestedInputStream is OpenPgpMessageInputStream) { + val child = nestedInputStream as OpenPgpMessageInputStream + metadata.setChild(child.metadata as Nested) + } + } + + override fun getMetadata(): MessageMetadata { + check(closed) { "Stream must be closed before access to metadata can be granted." } + + return MessageMetadata((metadata as Message)) + } + + private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.decryptionKeys.firstOrNull { + it.any { + k -> k.keyID == keyId + }.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { + k -> k.keyID == keyId + }) + } + + private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List> { + val algorithm = pkesk.algorithm + val candidates = mutableListOf>() + options.decryptionKeys.forEach { + val info = PGPainless.inspectKeyRing(it) + for (key in info.decryptionSubkeys) { + if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { + candidates.add(it to it.getSecretKey(key.keyID)) + } + } + } + return candidates + } + + private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean = + policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) + + private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) { + if (!isAcceptable(algorithm)) { + throw UnacceptableAlgorithmException("Symmetric-Key algorithm $algorithm is not acceptable for message decryption.") + } + } + + private class SortedESKs(esks: PGPEncryptedDataList) { + val skesks: List + val pkesks: List + val anonPkesks: List + + init { + skesks = mutableListOf() + pkesks = mutableListOf() + anonPkesks = mutableListOf() + for (esk in esks) { + if (esk is PGPPBEEncryptedData) { + skesks.add(esk) + } else if (esk is PGPPublicKeyEncryptedData) { + if (esk.keyID != 0L) { + pkesks.add(esk) + } else { + anonPkesks.add(esk) + } + } else { + throw IllegalArgumentException("Unknown ESK class type ${esk.javaClass}") + } + } + } + + val all: List + get() = skesks.plus(pkesks).plus(anonPkesks) + } + + private class Signatures( + val options: ConsumerOptions + ) : OutputStream() { + val detachedSignatures = mutableListOf() + val prependedSignatures = mutableListOf() + val onePassSignatures = mutableListOf() + val opsUpdateStack = ArrayDeque>() + var literalOPS = mutableListOf() + val correspondingSignatures = mutableListOf() + val prependedSignaturesWithMissingCert = mutableListOf() + val inbandSignaturesWithMissingCert = mutableListOf() + val detachedSignaturesWithMissingCert = mutableListOf() + var isLiteral = true + + fun addDetachedSignatures(signatures: Collection) { + for (signature in signatures) { + addDetachedSignature(signature) + } + } + + fun addDetachedSignature(signature: PGPSignature) { + val check = initializeSignature(signature) + val keyId = SignatureUtils.determineIssuerKeyId(signature) + if (check != null) { + detachedSignatures.add(check) + } else { + LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( + SignatureVerification(signature, null), + SignatureValidationException("Missing verification key."))) + } + } + + fun addPrependedSignature(signature: PGPSignature) { + val check = initializeSignature(signature) + val keyId = SignatureUtils.determineIssuerKeyId(signature) + if (check != null) { + prependedSignatures.add(check) + } else { + LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( + SignatureVerification(signature, null), + SignatureValidationException("Missing verification key") + )) + } + } + + fun initializeSignature(signature: PGPSignature): SignatureCheck? { + val keyId = SignatureUtils.determineIssuerKeyId(signature) + val certificate = findCertificate(keyId) ?: return null + + val verifierKey = SubkeyIdentifier(certificate, keyId) + initialize(signature, certificate, keyId) + return SignatureCheck(signature, certificate, verifierKey) + } + + fun addOnePassSignature(signature: PGPOnePassSignature) { + val certificate = findCertificate(signature.keyID) + + if (certificate != null) { + val ops = OnePassSignatureCheck(signature, certificate) + initialize(signature, certificate) + onePassSignatures.add(ops) + literalOPS.add(ops) + } + if (signature.isContaining) { + enterNesting() + } + } + + fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer, policy: Policy) { + var found = false + val keyId = SignatureUtils.determineIssuerKeyId(signature) + for ((i, check) in onePassSignatures.withIndex().reversed()) { + if (check.onePassSignature.keyID != keyId) { + continue + } + found = true + + if (check.signature != null) { + continue + } + + check.signature = signature + val verification = SignatureVerification(signature, + SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) + + try { + SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + .verify(signature) + CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedOnePassSignature(verification) + } catch (e: SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedOnePassSignature(SignatureVerification.Failure(verification, e)) + } + break + } + + if (!found) { + LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( + SignatureVerification(signature, null), + SignatureValidationException("Missing verification key.") + )) + } + } + + fun enterNesting() { + opsUpdateStack.addFirst(literalOPS) + literalOPS = mutableListOf() + } + + fun leaveNesting() { + if (opsUpdateStack.isEmpty()) { + return + } + opsUpdateStack.removeFirst() + } + + fun findCertificate(keyId: Long): PGPPublicKeyRing? { + val cert = options.certificateSource.getCertificate(keyId) + if (cert != null) { + return cert + } + + if (options.missingCertificateCallback != null) { + return options.missingCertificateCallback!!.onMissingPublicKeyEncountered(keyId) + } + return null // TODO: Missing cert for sig + } + + fun updateLiteral(b: Byte) { + for (ops in literalOPS) { + ops.onePassSignature.update(b) + } + + for (detached in detachedSignatures) { + detached.signature.update(b) + } + + for (prepended in prependedSignatures) { + prepended.signature.update(b) + } + } + + fun updateLiteral(buf: ByteArray, off: Int, len: Int) { + for (ops in literalOPS) { + ops.onePassSignature.update(buf, off, len) + } + + for (detached in detachedSignatures) { + detached.signature.update(buf, off, len) + } + + for (prepended in prependedSignatures) { + prepended.signature.update(buf, off, len) + } + } + + fun updatePacket(b: Byte) { + for (nestedOPSs in opsUpdateStack.reversed()) { + for (ops in nestedOPSs) { + ops.onePassSignature.update(b) + } + } + } + + fun updatePacket(buf: ByteArray, off: Int, len: Int) { + for (nestedOPSs in opsUpdateStack.reversed()) { + for (ops in nestedOPSs) { + ops.onePassSignature.update(buf, off, len) + } + } + } + + fun finish(layer: Layer, policy: Policy) { + for (detached in detachedSignatures) { + val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier) + try { + SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + .verify(detached.signature) + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedDetachedSignature(verification) + } catch (e : SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedDetachedSignature(SignatureVerification.Failure(verification, e)) + } + } + + for (prepended in prependedSignatures) { + val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) + try { + SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + .verify(prepended.signature) + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy) + LOGGER.debug("Acceptable signature by key ${verification.signingKey}") + layer.addVerifiedPrependedSignature(verification) + } catch (e : SignatureValidationException) { + LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) + layer.addRejectedPrependedSignature(SignatureVerification.Failure(verification, e)) + } + } + + for (rejected in inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected) + } + + for (rejected in prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected) + } + + for (rejected in detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected) + } + } + + override fun write(b: Int) { + updatePacket(b.toByte()) + } + + override fun write(buf: ByteArray, off: Int, len: Int) { + updatePacket(buf, off, len) + } + + fun nextPacket(nextPacket: OpenPgpPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true + if (literalOPS.isEmpty() && opsUpdateStack.isNotEmpty()) { + literalOPS = opsUpdateStack.removeFirst() + } + } else { + isLiteral = false + } + } + + companion object { + @JvmStatic + private fun initialize(signature: PGPSignature, certificate: PGPPublicKeyRing, keyId: Long) { + val verifierProvider = ImplementationFactory.getInstance() + .pgpContentVerifierBuilderProvider + try { + signature.init(verifierProvider, certificate.getPublicKey(keyId)) + } catch (e : PGPException) { + throw RuntimeException(e) + } + } + + @JvmStatic + private fun initialize(ops: PGPOnePassSignature, certificate: PGPPublicKeyRing) { + val verifierProvider = ImplementationFactory.getInstance() + .pgpContentVerifierBuilderProvider + try { + ops.init(verifierProvider, certificate.getPublicKey(ops.keyID)) + } catch (e : PGPException) { + throw RuntimeException(e) + } + } + } + } + + companion object { + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java) + + @JvmStatic + fun create(inputStream: InputStream, + options: ConsumerOptions) = create(inputStream, options, PGPainless.getPolicy()) + + @JvmStatic + fun create(inputStream: InputStream, + options: ConsumerOptions, + policy: Policy) = create(inputStream, options, Message(), policy) + + @JvmStatic + internal fun create(inputStream: InputStream, + options: ConsumerOptions, + metadata: Layer, + policy: Policy): OpenPgpMessageInputStream { + val openPgpIn = OpenPgpInputStream(inputStream) + openPgpIn.reset() + + if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData) { + return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy) + } + + if (openPgpIn.isBinaryOpenPgp) { + // Simply consume OpenPGP message + return OpenPgpMessageInputStream(Type.standard, openPgpIn, options, metadata, policy) + } + + return if (openPgpIn.isAsciiArmored) { + val armorIn = ArmoredInputStreamFactory.get(openPgpIn) + if (armorIn.isClearText) { + (metadata as Message).cleartextSigned = true + OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy) + } else { + // Simply consume dearmored OpenPGP message + OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy) + } + } else { + throw AssertionError("Cannot deduce type of data.") + } + } + } +} \ No newline at end of file From b33ee90845ddbb08e68adeb299cde1192f360e0d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 28 Aug 2023 16:17:47 +0200 Subject: [PATCH 078/351] Kotlin conversion: SignatureUtils --- .../pgpainless/signature/SignatureUtils.java | 342 ------------------ .../pgpainless/signature/SignatureUtils.kt | 263 ++++++++++++++ 2 files changed, 263 insertions(+), 342 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java deleted file mode 100644 index 663ef003..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureUtils.java +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.encoders.Hex; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.ArmorUtils; - -/** - * Utility methods related to signatures. - */ -public final class SignatureUtils { - - public static final int MAX_ITERATIONS = 10000; - - private SignatureUtils() { - - } - - /** - * Extract and return the key expiration date value from the given signature. - * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. - * - * @param keyCreationDate creation date of the key - * @param signature signature - * @return key expiration date as given by the signature - */ - public static Date getKeyExpirationDate(Date keyCreationDate, PGPSignature signature) { - KeyExpirationTime keyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature); - long expiresInSecs = keyExpirationTime == null ? 0 : keyExpirationTime.getTime(); - return datePlusSeconds(keyCreationDate, expiresInSecs); - } - - /** - * Return the expiration date of the signature. - * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. - * - * @param signature signature - * @return expiration date of the signature, or null if it does not expire. - */ - public static Date getSignatureExpirationDate(PGPSignature signature) { - Date creationDate = signature.getCreationTime(); - SignatureExpirationTime signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature); - long expiresInSecs = signatureExpirationTime == null ? 0 : signatureExpirationTime.getTime(); - return datePlusSeconds(creationDate, expiresInSecs); - } - - /** - * Return a new date which represents the given date plus the given amount of seconds added. - * - * Since '0' is a special date value in the OpenPGP specification - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. - * - * @param date date - * @param seconds number of seconds to be added - * @return date plus seconds or null if seconds is '0' - */ - public static Date datePlusSeconds(Date date, long seconds) { - if (seconds == 0) { - return null; - } - return new Date(date.getTime() + 1000 * seconds); - } - - /** - * Return true, if the expiration date of the {@link PGPSignature} lays in the past. - * If no expiration date is present in the signature, it is considered non-expired. - * - * @param signature signature - * @return true if expired, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature) { - return isSignatureExpired(signature, new Date()); - } - - /** - * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. - * If no expiration date is present in the signature, it is considered non-expiring. - * - * @param signature signature - * @param comparisonDate reference date - * @return true if sig is expired at reference date, false otherwise - */ - public static boolean isSignatureExpired(PGPSignature signature, Date comparisonDate) { - Date expirationDate = getSignatureExpirationDate(signature); - return expirationDate != null && comparisonDate.after(expirationDate); - } - - /** - * Return true if the provided signature is a hard revocation. - * Hard revocations are revocation signatures which either carry a revocation reason of - * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, - * or no reason at all. - * - * @param signature signature - * @return true if signature is a hard revocation - */ - public static boolean isHardRevocation(PGPSignature signature) { - - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { - // Not a revocation - return false; - } - - RevocationReason reasonSubpacket = SignatureSubpacketsUtil.getRevocationReason(signature); - if (reasonSubpacket == null) { - // no reason -> hard revocation - return true; - } - return RevocationAttributes.Reason.isHardRevocation(reasonSubpacket.getRevocationReason()); - } - - /** - * Parse an ASCII encoded list of OpenPGP signatures into a {@link PGPSignatureList} - * and return it as a {@link List}. - * - * @param encodedSignatures ASCII armored signature list - * @return signature list - * - * @throws IOException if the signatures cannot be read - * @throws PGPException in case of a broken signature - */ - public static List readSignatures(String encodedSignatures) throws IOException, PGPException { - @SuppressWarnings("CharsetObjectCanBeUsed") - Charset utf8 = Charset.forName("UTF-8"); - byte[] bytes = encodedSignatures.getBytes(utf8); - return readSignatures(bytes); - } - - /** - * Read a single, or a list of {@link PGPSignature PGPSignatures} and return them as a {@link List}. - * - * @param encodedSignatures ASCII armored or binary signatures - * @return signatures - * @throws IOException if the signatures cannot be read - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(byte[] encodedSignatures) throws IOException, PGPException { - InputStream inputStream = new ByteArrayInputStream(encodedSignatures); - return readSignatures(inputStream); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be armored, compressed and may contain marker packets. - * - * @param inputStream input stream - * @return list of encountered signatures - * @throws IOException in case of a stream error - * @throws PGPException in case of an OpenPGP error - */ - public static List readSignatures(InputStream inputStream) throws IOException, PGPException { - return readSignatures(inputStream, MAX_ITERATIONS); - } - - /** - * Read and return {@link PGPSignature PGPSignatures}. - * This method can deal with signatures that may be binary, armored and may contain marker packets. - * - * @param inputStream input stream - * @param maxIterations number of loop iterations until reading is aborted - * @return list of encountered signatures - * @throws IOException in case of a stream error - */ - public static List readSignatures(InputStream inputStream, int maxIterations) throws IOException { - List signatures = new ArrayList<>(); - InputStream pgpIn = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - - int i = 0; - Object nextObject; - while (i++ < maxIterations && (nextObject = objectFactory.nextObject()) != null) { - - // Since signatures are indistinguishable from randomness, there is no point in having them compressed, - // except for an attacker who is trying to exploit flaws in the decompression algorithm. - // Therefore, we ignore compressed data packets without attempting decompression. - if (nextObject instanceof PGPCompressedData) { - PGPCompressedData compressedData = (PGPCompressedData) nextObject; - // getInputStream() does not do decompression, contrary to getDataStream(). - Streams.drain(compressedData.getInputStream()); // Skip packet without decompressing - } - - if (nextObject instanceof PGPSignatureList) { - PGPSignatureList signatureList = (PGPSignatureList) nextObject; - for (PGPSignature s : signatureList) { - signatures.add(s); - } - } - - if (nextObject instanceof PGPSignature) { - signatures.add((PGPSignature) nextObject); - } - } - pgpIn.close(); - - return signatures; - } - - /** - * Determine the issuer key-id of a {@link PGPSignature}. - * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. - * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. - * - * Otherwise, it returns 0. - * @param signature signature - * @return signatures issuing key id - */ - public static long determineIssuerKeyId(PGPSignature signature) { - if (signature.getVersion() == 3) { - // V3 sigs do not contain subpackets - return signature.getKeyID(); - } - - IssuerKeyID issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature); - OpenPgpFingerprint fingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - - if (issuerKeyId != null && issuerKeyId.getKeyID() != 0) { - return issuerKeyId.getKeyID(); - } - if (issuerKeyId == null && fingerprint != null) { - return fingerprint.getKeyId(); - } - return 0; - } - - /** - * Return the digest prefix of the signature as hex-encoded String. - * - * @param signature signature - * @return digest prefix - */ - public static String getSignatureDigestPrefix(PGPSignature signature) { - return Hex.toHexString(signature.getDigestPrefix()); - } - - public static boolean wasIssuedBy(byte[] fingerprint, PGPSignature signature) { - try { - OpenPgpFingerprint fp = OpenPgpFingerprint.parseFromBinary(fingerprint); - OpenPgpFingerprint issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (issuerFp == null) { - return fp.getKeyId() == signature.getKeyID(); - } - return fp.equals(issuerFp); - } catch (IllegalArgumentException e) { - // Unknown fingerprint length - return false; - } - } - - /** - * Extract all signatures from the given
key
which were issued by
issuerKeyId
- * over
userId
. - * - * @param key public key - * @param userId user-id - * @param issuerKeyId issuer key-id - * @return (potentially empty) list of signatures - */ - public static @Nonnull List getSignaturesOverUserIdBy( - @Nonnull PGPPublicKey key, - @Nonnull String userId, - long issuerKeyId) { - List signaturesByKeyId = new ArrayList<>(); - Iterator userIdSignatures = key.getSignaturesForID(userId); - - // getSignaturesForID() is nullable for some reason -.- - if (userIdSignatures == null) { - return signaturesByKeyId; - } - - // filter for signatures by key-id - while (userIdSignatures.hasNext()) { - PGPSignature signature = userIdSignatures.next(); - if (signature.getKeyID() == issuerKeyId) { - signaturesByKeyId.add(signature); - } - } - - return Collections.unmodifiableList(signaturesByKeyId); - } - - public static @Nonnull List getDelegations(PGPPublicKeyRing key) { - List delegations = new ArrayList<>(); - PGPPublicKey primaryKey = key.getPublicKey(); - Iterator signatures = primaryKey.getKeySignatures(); - outerloop: while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - Iterator subkeys = key.getPublicKeys(); - while (subkeys.hasNext()) { - if (signature.getKeyID() == subkeys.next().getKeyID()) { - continue outerloop; - } - } - delegations.add(signature); - } - - return delegations; - } - - public static @Nonnull List get3rdPartyCertificationsFor(String userId, PGPPublicKeyRing key) { - PGPPublicKey primaryKey = key.getPublicKey(); - List certifications = new ArrayList<>(); - Iterator it = primaryKey.getSignaturesForID(userId); - while (it.hasNext()) { - PGPSignature sig = it.next(); - if (sig.getKeyID() != primaryKey.getKeyID()) { - certifications.add(sig); - } - } - return certifications; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt new file mode 100644 index 00000000..2f320bf0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature + +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.encoders.Hex +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.RevocationAttributes.Reason +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.util.ArmorUtils +import java.io.InputStream +import java.util.* + +const val MAX_ITERATIONS = 10000 + +class SignatureUtils { + companion object { + + /** + * Extract and return the key expiration date value from the given signature. + * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. + * + * @param keyCreationDate creation date of the key + * @param signature signature + * @return key expiration date as given by the signature + */ + @JvmStatic + fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { + val expirationPacket: KeyExpirationTime? = SignatureSubpacketsUtil.getKeyExpirationTime(signature) + val expiresInSeconds = expirationPacket?.time ?: 0 + return datePlusSeconds(keyCreationDate, expiresInSeconds) + } + + /** + * Return the expiration date of the signature. + * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. + * + * @param signature signature + * @return expiration date of the signature, or null if it does not expire. + */ + @JvmStatic + fun getSignatureExpirationDate(signature: PGPSignature): Date? { + val creationTime = signature.creationTime + val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) + val expiresInSeconds = expirationTime?.time ?: 0 + return datePlusSeconds(creationTime, expiresInSeconds) + } + + /** + * Return a new date which represents the given date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification + * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ + @JvmStatic + fun datePlusSeconds(date: Date, seconds: Long): Date? { + if (seconds == 0L) { + return null + } + return Date(date.time + 1000 * seconds) + } + + /** + * Return true, if the expiration date of the {@link PGPSignature} lays in the past. + * If no expiration date is present in the signature, it is considered non-expired. + * + * @param signature signature + * @return true if expired, false otherwise + */ + @JvmStatic + fun isSignatureExpired(signature: PGPSignature): Boolean { + return isSignatureExpired(signature, Date()) + } + + /** + * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. + * If no expiration date is present in the signature, it is considered non-expiring. + * + * @param signature signature + * @param referenceTime reference date + * @return true if sig is expired at reference date, false otherwise + */ + @JvmStatic + fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { + val expirationDate = getSignatureExpirationDate(signature) + return expirationDate != null && referenceTime >= expirationDate + } + + /** + * Return true if the provided signature is a hard revocation. + * Hard revocations are revocation signatures which either carry a revocation reason of + * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, + * or no reason at all. + * + * @param signature signature + * @return true if signature is a hard revocation + */ + @JvmStatic + fun isHardRevocation(signature: PGPSignature): Boolean { + val type = SignatureType.requireFromCode(signature.signatureType) + if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { + // Not a revocation + return false + } + + val reason = SignatureSubpacketsUtil.getRevocationReason(signature) ?: return true // no reason -> hard revocation + return Reason.isHardRevocation(reason.revocationReason) + } + + @JvmStatic + fun readSignatures(encodedSignatures: String): List { + return readSignatures(encodedSignatures.toByteArray()) + } + + @JvmStatic + fun readSignatures(encodedSignatures: ByteArray): List { + return readSignatures(encodedSignatures.inputStream()) + } + + @JvmStatic + fun readSignatures(inputStream: InputStream): List { + return readSignatures(inputStream, MAX_ITERATIONS) + } + + /** + * Read and return {@link PGPSignature PGPSignatures}. + * This method can deal with signatures that may be binary, armored and may contain marker packets. + * + * @param inputStream input stream + * @param maxIterations number of loop iterations until reading is aborted + * @return list of encountered signatures + */ + @JvmStatic + fun readSignatures(inputStream: InputStream, maxIterations: Int): List { + val signatures = mutableListOf() + val pgpIn = ArmorUtils.getDecoderStream(inputStream) + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + + var i = 0 + var nextObject: Any? = null + while (i++ < maxIterations && objectFactory.nextObject().also { nextObject = it } != null) { + // Since signatures are indistinguishable from randomness, there is no point in having them compressed, + // except for an attacker who is trying to exploit flaws in the decompression algorithm. + // Therefore, we ignore compressed data packets without attempting decompression. + if (nextObject is PGPCompressedData) { + // getInputStream() does not do decompression, contrary to getDataStream(). + Streams.drain((nextObject as PGPCompressedData).inputStream) // Skip packet without decompressing + } + + if (nextObject is PGPSignatureList) { + signatures.addAll(nextObject as PGPSignatureList) + } + + if (nextObject is PGPSignature) { + signatures.add(nextObject as PGPSignature) + } + } + + pgpIn.close() + return signatures.toList() + } + + /** + * Determine the issuer key-id of a {@link PGPSignature}. + * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. + * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. + * + * Otherwise, it returns 0. + * @param signature signature + * @return signatures issuing key id + */ + @JvmStatic + fun determineIssuerKeyId(signature: PGPSignature): Long { + if (signature.version == 3) { + // V3 sigs do not contain subpackets + return signature.keyID + } + + val issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature) + val issuerFingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) + + if (issuerKeyId != null && issuerKeyId.keyID != 0L) { + return issuerKeyId.keyID + } + if (issuerKeyId == null && issuerFingerprint != null) { + return issuerFingerprint.keyId + } + return 0 + } + + /** + * Return the digest prefix of the signature as hex-encoded String. + * + * @param signature signature + * @return digest prefix + */ + @JvmStatic + fun getSignatureDigestPrefix(signature: PGPSignature): String { + return Hex.toHexString(signature.digestPrefix) + } + + @JvmStatic + fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { + return try { + val pgpFingerprint = OpenPgpFingerprint.parseFromBinary(fingerprint) + wasIssuedBy(pgpFingerprint, signature) + } catch (e : IllegalArgumentException) { + // Unknown fingerprint length + false + } + } + + @JvmStatic + fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { + val issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) + ?: return fingerprint.keyId == signature.keyID + return fingerprint == issuerFp + } + + /** + * Extract all signatures from the given
key
which were issued by
issuerKeyId
+ * over
userId
. + * + * @param key public key + * @param userId user-id + * @param issuer issuer key-id + * @return (potentially empty) list of signatures + */ + @JvmStatic + fun getSignaturesOverUserIdBy(key: PGPPublicKey, userId: String, issuer: Long): List { + return key.getSignaturesForID(userId) + ?.asSequence() + ?.filter { it.keyID == issuer } + ?.toList() ?: listOf() + } + + @JvmStatic + fun getDelegations(key: PGPPublicKeyRing): List { + return key.publicKey.keySignatures + .asSequence() + .filter { key.getPublicKey(it.keyID) == null } // Filter out back-sigs from subkeys + .toList() + } + + @JvmStatic + fun get3rdPartyCertificationsFor(key: PGPPublicKeyRing, userId: String): List { + return key.publicKey.getSignaturesForID(userId) + .asSequence() + .filter { it.keyID != key.publicKey.keyID } // Filter out self-sigs + .toList() + } + } +} \ No newline at end of file From 85b1ffe2e9113797c319d777e1eae4c9c706ab0e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 28 Aug 2023 16:18:41 +0200 Subject: [PATCH 079/351] Add PGPSignatureExtensions file --- .../extensions/PGPSignatureExtensions.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt new file mode 100644 index 00000000..657abba9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.signature.SignatureUtils +import java.util.* + +fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = + SignatureUtils.getKeyExpirationDate(keyCreationDate, this) + +fun PGPSignature.getSignatureExpirationDate(): Date? = + SignatureUtils.getSignatureExpirationDate(this) + +fun PGPSignature.isExpired(referenceTime: Date = Date()) = + SignatureUtils.isSignatureExpired(this, referenceTime) + +fun PGPSignature.getIssuerKeyId() = SignatureUtils.determineIssuerKeyId(this) + +fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint) = SignatureUtils.wasIssuedBy(fingerprint, this) From fa765fdb0d462acb563e63258535c7e305612a1c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 28 Aug 2023 16:26:52 +0200 Subject: [PATCH 080/351] Add documentation to PGPSignatureExtensions --- .../extensions/PGPSignatureExtensions.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 657abba9..ccbb8f45 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -9,15 +9,38 @@ import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.signature.SignatureUtils import java.util.* +/** + * Return the value of the KeyExpirationDate subpacket, or null, if the signature does not carry + * such a subpacket. + */ fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = SignatureUtils.getKeyExpirationDate(keyCreationDate, this) +/** + * Return the value of the signature ExpirationTime subpacket, or null, if the signature + * does not carry such a subpacket. + */ fun PGPSignature.getSignatureExpirationDate(): Date? = SignatureUtils.getSignatureExpirationDate(this) +/** + * Return true, if the signature is expired at the given reference time. + */ fun PGPSignature.isExpired(referenceTime: Date = Date()) = SignatureUtils.isSignatureExpired(this, referenceTime) +/** + * Return the key-ID of the issuer, determined by examining the IssuerKeyId and IssuerFingerprint + * subpackets of the signature. + */ fun PGPSignature.getIssuerKeyId() = SignatureUtils.determineIssuerKeyId(this) +/** + * Return true, if the signature was likely issued by the key with the given fingerprint. + */ fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint) = SignatureUtils.wasIssuedBy(fingerprint, this) + +/** + * Return true, if this signature is a hard revocation. + */ +fun PGPSignature.isHardRevocation() = SignatureUtils.isHardRevocation(this) From 6dc08e74450c31d7f47e049c95139992e1632b0f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 28 Aug 2023 16:39:53 +0200 Subject: [PATCH 081/351] Improve SignatureUtils readability --- .../pgpainless/signature/SignatureUtils.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 2f320bf0..b9b80f8f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -24,7 +24,7 @@ class SignatureUtils { /** * Extract and return the key expiration date value from the given signature. - * If the signature does not carry a {@link KeyExpirationTime} subpacket, return null. + * If the signature does not carry a [KeyExpirationTime] subpacket, return null. * * @param keyCreationDate creation date of the key * @param signature signature @@ -32,24 +32,24 @@ class SignatureUtils { */ @JvmStatic fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { - val expirationPacket: KeyExpirationTime? = SignatureSubpacketsUtil.getKeyExpirationTime(signature) - val expiresInSeconds = expirationPacket?.time ?: 0 + val expirationPacket: KeyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature) ?: return null + val expiresInSeconds = expirationPacket.time return datePlusSeconds(keyCreationDate, expiresInSeconds) } /** * Return the expiration date of the signature. - * If the signature has no expiration date, {@link #datePlusSeconds(Date, long)} will return null. + * If the signature has no expiration date, [datePlusSeconds] will return null. * * @param signature signature * @return expiration date of the signature, or null if it does not expire. */ @JvmStatic fun getSignatureExpirationDate(signature: PGPSignature): Date? { - val creationTime = signature.creationTime - val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) - val expiresInSeconds = expirationTime?.time ?: 0 - return datePlusSeconds(creationTime, expiresInSeconds) + val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) ?: return null + + val expiresInSeconds = expirationTime.time + return datePlusSeconds(signature.creationTime, expiresInSeconds) } /** @@ -71,7 +71,7 @@ class SignatureUtils { } /** - * Return true, if the expiration date of the {@link PGPSignature} lays in the past. + * Return true, if the expiration date of the [PGPSignature] lays in the past. * If no expiration date is present in the signature, it is considered non-expired. * * @param signature signature @@ -83,7 +83,7 @@ class SignatureUtils { } /** - * Return true, if the expiration date of the given {@link PGPSignature} is past the given comparison {@link Date}. + * Return true, if the expiration date of the given [PGPSignature] is past the given comparison [Date]. * If no expiration date is present in the signature, it is considered non-expiring. * * @param signature signature @@ -92,15 +92,14 @@ class SignatureUtils { */ @JvmStatic fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { - val expirationDate = getSignatureExpirationDate(signature) - return expirationDate != null && referenceTime >= expirationDate + val expirationDate = getSignatureExpirationDate(signature) ?: return false + return referenceTime >= expirationDate } /** * Return true if the provided signature is a hard revocation. * Hard revocations are revocation signatures which either carry a revocation reason of - * {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON}, - * or no reason at all. + * [Reason.KEY_COMPROMISED] or [Reason.NO_REASON], or no reason at all. * * @param signature signature * @return true if signature is a hard revocation @@ -133,7 +132,7 @@ class SignatureUtils { } /** - * Read and return {@link PGPSignature PGPSignatures}. + * Read and return [PGPSignatures][PGPSignature]. * This method can deal with signatures that may be binary, armored and may contain marker packets. * * @param inputStream input stream @@ -171,9 +170,9 @@ class SignatureUtils { } /** - * Determine the issuer key-id of a {@link PGPSignature}. - * This method first inspects the {@link IssuerKeyID} subpacket of the signature and returns the key-id if present. - * If not, it inspects the {@link org.bouncycastle.bcpg.sig.IssuerFingerprint} packet and retrieves the key-id from the fingerprint. + * Determine the issuer key-id of a [PGPSignature]. + * This method first inspects the [org.bouncycastle.bcpg.sig.IssuerKeyID] subpacket of the signature and returns the key-id if present. + * If not, it inspects the [org.bouncycastle.bcpg.sig.IssuerFingerprint] packet and retrieves the key-id from the fingerprint. * * Otherwise, it returns 0. * @param signature signature @@ -238,10 +237,11 @@ class SignatureUtils { */ @JvmStatic fun getSignaturesOverUserIdBy(key: PGPPublicKey, userId: String, issuer: Long): List { - return key.getSignaturesForID(userId) - ?.asSequence() - ?.filter { it.keyID == issuer } - ?.toList() ?: listOf() + val signatures = key.getSignaturesForID(userId) ?: return listOf() + return signatures + .asSequence() + .filter { it.keyID == issuer } + .toList() } @JvmStatic From cc63095ab0197539fc2c654366ee7b5388f3dcbe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 28 Aug 2023 18:24:16 +0200 Subject: [PATCH 082/351] Kotlin conversion: SignatureSubpacketsUtil --- .../subpackets/SignatureSubpacketsUtil.java | 703 ------------------ .../subpackets/SignatureSubpacketsUtil.kt | 575 ++++++++++++++ 2 files changed, 575 insertions(+), 703 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt 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 deleted file mode 100644 index 6d53ad1d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ /dev/null @@ -1,703 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.IssuerFingerprint; -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.bcpg.sig.SignatureTarget; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.bcpg.sig.TrustSignature; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.OpenPgpV4Fingerprint; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.signature.SignatureUtils; - -/** - * Utility class to access signature subpackets from signatures. - * - * Since rfc4880 is not always clear about where a signature subpacket can be located (hashed/unhashed area), - * this class makes some educated guesses as to where the subpacket may be found when necessary. - */ -public final class SignatureSubpacketsUtil { - - private SignatureSubpacketsUtil() { - - } - - /** - * Return the issuer-fingerprint subpacket of the signature. - * Since this packet is self-authenticating, we expect it to be in the unhashed area, - * however as it cannot hurt we search for it in the hashed area first. - * - * @param signature signature - * @return issuer fingerprint or null - */ - public static @Nullable IssuerFingerprint getIssuerFingerprint(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint); - } - - /** - * Return the {@link IssuerFingerprint} subpacket of the signature into a {@link org.pgpainless.key.OpenPgpFingerprint}. - * If no v4 issuer fingerprint is present in the signature, return null. - * - * @param signature signature - * @return v4 fingerprint of the issuer, or null - */ - public static @Nullable OpenPgpFingerprint getIssuerFingerprintAsOpenPgpFingerprint(PGPSignature signature) { - IssuerFingerprint subpacket = getIssuerFingerprint(signature); - if (subpacket == null) { - return null; - } - - OpenPgpFingerprint fingerprint = null; - if (subpacket.getKeyVersion() == 4) { - fingerprint = new OpenPgpV4Fingerprint(subpacket.getFingerprint()); - } - - return fingerprint; - } - - /** - * Return the issuer key-id subpacket of the signature. - * Since this packet is self-authenticating, we expect it to be in the unhashed area, - * however as it cannot hurt we search for it in the hashed area first. - * - * @param signature signature - * @return issuer key-id or null - */ - public static @Nullable IssuerKeyID getIssuerKeyId(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId); - } - - /** - * Inspect the given signature's {@link IssuerKeyID} packet to determine the issuer key-id. - * If no such packet is present, return null. - * - * @param signature signature - * @return issuer key-id as {@link Long} - */ - public static @Nullable Long getIssuerKeyIdAsLong(PGPSignature signature) { - IssuerKeyID keyID = getIssuerKeyId(signature); - if (keyID == null) { - return null; - } - return keyID.getKeyID(); - } - - /** - * Return the revocation reason subpacket of the signature. - * Since this packet is rather important for revocations, we only search for it in the - * hashed area of the signature. - * - * @param signature signature - * @return revocation reason - */ - public static @Nullable RevocationReason getRevocationReason(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocationReason); - } - - /** - * Return the signature creation time subpacket. - * Since this packet is rather important, we only search for it in the hashed area - * of the signature. - * - * @param signature signature - * @return signature creation time subpacket - */ - public static @Nullable SignatureCreationTime getSignatureCreationTime(PGPSignature signature) { - if (signature.getVersion() == 3) { - return new SignatureCreationTime(false, signature.getCreationTime()); - } - return hashed(signature, SignatureSubpacket.signatureCreationTime); - } - - /** - * Return the signature expiration time subpacket of the signature. - * Since this packet is rather important, we only search for it in the hashed area of the signature. - * - * @param signature signature - * @return signature expiration time - */ - public static @Nullable SignatureExpirationTime getSignatureExpirationTime(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.signatureExpirationTime); - } - - /** - * Return the signatures' expiration time as a date. - * The expiration date is computed by adding the expiration time to the signature creation date. - * If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null. - * - * @param signature signature - * @return expiration time as date - */ - public static @Nullable Date getSignatureExpirationTimeAsDate(PGPSignature signature) { - SignatureExpirationTime subpacket = getSignatureExpirationTime(signature); - if (subpacket == null) { - return null; - } - return SignatureUtils.datePlusSeconds(signature.getCreationTime(), subpacket.getTime()); - } - - /** - * Return the key expiration time subpacket of this signature. - * We only look for it in the hashed area of the signature. - * - * @param signature signature - * @return key expiration time - */ - public static @Nullable KeyExpirationTime getKeyExpirationTime(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.keyExpirationTime); - } - - /** - * Return the signatures key-expiration time as a date. - * The expiration date is computed by adding the signatures' key-expiration time to the signing keys - * creation date. - * If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null. - * - * @param signature self-signature carrying the key-expiration time subpacket - * @param signingKey signature creation key - * @return key expiration time as date - */ - public static @Nullable Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) { - if (signature.getKeyID() != signingKey.getKeyID()) { - throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")"); - } - KeyExpirationTime subpacket = getKeyExpirationTime(signature); - if (subpacket == null) { - return null; - } - - return SignatureUtils.datePlusSeconds(signingKey.getCreationTime(), subpacket.getTime()); - } - - /** - * Calculate the duration in seconds until the key expires after creation. - * - * @param expirationDate new expiration date - * @param creationDate key creation time - * @return lifetime of the key in seconds - */ - public static long getKeyLifetimeInSeconds(@Nullable Date expirationDate, @Nonnull Date creationDate) { - long secondsToExpire = 0; // 0 means "no expiration" - if (expirationDate != null) { - if (creationDate.after(expirationDate)) { - throw new IllegalArgumentException("Key MUST NOT expire before being created. " + - "(creation: " + creationDate + ", expiration: " + expirationDate + ")"); - } - secondsToExpire = (expirationDate.getTime() - creationDate.getTime()) / 1000; - } - return secondsToExpire; - } - - /** - * Return the revocable subpacket of this signature. - * We only look for it in the hashed area of the signature. - * - * @param signature signature - * @return revocable subpacket - */ - public static @Nullable Revocable getRevocable(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocable); - } - - /** - * Return the symmetric algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return symm. algo. prefs - */ - public static @Nullable PreferredAlgorithms getPreferredSymmetricAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms); - } - - /** - * Return the preferred {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} as present in the signature. - * If no preference is given with regard to symmetric encryption algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of symmetric key algorithm preferences - */ - public static @Nonnull Set parsePreferredSymmetricKeyAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredSymmetricAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the hash algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return hash algo prefs - */ - public static @Nullable PreferredAlgorithms getPreferredHashAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredHashAlgorithms); - } - - /** - * Return the preferred {@link HashAlgorithm HashAlgorithms} as present in the signature. - * If no preference is given with regard to hash algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of hash algorithm preferences - */ - public static @Nonnull Set parsePreferredHashAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredHashAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - HashAlgorithm algorithm = HashAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the compression algorithm preferences from the signatures hashed area. - * - * @param signature signature - * @return compression algo prefs - */ - public static @Nullable PreferredAlgorithms getPreferredCompressionAlgorithms(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms); - } - - /** - * Return the preferred {@link CompressionAlgorithm CompressionAlgorithms} as present in the signature. - * If no preference is given with regard to compression algorithms, return an empty set. - * - * In any case, the resulting set is ordered by occurrence ({@link LinkedHashSet}). - * @param signature signature - * @return ordered set of compression algorithm preferences - */ - public static @Nonnull Set parsePreferredCompressionAlgorithms(PGPSignature signature) { - Set algorithms = new LinkedHashSet<>(); - PreferredAlgorithms preferences = getPreferredCompressionAlgorithms(signature); - if (preferences != null) { - for (int code : preferences.getPreferences()) { - CompressionAlgorithm algorithm = CompressionAlgorithm.fromId(code); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - } - return algorithms; - } - - /** - * Return the primary user-id subpacket from the signatures hashed area. - * - * @param signature signature - * @return primary user id - */ - public static @Nullable PrimaryUserID getPrimaryUserId(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.primaryUserId); - } - - /** - * Return the key flags subpacket from the signatures hashed area. - * - * @param signature signature - * @return key flags - */ - public static @Nullable KeyFlags getKeyFlags(PGPSignature signature) { - 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 @Nullable 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. - * - * @param signature signature - * @return features subpacket - */ - public static @Nullable Features getFeatures(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.features); - } - - /** - * Parse out the features subpacket of a signature. - * If the signature has no features subpacket, return null. - * Otherwise, return the features as a feature set. - * - * @param signature signature - * @return features as set - */ - public static @Nullable Set parseFeatures(PGPSignature signature) { - Features features = getFeatures(signature); - if (features == null) { - return null; - } - return new LinkedHashSet<>(Feature.fromBitmask(features.getData()[0])); - } - - /** - * Return the signature target subpacket from the signature. - * We search for this subpacket in the hashed and unhashed area (in this order). - * - * @param signature signature - * @return signature target - */ - public static @Nullable SignatureTarget getSignatureTarget(PGPSignature signature) { - return hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget); - } - - /** - * Return the notation data subpackets from the signatures hashed area. - * - * @param signature signature - * @return hashed notations - */ - public static @Nonnull List getHashedNotationData(PGPSignature signature) { - NotationData[] notations = signature.getHashedSubPackets().getNotationDataOccurrences(); - return Arrays.asList(notations); - } - - /** - * Return a list of all {@link NotationData} objects from the hashed area of the signature that have a - * notation name equal to the given notationName argument. - * - * @param signature signature - * @param notationName notation name - * @return list of matching notation data objects - */ - public static @Nonnull List getHashedNotationData(PGPSignature signature, String notationName) { - List allNotations = getHashedNotationData(signature); - List withName = new ArrayList<>(); - for (NotationData data : allNotations) { - if (data.getNotationName().equals(notationName)) { - withName.add(data); - } - } - return withName; - } - - /** - * Return the notation data subpackets from the signatures unhashed area. - * - * @param signature signature - * @return unhashed notations - */ - public static @Nonnull List getUnhashedNotationData(PGPSignature signature) { - NotationData[] notations = signature.getUnhashedSubPackets().getNotationDataOccurrences(); - return Arrays.asList(notations); - } - - /** - * Return a list of all {@link NotationData} objects from the unhashed area of the signature that have a - * notation name equal to the given notationName argument. - * - * @param signature signature - * @param notationName notation name - * @return list of matching notation data objects - */ - public static @Nonnull List getUnhashedNotationData(PGPSignature signature, String notationName) { - List allNotations = getUnhashedNotationData(signature); - List withName = new ArrayList<>(); - for (NotationData data : allNotations) { - if (data.getNotationName().equals(notationName)) { - withName.add(data); - } - } - return withName; - } - - /** - * Return the revocation key subpacket from the signatures hashed area. - * - * @param signature signature - * @return revocation key - */ - public static @Nullable RevocationKey getRevocationKey(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.revocationKey); - } - - /** - * Return the signers user-id from the hashed area of the signature. - * TODO: Can this subpacket also be found in the unhashed area? - * - * @param signature signature - * @return signers user-id - */ - public static @Nullable SignerUserID getSignerUserID(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.signerUserId); - } - - /** - * Return the intended recipients fingerprint subpackets from the hashed area of this signature. - * - * @param signature signature - * @return intended recipient fingerprint subpackets - */ - public static @Nonnull List getIntendedRecipientFingerprints(PGPSignature signature) { - org.bouncycastle.bcpg.SignatureSubpacket[] subpackets = signature.getHashedSubPackets().getSubpackets(SignatureSubpacket.intendedRecipientFingerprint.getCode()); - List intendedRecipients = new ArrayList<>(subpackets.length); - for (org.bouncycastle.bcpg.SignatureSubpacket subpacket : subpackets) { - intendedRecipients.add((IntendedRecipientFingerprint) subpacket); - } - return intendedRecipients; - } - - /** - * Return the embedded signature subpacket from the signatures hashed area. - * - * @param signature signature - * @return embedded signature - * - * @throws PGPException in case the embedded signatures cannot be parsed - */ - public static @Nullable PGPSignatureList getEmbeddedSignature(PGPSignature signature) throws PGPException { - PGPSignatureList hashed = signature.getHashedSubPackets().getEmbeddedSignatures(); - if (!hashed.isEmpty()) { - return hashed; - } - return signature.getUnhashedSubPackets().getEmbeddedSignatures(); - } - - /** - * Return the signatures exportable certification subpacket from the hashed area. - * - * @param signature signature - * @return exportable certification subpacket - */ - public static @Nullable Exportable getExportableCertification(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.exportableCertification); - } - - public static boolean isExportable(PGPSignature signature) { - Exportable exportable = getExportableCertification(signature); - return exportable == null || exportable.isExportable(); - } - - /** - * Return the trust signature packet from the signatures hashed area. - * - * @param signature signature - * @return trust signature subpacket - */ - public static @Nullable TrustSignature getTrustSignature(PGPSignature signature) { - return hashed(signature, SignatureSubpacket.trustSignature); - } - - public static int getTrustDepthOr(PGPSignature signature, int defaultDepth) { - TrustSignature packet = getTrustSignature(signature); - if (packet != null) { - return packet.getDepth(); - } - return defaultDepth; - } - - public static int getTrustAmountOr(PGPSignature signature, int defaultAmount) { - TrustSignature packet = getTrustSignature(signature); - if (packet != null) { - return packet.getTrustAmount(); - } - return defaultAmount; - } - - /** - * Return all regular expression subpackets from the hashed area of the given signature. - * - * @param signature signature - * @return list of regular expressions - */ - public static List getRegularExpressions(PGPSignature signature) { - org.bouncycastle.bcpg.SignatureSubpacket[] subpackets = signature.getHashedSubPackets() - .getSubpackets(SignatureSubpacket.regularExpression.getCode()); - List regularExpressions = new ArrayList<>(subpackets.length); - for (org.bouncycastle.bcpg.SignatureSubpacket subpacket : subpackets) { - regularExpressions.add((RegularExpression) subpacket); - } - return regularExpressions; - } - - - /** - * Select a list of all signature subpackets of the given type, which are present in the hashed area of - * the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the hashed area - */ - private static @Nullable

P hashed(PGPSignature signature, SignatureSubpacket type) { - return getSignatureSubpacket(signature.getHashedSubPackets(), type); - } - - /** - * Select a list of all signature subpackets of the given type, which are present in the unhashed area of - * the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the unhashed area - */ - private static @Nullable

P unhashed(PGPSignature signature, SignatureSubpacket type) { - return getSignatureSubpacket(signature.getUnhashedSubPackets(), type); - } - - /** - * Select a list of all signature subpackets of the given type, which are present in either the hashed - * or the unhashed area of the given signature. - * - * @param signature signature - * @param type subpacket type - * @param

generic subpacket type - * @return list of subpackets from the hashed/unhashed area - */ - private static @Nullable

P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) { - P hashedSubpacket = hashed(signature, type); - return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, type); - } - - /** - * Return the last occurrence of a subpacket type in the given signature subpacket vector. - * - * @param vector subpacket vector (hashed/unhashed) - * @param type subpacket type - * @param

generic return type of the subpacket - * @return last occurrence of the subpacket in the vector - */ - public static @Nullable

P getSignatureSubpacket(PGPSignatureSubpacketVector vector, SignatureSubpacket type) { - if (vector == null) { - // Almost never happens, but may be caused by broken signatures. - return null; - } - org.bouncycastle.bcpg.SignatureSubpacket[] allPackets = vector.getSubpackets(type.getCode()); - if (allPackets.length == 0) { - return null; - } - - org.bouncycastle.bcpg.SignatureSubpacket last = allPackets[allPackets.length - 1]; - return (P) last; - } - - /** - * Make sure that the given key type can carry the given key flags. - * - * @param type key type - * @param flags key flags - */ - public static void assureKeyCanCarryFlags(KeyType type, KeyFlag... flags) { - final int mask = KeyFlag.toBitmask(flags); - - if (!type.canCertify() && KeyFlag.hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag CERTIFY_OTHER."); - } - - if (!type.canSign() && KeyFlag.hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag SIGN_DATA."); - } - - if (!type.canEncryptCommunication() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag ENCRYPT_COMMS."); - } - - if (!type.canEncryptStorage() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag ENCRYPT_STORAGE."); - } - - if (!type.canAuthenticate() && KeyFlag.hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { - throw new IllegalArgumentException("KeyType " + type.getName() + " cannot carry key flag AUTHENTICATION."); - } - } - - /** - * Make sure that a key of the given {@link PublicKeyAlgorithm} is able to carry the given key flags. - * - * @param algorithm key algorithm - * @param flags key flags - */ - public static void assureKeyCanCarryFlags(PublicKeyAlgorithm algorithm, KeyFlag... flags) { - final int mask = KeyFlag.toBitmask(flags); - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag CERTIFY_OTHER."); - } - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag SIGN_DATA."); - } - - if (!algorithm.isEncryptionCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag ENCRYPT_COMMS."); - } - - if (!algorithm.isEncryptionCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag ENCRYPT_STORAGE."); - } - - if (!algorithm.isSigningCapable() && KeyFlag.hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { - throw new IllegalArgumentException("Algorithm " + algorithm + " cannot be used with key flag AUTHENTICATION."); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt new file mode 100644 index 00000000..23fe716c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -0,0 +1,575 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.algorithm.KeyFlag.Companion.hasKeyFlag +import org.pgpainless.algorithm.KeyFlag.Companion.toBitmask +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.OpenPgpV4Fingerprint +import org.pgpainless.key.OpenPgpV5Fingerprint +import org.pgpainless.key.OpenPgpV6Fingerprint +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.util.KeyIdUtil +import org.pgpainless.signature.SignatureUtils +import java.util.* + +class SignatureSubpacketsUtil { + companion object { + + /** + * Return the issuer-fingerprint subpacket of the signature. + * Since this packet is self-authenticating, we expect it to be in the unhashed area, + * however as it cannot hurt we search for it in the hashed area first. + * + * @param signature signature + * @return issuer fingerprint or null + */ + @JvmStatic + fun getIssuerFingerprint(signature: PGPSignature): IssuerFingerprint? = + hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint) + + /** + * Return the [IssuerFingerprint] subpacket of the signature into a [org.pgpainless.key.OpenPgpFingerprint]. + * If no v4, v5 or v6 issuer fingerprint is present in the signature, return null. + * + * @param signature signature + * @return fingerprint of the issuer, or null + */ + @JvmStatic + fun getIssuerFingerprintAsOpenPgpFingerprint(signature: PGPSignature): OpenPgpFingerprint? { + val subpacket = getIssuerFingerprint(signature) ?: return null + return when(subpacket.keyVersion) { + 4 -> OpenPgpV4Fingerprint(subpacket.fingerprint) + 5 -> OpenPgpV5Fingerprint(subpacket.fingerprint) + 6 -> OpenPgpV6Fingerprint(subpacket.fingerprint) + else -> null + } + } + + @JvmStatic + fun getIssuerKeyId(signature: PGPSignature): IssuerKeyID? = + hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId) + + /** + * Inspect the given signature's [IssuerKeyID] packet to determine the issuer key-id. + * If no such packet is present, return null. + * + * @param signature signature + * @return issuer key-id as {@link Long} + */ + @JvmStatic + fun getIssuerKeyIdAsLong(signature: PGPSignature): Long? = + getIssuerKeyId(signature)?.keyID + + /** + * Return the revocation reason subpacket of the signature. + * Since this packet is rather important for revocations, we only search for it in the + * hashed area of the signature. + * + * @param signature signature + * @return revocation reason + */ + @JvmStatic + fun getRevocationReason(signature: PGPSignature): RevocationReason? = + hashed(signature, SignatureSubpacket.revocationReason) + + /** + * Return the signature creation time subpacket. + * Since this packet is rather important, we only search for it in the hashed area + * of the signature. + * + * @param signature signature + * @return signature creation time subpacket + */ + @JvmStatic + fun getSignatureCreationTime(signature: PGPSignature): SignatureCreationTime? = + if (signature.version == 3) SignatureCreationTime(false, signature.creationTime) + else hashed(signature, SignatureSubpacket.signatureCreationTime) + + /** + * Return the signature expiration time subpacket of the signature. + * Since this packet is rather important, we only search for it in the hashed area of the signature. + * + * @param signature signature + * @return signature expiration time + */ + @JvmStatic + fun getSignatureExpirationTime(signature: PGPSignature): SignatureExpirationTime? = + hashed(signature, SignatureSubpacket.signatureExpirationTime) + + /** + * Return the signatures' expiration time as a date. + * The expiration date is computed by adding the expiration time to the signature creation date. + * If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null. + * + * @param signature signature + * @return expiration time as date + */ + @JvmStatic + fun getSignatureExpirationTimeAsDate(signature: PGPSignature): Date? = + getSignatureExpirationTime(signature)?.let { + SignatureUtils.datePlusSeconds(signature.creationTime, it.time) + } + + /** + * Return the key expiration time subpacket of this signature. + * We only look for it in the hashed area of the signature. + * + * @param signature signature + * @return key expiration time + */ + @JvmStatic + fun getKeyExpirationTime(signature: PGPSignature): KeyExpirationTime? = + hashed(signature, SignatureSubpacket.keyExpirationTime) + + /** + * Return the signatures key-expiration time as a date. + * The expiration date is computed by adding the signatures' key-expiration time to the signing keys + * creation date. + * If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null. + * + * @param signature self-signature carrying the key-expiration time subpacket + * @param signingKey signature creation key + * @return key expiration time as date + */ + @JvmStatic + fun getKeyExpirationTimeAsDate(signature: PGPSignature, signingKey: PGPPublicKey): Date? = + require(signature.keyID == signingKey.keyID) { + "Provided key (${KeyIdUtil.formatKeyId(signingKey.keyID)}) did not create the signature (${KeyIdUtil.formatKeyId(signature.keyID)})" + }.run { + getKeyExpirationTime(signature)?.let { + SignatureUtils.datePlusSeconds(signingKey.creationTime, it.time) + } + } + + /** + * Calculate the duration in seconds until the key expires after creation. + * + * @param expirationTime new expiration date + * @param creationTime key creation time + * @return lifetime of the key in seconds + */ + @JvmStatic + fun getKeyLifetimeInSeconds(creationTime: Date, expirationTime: Date?): Long = + expirationTime?.let { + require(creationTime <= it) { + "Key MUST NOT expire before being created.\n" + + "(creation: $creationTime, expiration: $it)" + }.run { + (it.time - creationTime.time) / 1000 + } + } ?: 0 // 0 means "no expiration" + + /** + * Return the revocable subpacket of this signature. + * We only look for it in the hashed area of the signature. + * + * @param signature signature + * @return revocable subpacket + */ + @JvmStatic + fun getRevocable(signature: PGPSignature): Revocable? = + hashed(signature, SignatureSubpacket.revocable) + + /** + * Return the symmetric algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return symm. algo. prefs + */ + @JvmStatic + fun getPreferredSymmetricAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms) + + /** + * Return the preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] as present in the signature. + * If no preference is given with regard to symmetric encryption algorithms, return an empty set. + * + * In any case, the resulting set is ordered by occurrence. + * @param signature signature + * @return ordered set of symmetric key algorithm preferences + */ + @JvmStatic + fun parsePreferredSymmetricKeyAlgorithms(signature: PGPSignature): Set = + getPreferredSymmetricAlgorithms(signature) + ?.preferences + ?.map { SymmetricKeyAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() ?: setOf() + + /** + * Return the hash algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return hash algo prefs + */ + @JvmStatic + fun getPreferredHashAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredHashAlgorithms) + + /** + * Return the preferred [HashAlgorithms][HashAlgorithm] as present in the signature. + * If no preference is given with regard to hash algorithms, return an empty set. + * + * In any case, the resulting set is ordered by occurrence. + * @param signature signature + * @return ordered set of hash algorithm preferences + */ + @JvmStatic + fun parsePreferredHashAlgorithms(signature: PGPSignature): Set = + getPreferredHashAlgorithms(signature) + ?.preferences + ?.map { HashAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() ?: setOf() + + /** + * Return the compression algorithm preferences from the signatures hashed area. + * + * @param signature signature + * @return compression algo prefs + */ + @JvmStatic + fun getPreferredCompressionAlgorithms(signature: PGPSignature): PreferredAlgorithms? = + hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms) + + /** + * Return the preferred [CompressionAlgorithms][CompressionAlgorithm] as present in the signature. + * If no preference is given with regard to compression algorithms, return an empty set. + * + * In any case, the resulting set is ordered by occurrence. + * @param signature signature + * @return ordered set of compression algorithm preferences + */ + @JvmStatic + fun parsePreferredCompressionAlgorithms(signature: PGPSignature): Set = + getPreferredCompressionAlgorithms(signature) + ?.preferences + ?.map { CompressionAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() ?: setOf() + + @JvmStatic + fun getPreferredAeadAlgorithms(signature: PGPSignature): PreferredAEADCiphersuites? = + hashed(signature, SignatureSubpacket.preferredAEADAlgorithms) + + /** + * Return the primary user-id subpacket from the signatures hashed area. + * + * @param signature signature + * @return primary user id + */ + @JvmStatic + fun getPrimaryUserId(signature: PGPSignature): PrimaryUserID? = + hashed(signature, SignatureSubpacket.primaryUserId) + + /** + * Return the key flags subpacket from the signatures hashed area. + * + * @param signature signature + * @return key flags + */ + @JvmStatic + fun getKeyFlags(signature: PGPSignature): KeyFlags? = + hashed(signature, SignatureSubpacket.keyFlags) + + /** + * Return a list of key flags carried by the signature. + * If the signature is null, or has no [KeyFlags] subpacket, return null. + * + * @param signature signature + * @return list of key flags + */ + @JvmStatic + fun parseKeyFlags(signature: PGPSignature?): List? = + signature?.let { sig -> + getKeyFlags(sig)?.let { + KeyFlag.fromBitmask(it.flags) + } + } + + /** + * Return the features subpacket from the signatures hashed area. + * + * @param signature signature + * @return features subpacket + */ + @JvmStatic + fun getFeatures(signature: PGPSignature): Features? = + hashed(signature, SignatureSubpacket.features) + + /** + * Parse out the features subpacket of a signature. + * If the signature has no features subpacket, return null. + * Otherwise, return the features as a feature set. + * + * @param signature signature + * @return features as set + */ + @JvmStatic + fun parseFeatures(signature: PGPSignature): Set? = + getFeatures(signature)?.let { + Feature.fromBitmask(it.features.toInt()).toSet() + } + + /** + * Return the signature target subpacket from the signature. + * We search for this subpacket in the hashed and unhashed area (in this order). + * + * @param signature signature + * @return signature target + */ + @JvmStatic + fun getSignatureTarget(signature: PGPSignature): SignatureTarget? = + hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget) + + /** + * Return the notation data subpackets from the signatures hashed area. + * + * @param signature signature + * @return hashed notations + */ + @JvmStatic + fun getHashedNotationData(signature: PGPSignature): List = + signature.hashedSubPackets.notationDataOccurrences.toList() + + /** + * Return a list of all [NotationData] objects from the hashed area of the signature that have a + * notation name equal to the given notationName argument. + * + * @param signature signature + * @param notationName notation name + * @return list of matching notation data objects + */ + @JvmStatic + fun getHashedNotationData(signature: PGPSignature, notationName: String): List = + getHashedNotationData(signature) + .filter { it.notationName == notationName } + + /** + * Return the notation data subpackets from the signatures unhashed area. + * + * @param signature signature + * @return unhashed notations + */ + @JvmStatic + fun getUnhashedNotationData(signature: PGPSignature): List = + signature.unhashedSubPackets.notationDataOccurrences.toList() + + /** + * Return a list of all [NotationData] objects from the unhashed area of the signature that have a + * notation name equal to the given notationName argument. + * + * @param signature signature + * @param notationName notation name + * @return list of matching notation data objects + */ + @JvmStatic + fun getUnhashedNotationData(signature: PGPSignature, notationName: String) = + getUnhashedNotationData(signature) + .filter { it.notationName == notationName } + + /** + * Return the revocation key subpacket from the signatures hashed area. + * + * @param signature signature + * @return revocation key + */ + @JvmStatic + fun getRevocationKey(signature: PGPSignature): RevocationKey? = + hashed(signature, SignatureSubpacket.revocationKey) + + /** + * Return the signers user-id from the hashed area of the signature. + * TODO: Can this subpacket also be found in the unhashed area? + * + * @param signature signature + * @return signers user-id + */ + @JvmStatic + fun getSignerUserID(signature: PGPSignature): SignerUserID? = + hashed(signature, SignatureSubpacket.signerUserId) + + /** + * Return the intended recipients fingerprint subpackets from the hashed area of this signature. + * + * @param signature signature + * @return intended recipient fingerprint subpackets + */ + @JvmStatic + fun getIntendedRecipientFingerprints(signature: PGPSignature): List = + signature.hashedSubPackets.intendedRecipientFingerprints.toList() + + /** + * Return the embedded signature subpacket from the signatures hashed area or unhashed area. + * + * @param signature signature + * @return embedded signature + */ + @JvmStatic + fun getEmbeddedSignature(signature: PGPSignature): PGPSignatureList = + signature.hashedSubPackets.embeddedSignatures.let { + if (it.isEmpty) signature.unhashedSubPackets.embeddedSignatures + else it + } + + /** + * Return the signatures exportable certification subpacket from the hashed area. + * + * @param signature signature + * @return exportable certification subpacket + */ + @JvmStatic + fun getExportableCertification(signature: PGPSignature): Exportable? = + hashed(signature, SignatureSubpacket.exportableCertification) + + /** + * Return true, if the signature is not explicitly marked as non-exportable. + */ + @JvmStatic + fun isExportable(signature: PGPSignature): Boolean = + getExportableCertification(signature)?.isExportable ?: true + + /** + * Return the trust signature packet from the signatures hashed area. + * + * @param signature signature + * @return trust signature subpacket + */ + @JvmStatic + fun getTrustSignature(signature: PGPSignature): TrustSignature? = + hashed(signature, SignatureSubpacket.trustSignature) + + /** + * Return the trust depth set in the signatures [TrustSignature] packet, or [defaultDepth] if no such packet + * is found. + * + * @param signature signature + * @param defaultDepth default value that is returned if no trust signature packet is found + * @return depth or default depth + */ + @JvmStatic + fun getTrustDepthOr(signature: PGPSignature, defaultDepth: Int): Int = + getTrustSignature(signature)?.depth ?: defaultDepth + + /** + * Return the trust amount set in the signatures [TrustSignature] packet, or [defaultAmount] if no such packet + * is found. + * + * @param signature signature + * @param defaultAmount default value that is returned if no trust signature packet is found + * @return amount or default amount + */ + @JvmStatic + fun getTrustAmountOr(signature: PGPSignature, defaultAmount: Int): Int = + getTrustSignature(signature)?.trustAmount ?: defaultAmount + + /** + * Return all regular expression subpackets from the hashed area of the given signature. + * + * @param signature signature + * @return list of regular expressions + */ + @JvmStatic + fun getRegularExpressions(signature: PGPSignature): List = + signature.hashedSubPackets.regularExpressions.toList() + + /** + * Select a list of all signature subpackets of the given type, which are present in either the hashed + * or the unhashed area of the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed/unhashed area + */ + @JvmStatic + fun

hashedOrUnhashed(signature: PGPSignature, type: SignatureSubpacket): P? { + return hashed(signature, type) ?: unhashed(signature, type) + } + + /** + * Select a list of all signature subpackets of the given type, which are present in the hashed area of + * the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the hashed area + */ + @JvmStatic + fun

hashed(signature: PGPSignature, type: SignatureSubpacket): P? { + return getSignatureSubpacket(signature.hashedSubPackets, type) + } + + /** + * Select a list of all signature subpackets of the given type, which are present in the unhashed area of + * the given signature. + * + * @param signature signature + * @param type subpacket type + * @param

generic subpacket type + * @return list of subpackets from the unhashed area + */ + @JvmStatic + fun

unhashed(signature: PGPSignature, type: SignatureSubpacket): P? { + return getSignatureSubpacket(signature.unhashedSubPackets, type) + } + + /** + * Return the last occurrence of a subpacket type in the given signature subpacket vector. + * + * @param vector subpacket vector (hashed/unhashed) + * @param type subpacket type + * @param

generic return type of the subpacket + * @return last occurrence of the subpacket in the vector + */ + @JvmStatic + fun

getSignatureSubpacket(vector: PGPSignatureSubpacketVector?, type: SignatureSubpacket): P? { + val allPackets = vector?.getSubpackets(type.code) ?: return null + return if (allPackets.isEmpty()) + null + else + @Suppress("UNCHECKED_CAST") + allPackets.last() as P + } + + @JvmStatic + fun assureKeyCanCarryFlags(type: KeyType, vararg flags: KeyFlag) { + assureKeyCanCarryFlags(type.algorithm, *flags) + } + + @JvmStatic + fun assureKeyCanCarryFlags(algorithm: PublicKeyAlgorithm, vararg flags: KeyFlag) { + val mask = toBitmask(*flags) + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { + throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag CERTIFY_OTHER.") + } + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { + throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag SIGN_DATA.") + } + + if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { + throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag ENCRYPT_COMMS.") + } + + if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { + throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag ENCRYPT_STORAGE.") + } + + if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { + throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag AUTHENTICATION.") + } + } + } +} \ No newline at end of file From 4a19e6ca2003efc62776cbe56cb851c128268747 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 13:50:49 +0200 Subject: [PATCH 083/351] WIP: Kotlin conversion: ConsumerOptions --- .../ConsumerOptions.java | 508 ------------------ .../ConsumerOptions.kt | 394 ++++++++++++++ .../OpenPgpMessageInputStream.kt | 42 +- .../pgpainless/signature/SignatureUtils.kt | 2 + .../MissingPassphraseForDecryptionTest.java | 24 - 5 files changed, 417 insertions(+), 553 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java deleted file mode 100644 index d0d9230b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ /dev/null @@ -1,508 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; - -/** - * Options for decryption and signature verification. - */ -public class ConsumerOptions { - - private boolean ignoreMDCErrors = false; - private boolean forceNonOpenPgpData = false; - - private Date verifyNotBefore = null; - private Date verifyNotAfter = new Date(); - - private final CertificateSource certificates = new CertificateSource(); - private final Set detachedSignatures = new HashSet<>(); - private MissingPublicKeyCallback missingCertificateCallback = null; - - // Session key for decryption without passphrase/key - private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = - new HashMap<>(); - - private final Map decryptionKeys = new HashMap<>(); - private final Set decryptionPassphrases = new HashSet<>(); - private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; - - private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); - - public static ConsumerOptions get() { - return new ConsumerOptions(); - } - - /** - * Consider signatures on the message made before the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotBefore(Date timestamp) { - this.verifyNotBefore = timestamp; - return this; - } - - /** - * Return the earliest creation date on which signatures on the message are considered valid. - * Signatures made earlier than this date are considered invalid. - * - * @return earliest allowed signature creation date or null - */ - public @Nullable Date getVerifyNotBefore() { - return verifyNotBefore; - } - - /** - * Consider signatures on the message made after the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotAfter(Date timestamp) { - this.verifyNotAfter = timestamp; - return this; - } - - /** - * Return the latest possible creation date on which signatures made on the message are considered valid. - * Signatures made later than this date are considered invalid. - * - * @return Latest possible creation date or null. - */ - public Date getVerifyNotAfter() { - return verifyNotAfter; - } - - /** - * Add a certificate (public key ring) for signature verification. - * - * @param verificationCert certificate for signature verification - * @return options - */ - public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { - this.certificates.addCertificate(verificationCert); - return this; - } - - /** - * Add a set of certificates (public key rings) for signature verification. - * - * @param verificationCerts certificates for signature verification - * @return options - */ - public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) { - for (PGPPublicKeyRing certificate : verificationCerts) { - addVerificationCert(certificate); - } - return this; - } - - /** - * Add some detached signatures from the given {@link InputStream} for verification. - * - * @param signatureInputStream input stream of detached signatures - * @return options - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - */ - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) - throws IOException, PGPException { - List signatures = SignatureUtils.readSignatures(signatureInputStream); - return addVerificationOfDetachedSignatures(signatures); - } - - /** - * Add some detached signatures for verification. - * - * @param detachedSignatures detached signatures - * @return options - */ - public ConsumerOptions addVerificationOfDetachedSignatures(List detachedSignatures) { - for (PGPSignature signature : detachedSignatures) { - addVerificationOfDetachedSignature(signature); - } - return this; - } - - /** - * Add a detached signature for the signature verification process. - * - * @param detachedSignature detached signature - * @return options - */ - public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) { - detachedSignatures.add(detachedSignature); - return this; - } - - /** - * Set a callback that's used when a certificate (public key) is missing for signature verification. - * - * @param callback callback - * @return options - */ - public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) { - this.missingCertificateCallback = callback; - return this; - } - - - /** - * Attempt decryption using a session key. - * - * Note: PGPainless does not yet support decryption with session keys. - * - * @see RFC4880 on Session Keys - * - * @param sessionKey session key - * @return options - */ - public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) { - this.sessionKey = sessionKey; - return this; - } - - /** - * Return the session key. - * - * @return session key or null - */ - public @Nullable SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Add a key for message decryption. - * The key is expected to be unencrypted. - * - * @param key unencrypted key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) { - return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys()); - } - - /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} - * is used to decrypt it when needed. - * - * @param key key - * @param keyRingProtector protector for the secret key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, - @Nonnull SecretKeyRingProtector keyRingProtector) { - decryptionKeys.put(key, keyRingProtector); - return this; - } - - /** - * Add the keys in the provided key collection for message decryption. - * - * @param keys key collection - * @param keyRingProtector protector for encrypted secret keys - * @return options - */ - public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, - @Nonnull SecretKeyRingProtector keyRingProtector) { - for (PGPSecretKeyRing key : keys) { - addDecryptionKey(key, keyRingProtector); - } - return this; - } - - /** - * Add a passphrase for message decryption. - * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. - * - * @see Symmetrically Encrypted Data Packet - * - * @param passphrase passphrase - * @return options - */ - public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) { - decryptionPassphrases.add(passphrase); - return this; - } - - /** - * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using - * hardware-backed secret keys. - * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). - * - * @param factory decryptor factory - * @return options - */ - public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); - return this; - } - - /** - * Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were - * set by the user. - * These factories can be used to decrypt session keys using a custom logic. - * - * @return custom decryptor factories - */ - Map getCustomDecryptorFactories() { - return new HashMap<>(customPublicKeyDataDecryptorFactories); - } - - /** - * Return the set of available decryption keys. - * - * @return decryption keys - */ - public @Nonnull Set getDecryptionKeys() { - return Collections.unmodifiableSet(decryptionKeys.keySet()); - } - - /** - * Return the set of available message decryption passphrases. - * - * @return decryption passphrases - */ - public @Nonnull Set getDecryptionPassphrases() { - return Collections.unmodifiableSet(decryptionPassphrases); - } - - /** - * Return the explicitly set verification certificates. - * - * @deprecated use {@link #getCertificateSource()} instead. - * @return verification certs - */ - @Deprecated - public @Nonnull Set getCertificates() { - return certificates.getExplicitCertificates(); - } - - /** - * Return an object holding available certificates for signature verification. - * - * @return certificate source - */ - public @Nonnull CertificateSource getCertificateSource() { - return certificates; - } - - /** - * Return the callback that gets called when a certificate for signature verification is missing. - * This method might return

null
if the users hasn't set a callback. - * - * @return missing public key callback - */ - public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { - return missingCertificateCallback; - } - - /** - * Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}. - * - * @param decryptionKeyRing secret key - * @return protector for that particular secret key - */ - public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { - return decryptionKeys.get(decryptionKeyRing); - } - - /** - * Return the set of detached signatures the user provided. - * - * @return detached signatures - */ - public @Nonnull Set getDetachedSignatures() { - return Collections.unmodifiableSet(detachedSignatures); - } - - /** - * By default, PGPainless will require encrypted messages to make use of SEIP data packets. - * Those are Symmetrically Encrypted Integrity Protected Data packets. - * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection - * code of the SEIP packet fails. - * - * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an - * attack or data corruption. - * - * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data - * without integrity protection. - * If the flag
ignoreMDCErrors
is set to true, PGPainless will - *
    - *
  • not throw exceptions for SEIP packets with tampered ciphertext
  • - *
  • not throw exceptions for SEIP packets with tampered MDC
  • - *
  • not throw exceptions for MDCs with bad CTB
  • - *
  • not throw exceptions for MDCs with bad length
  • - *
- * - * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC - * - * @see - * Sym. Encrypted Integrity Protected Data Packet - * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. - * @return options - */ - @Deprecated - public ConsumerOptions setIgnoreMDCErrors(boolean ignoreMDCErrors) { - this.ignoreMDCErrors = ignoreMDCErrors; - return this; - } - - /** - * Return true, if PGPainless is ignoring MDC errors. - * - * @return ignore mdc errors - */ - boolean isIgnoreMDCErrors() { - return ignoreMDCErrors; - } - - /** - * Force PGPainless to handle the data provided by the {@link InputStream} as non-OpenPGP data. - * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. - * - * @return options - */ - public ConsumerOptions forceNonOpenPgpData() { - this.forceNonOpenPgpData = true; - return this; - } - - /** - * Return true, if the ciphertext should be handled as binary non-OpenPGP data. - * - * @return true if non-OpenPGP data is forced - */ - boolean isForceNonOpenPgpData() { - return forceNonOpenPgpData; - } - - /** - * Specify the {@link MissingKeyPassphraseStrategy}. - * This strategy defines, how missing passphrases for unlocking secret keys are handled. - * In interactive mode ({@link MissingKeyPassphraseStrategy#INTERACTIVE}) PGPainless will try to obtain missing - * passphrases for secret keys via the {@link SecretKeyRingProtector SecretKeyRingProtectors} - * {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider} callback. - * - * In non-interactice mode ({@link MissingKeyPassphraseStrategy#THROW_EXCEPTION}, PGPainless will instead - * throw a {@link org.pgpainless.exception.MissingPassphraseException} containing the ids of all keys for which - * there are missing passphrases. - * - * @param strategy strategy - * @return options - */ - public ConsumerOptions setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy strategy) { - this.missingKeyPassphraseStrategy = strategy; - return this; - } - - /** - * Return the currently configured {@link MissingKeyPassphraseStrategy}. - * - * @return missing key passphrase strategy - */ - MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() { - return missingKeyPassphraseStrategy; - } - - /** - * Set a custom multi-pass strategy for processing cleartext-signed messages. - * Uses {@link InMemoryMultiPassStrategy} by default. - * - * @param multiPassStrategy multi-pass caching strategy - * @return builder - */ - public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) { - this.multiPassStrategy = multiPassStrategy; - return this; - } - - /** - * Return the currently configured {@link MultiPassStrategy}. - * Defaults to {@link InMemoryMultiPassStrategy}. - * - * @return multi-pass strategy - */ - public MultiPassStrategy getMultiPassStrategy() { - return multiPassStrategy; - } - - /** - * Source for OpenPGP certificates. - * When verifying signatures on a message, this object holds available signer certificates. - */ - public static class CertificateSource { - - private Set explicitCertificates = new HashSet<>(); - - /** - * Add a certificate as verification cert explicitly. - * - * @param certificate certificate - */ - public void addCertificate(PGPPublicKeyRing certificate) { - this.explicitCertificates.add(certificate); - } - - /** - * Return the set of explicitly set verification certificates. - * @return explicitly set verification certs - */ - public Set getExplicitCertificates() { - return Collections.unmodifiableSet(explicitCertificates); - } - - /** - * Return a certificate which contains a subkey with the given keyId. - * This method first checks all explicitly set verification certs and if no cert is found it consults - * the certificate stores. - * - * @param keyId key id - * @return certificate - */ - public PGPPublicKeyRing getCertificate(long keyId) { - - for (PGPPublicKeyRing cert : explicitCertificates) { - if (cert.getPublicKey(keyId) != null) { - return cert; - } - } - - return null; - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt new file mode 100644 index 00000000..5bbe098e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -0,0 +1,394 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.util.Passphrase +import org.pgpainless.util.SessionKey +import java.io.IOException +import java.io.InputStream +import java.util.* + +/** + * Options for decryption and signature verification. + */ +class ConsumerOptions { + + private var ignoreMDCErrors = false + private var forceNonOpenPgpData = false + private var verifyNotBefore: Date? = null + private var verifyNotAfter: Date? = Date() + + private val certificates = CertificateSource() + private val detachedSignatures = mutableSetOf() + private var missingCertificateCallback: MissingPublicKeyCallback? = null + + private var sessionKey: SessionKey? = null + private val customDecryptorFactories = mutableMapOf() + private val decryptionKeys = mutableMapOf() + private val decryptionPassphrases = mutableSetOf() + private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE + private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() + + /** + * Consider signatures on the message made before the given timestamp invalid. + * Null means no limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotBefore(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotBefore = timestamp + } + + fun getVerifyNotBefore() = verifyNotBefore + + /** + * Consider signatures on the message made after the given timestamp invalid. + * Null means no limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotAfter(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotAfter = timestamp + } + + fun getVerifyNotAfter() = verifyNotAfter + + /** + * Add a certificate (public key ring) for signature verification. + * + * @param verificationCert certificate for signature verification + * @return options + */ + fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply { + this.certificates.addCertificate(verificationCert) + } + + /** + * Add a set of certificates (public key rings) for signature verification. + * + * @param verificationCerts certificates for signature verification + * @return options + */ + fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = apply { + for (cert in verificationCerts) { + addVerificationCert(cert) + } + } + + /** + * Add some detached signatures from the given [InputStream] for verification. + * + * @param signatureInputStream input stream of detached signatures + * @return options + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + @Throws(IOException::class, PGPException::class) + fun addVerificationOfDetachedSignatures(signatureInputStream: InputStream): ConsumerOptions = apply { + val signatures = SignatureUtils.readSignatures(signatureInputStream) + addVerificationOfDetachedSignatures(signatures) + } + + /** + * Add some detached signatures for verification. + * + * @param detachedSignatures detached signatures + * @return options + */ + fun addVerificationOfDetachedSignatures(detachedSignatures: List): ConsumerOptions = apply { + for (signature in detachedSignatures) { + addVerificationOfDetachedSignature(signature) + } + } + + /** + * Add a detached signature for the signature verification process. + * + * @param detachedSignature detached signature + * @return options + */ + fun addVerificationOfDetachedSignature(detachedSignature: PGPSignature): ConsumerOptions = apply { + detachedSignatures.add(detachedSignature) + } + + fun getDetachedSignatures() = detachedSignatures.toList() + + /** + * Set a callback that's used when a certificate (public key) is missing for signature verification. + * + * @param callback callback + * @return options + */ + fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply { + this.missingCertificateCallback = callback + } + + /** + * Attempt decryption using a session key. + * + * Note: PGPainless does not yet support decryption with session keys. + * + * See [RFC4880 on Session Keys](https://datatracker.ietf.org/doc/html/rfc4880#section-2.1) + * + * @param sessionKey session key + * @return options + */ + fun setSessionKey(sessionKey: SessionKey) = apply { this.sessionKey = sessionKey } + + fun getSessionKey() = sessionKey + + /** + * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] + * is used to decrypt it when needed. + * + * @param key key + * @param keyRingProtector protector for the secret key + * @return options + */ + @JvmOverloads + fun addDecryptionKey(key: PGPSecretKeyRing, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { + decryptionKeys[key] = protector + } + + /** + * Add the keys in the provided key collection for message decryption. + * + * @param keys key collection + * @param keyRingProtector protector for encrypted secret keys + * @return options + */ + @JvmOverloads + fun addDecryptionKeys(keys: PGPSecretKeyRingCollection, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { + for (key in keys) { + addDecryptionKey(key, protector) + } + } + + /** + * Add a passphrase for message decryption. + * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. + * + * See [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * + * @param passphrase passphrase + * @return options + */ + fun addDecryptionPassphrase(passphrase: Passphrase) = apply { + decryptionPassphrases.add(passphrase) + } + + /** + * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. [org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory]). + * + * @param factory decryptor factory + * @return options + */ + fun addCustomDecryptorFactory(factory: CustomPublicKeyDataDecryptorFactory) = apply { + customDecryptorFactories[factory.subkeyIdentifier] = factory + } + + /** + * Return the custom [PublicKeyDataDecryptorFactory] that were + * set by the user. + * These factories can be used to decrypt session keys using a custom logic. + * + * @return custom decryptor factories + */ + fun getCustomDecryptorFactories() = customDecryptorFactories.toMap() + + /** + * Return the set of available decryption keys. + * + * @return decryption keys + */ + fun getDecryptionKeys() = decryptionKeys.keys.toSet() + + /** + * Return the set of available message decryption passphrases. + * + * @return decryption passphrases + */ + fun getDecryptionPassphrases() = decryptionPassphrases.toSet() + + /** + * Return an object holding available certificates for signature verification. + * + * @return certificate source + */ + fun getCertificateSource() = certificates + + /** + * Return the callback that gets called when a certificate for signature verification is missing. + * This method might return `null` if the users hasn't set a callback. + * + * @return missing public key callback + */ + fun getMissingCertificateCallback() = missingCertificateCallback + + /** + * Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing]. + * + * @param decryptionKeyRing secret key + * @return protector for that particular secret key + */ + fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? { + return decryptionKeys[decryptionKeyRing] + } + + /** + * By default, PGPainless will require encrypted messages to make use of SEIP data packets. + * Those are Symmetrically Encrypted Integrity Protected Data packets. + * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. + * Furthermore, PGPainless will throw an exception if verification of the MDC error detection + * code of the SEIP packet fails. + * + * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an + * attack or data corruption. + * + * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data + * without integrity protection. + * If the flag
ignoreMDCErrors
is set to true, PGPainless will + * + * * not throw exceptions for SEIP packets with tampered ciphertext + * * not throw exceptions for SEIP packets with tampered MDC + * * not throw exceptions for MDCs with bad CTB + * * not throw exceptions for MDCs with bad length + * + * + * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC + * + * See [Sym. Encrypted Integrity Protected Data Packet](https://datatracker.ietf.org/doc/html/rfc4880.section-5.13) + * + * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. + * @return options + */ + @Deprecated("Ignoring non-integrity-protected packets is discouraged.") + fun setIgnoreMDCErrors(ignoreMDCErrors: Boolean): ConsumerOptions = apply { this.ignoreMDCErrors = ignoreMDCErrors } + + fun isIgnoreMDCErrors() = ignoreMDCErrors + + /** + * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. + * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. + * + * @return options + */ + fun forceNonOpenPgpData(): ConsumerOptions = apply { + this.forceNonOpenPgpData = true + } + + /** + * Return true, if the ciphertext should be handled as binary non-OpenPGP data. + * + * @return true if non-OpenPGP data is forced + */ + fun isForceNonOpenPgpData() = forceNonOpenPgpData + + /** + * Specify the [MissingKeyPassphraseStrategy]. + * This strategy defines, how missing passphrases for unlocking secret keys are handled. + * In interactive mode ([MissingKeyPassphraseStrategy.INTERACTIVE]) PGPainless will try to obtain missing + * passphrases for secret keys via the [SecretKeyRingProtector] + * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider] callback. + * + * In non-interactice mode ([MissingKeyPassphraseStrategy.THROW_EXCEPTION]), PGPainless will instead + * throw a [org.pgpainless.exception.MissingPassphraseException] containing the ids of all keys for which + * there are missing passphrases. + * + * @param strategy strategy + * @return options + */ + fun setMissingKeyPassphraseStrategy(strategy: MissingKeyPassphraseStrategy): ConsumerOptions { + this.missingKeyPassphraseStrategy = strategy + return this + } + + /** + * Return the currently configured [MissingKeyPassphraseStrategy]. + * + * @return missing key passphrase strategy + */ + fun getMissingKeyPassphraseStrategy(): MissingKeyPassphraseStrategy { + return missingKeyPassphraseStrategy + } + + /** + * Set a custom multi-pass strategy for processing cleartext-signed messages. + * Uses [InMemoryMultiPassStrategy] by default. + * + * @param multiPassStrategy multi-pass caching strategy + * @return builder + */ + fun setMultiPassStrategy(multiPassStrategy: MultiPassStrategy): ConsumerOptions { + this.multiPassStrategy = multiPassStrategy + return this + } + + /** + * Return the currently configured [MultiPassStrategy]. + * Defaults to [InMemoryMultiPassStrategy]. + * + * @return multi-pass strategy + */ + fun getMultiPassStrategy(): MultiPassStrategy { + return multiPassStrategy + } + + /** + * Source for OpenPGP certificates. + * When verifying signatures on a message, this object holds available signer certificates. + */ + class CertificateSource { + private val explicitCertificates: MutableSet = mutableSetOf() + + /** + * Add a certificate as verification cert explicitly. + * + * @param certificate certificate + */ + fun addCertificate(certificate: PGPPublicKeyRing) { + explicitCertificates.add(certificate) + } + + /** + * Return the set of explicitly set verification certificates. + * @return explicitly set verification certs + */ + fun getExplicitCertificates(): Set { + return explicitCertificates.toSet() + } + + /** + * Return a certificate which contains a subkey with the given keyId. + * This method first checks all explicitly set verification certs and if no cert is found it consults + * the certificate stores. + * + * @param keyId key id + * @return certificate + */ + fun getCertificate(keyId: Long): PGPPublicKeyRing? { + return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } + } + } + + companion object { + @JvmStatic + fun get() = ConsumerOptions() + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 25b29100..49a6edf5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -53,7 +53,7 @@ class OpenPgpMessageInputStream( // Add detached signatures only on the outermost OpenPgpMessageInputStream if (metadata is Message) { - signatures.addDetachedSignatures(options.detachedSignatures) + signatures.addDetachedSignatures(options.getDetachedSignatures()) } when(type) { @@ -67,7 +67,7 @@ class OpenPgpMessageInputStream( } Type.cleartext_signed -> { - val multiPassStrategy = options.multiPassStrategy + val multiPassStrategy = options.getMultiPassStrategy() val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.messageOutputStream) @@ -75,7 +75,7 @@ class OpenPgpMessageInputStream( signatures.addDetachedSignature(signature) } - options.forceNonOpenPgpData() + options.isForceNonOpenPgpData() nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) } @@ -212,7 +212,7 @@ class OpenPgpMessageInputStream( val encDataList = packetInputStream!!.readEncryptedDataList() if (!encDataList.isIntegrityProtected) { LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") - if (!options.isIgnoreMDCErrors) { + if (!options.isIgnoreMDCErrors()) { throw MessageNotIntegrityProtectedException() } } @@ -223,7 +223,7 @@ class OpenPgpMessageInputStream( " have an anonymous recipient.") // try custom decryptor factories - for ((key, decryptorFactory) in options.customDecryptorFactories) { + for ((key, decryptorFactory) in options.getCustomDecryptorFactories()) { LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.") esks.pkesks.filter { // find matching PKESK @@ -237,8 +237,8 @@ class OpenPgpMessageInputStream( } // try provided session key - if (options.sessionKey != null) { - val sk = options.sessionKey!! + if (options.getSessionKey() != null) { + val sk = options.getSessionKey()!! LOGGER.debug("Attempt decryption with provided session key.") throwIfUnacceptable(sk.algorithm) @@ -260,7 +260,7 @@ class OpenPgpMessageInputStream( } // try passwords - for (passphrase in options.decryptionPassphrases) { + for (passphrase in options.getDecryptionPassphrases()) { for (skesk in esks.skesks) { LOGGER.debug("Attempt decryption with provided passphrase") val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) @@ -295,7 +295,7 @@ class OpenPgpMessageInputStream( } LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") - val protector = options.getSecretKeyProtector(decryptionKeys) + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(keyId)) { LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") postponedDueToMissingPassphrase.add(secretKey to pkesk) @@ -317,7 +317,7 @@ class OpenPgpMessageInputStream( } LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") - val protector = options.getSecretKeyProtector(decryptionKeys) + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(secretKey.keyID)) { LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") @@ -332,7 +332,7 @@ class OpenPgpMessageInputStream( } } - if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { // Non-interactive mode: Throw an exception with all locked decryption keys postponedDueToMissingPassphrase.map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) @@ -340,7 +340,7 @@ class OpenPgpMessageInputStream( if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } - } else if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.INTERACTIVE) { + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { val keyId = secretKey.keyID val decryptionKeys = getDecryptionKey(keyId)!! @@ -532,7 +532,7 @@ class OpenPgpMessageInputStream( return MessageMetadata((metadata as Message)) } - private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.decryptionKeys.firstOrNull { + private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { it.any { k -> k.keyID == keyId }.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { @@ -543,7 +543,7 @@ class OpenPgpMessageInputStream( private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List> { val algorithm = pkesk.algorithm val candidates = mutableListOf>() - options.decryptionKeys.forEach { + options.getDecryptionKeys().forEach { val info = PGPainless.inspectKeyRing(it) for (key in info.decryptionSubkeys) { if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { @@ -679,7 +679,7 @@ class OpenPgpMessageInputStream( SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(signature) CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) LOGGER.debug("Acceptable signature by key ${verification.signingKey}") @@ -713,13 +713,13 @@ class OpenPgpMessageInputStream( } fun findCertificate(keyId: Long): PGPPublicKeyRing? { - val cert = options.certificateSource.getCertificate(keyId) + val cert = options.getCertificateSource().getCertificate(keyId) if (cert != null) { return cert } - if (options.missingCertificateCallback != null) { - return options.missingCertificateCallback!!.onMissingPublicKeyEncountered(keyId) + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(keyId) } return null // TODO: Missing cert for sig } @@ -772,7 +772,7 @@ class OpenPgpMessageInputStream( for (detached in detachedSignatures) { val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(detached.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy) @@ -787,7 +787,7 @@ class OpenPgpMessageInputStream( for (prepended in prependedSignatures) { val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(prepended.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy) @@ -877,7 +877,7 @@ class OpenPgpMessageInputStream( val openPgpIn = OpenPgpInputStream(inputStream) openPgpIn.reset() - if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData) { + if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index b9b80f8f..4ca30e86 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -14,6 +14,7 @@ import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.util.ArmorUtils +import java.io.IOException import java.io.InputStream import java.util.* @@ -127,6 +128,7 @@ class SignatureUtils { } @JvmStatic + @Throws(IOException::class, PGPException::class) fun readSignatures(inputStream: InputStream): List { return readSignatures(inputStream, MAX_ITERATIONS) } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index f7d36aba..42562713 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -59,29 +58,6 @@ public class MissingPassphraseForDecryptionTest { message = out.toByteArray(); } - @Test - public void invalidPostponedKeysStrategyTest() { - SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @Override - public Passphrase getPassphraseFor(Long keyId) { - fail("MUST NOT get called in if postponed key strategy is invalid."); - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return true; - } - }; - ConsumerOptions options = new ConsumerOptions() - .setMissingKeyPassphraseStrategy(null) // illegal - .addDecryptionKey(secretKeys, SecretKeyRingProtector.defaultSecretKeyRingProtector(callback)); - - assertThrows(IllegalStateException.class, () -> PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message)) - .withOptions(options)); - } - @Test public void interactiveStrategy() throws PGPException, IOException { // interactive callback From 9988ba994011305993f5e1094a22b4b840badb11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 13:59:34 +0200 Subject: [PATCH 084/351] Kotlin conversion: DecryptionBuilder --- .../DecryptionBuilder.java | 42 ------------------- .../DecryptionBuilderInterface.java | 36 ---------------- .../DecryptionBuilder.kt | 26 ++++++++++++ .../DecryptionBuilderInterface.kt | 34 +++++++++++++++ 4 files changed, 60 insertions(+), 78 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java deleted file mode 100644 index 96b4ad60..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Builder class that takes an {@link InputStream} of ciphertext (or plaintext signed data) - * and combines it with a configured {@link ConsumerOptions} object to form a {@link DecryptionStream} which - * can be used to decrypt an OpenPGP message or verify signatures. - */ -public class DecryptionBuilder implements DecryptionBuilderInterface { - - @Override - public DecryptWith onInputStream(@Nonnull InputStream inputStream) { - return new DecryptWithImpl(inputStream); - } - - static class DecryptWithImpl implements DecryptWith { - - private final InputStream inputStream; - - DecryptWithImpl(InputStream inputStream) { - this.inputStream = inputStream; - } - - @Override - public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException { - if (consumerOptions == null) { - throw new IllegalArgumentException("Consumer options cannot be null."); - } - - return OpenPgpMessageInputStream.create(inputStream, consumerOptions); - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java deleted file mode 100644 index 07db42f0..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -public interface DecryptionBuilderInterface { - - /** - * Create a {@link DecryptionStream} on an {@link InputStream} which contains the encrypted and/or signed data. - * - * @param inputStream encrypted and/or signed data. - * @return api handle - */ - DecryptWith onInputStream(@Nonnull InputStream inputStream); - - interface DecryptWith { - - /** - * Add options for decryption / signature verification, such as keys, passphrases etc. - * - * @param consumerOptions consumer options - * @return decryption stream - * @throws PGPException in case of an OpenPGP related error - * @throws IOException in case of an IO error - */ - DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException; - - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt new file mode 100644 index 00000000..4934f5de --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.InputStream + +/** + * Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) + * and combines it with a configured [ConsumerOptions] object to form a [DecryptionStream] which + * can be used to decrypt an OpenPGP message or verify signatures. + */ +class DecryptionBuilder: DecryptionBuilderInterface { + + override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { + return DecryptWithImpl(inputStream) + } + + class DecryptWithImpl(val inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { + + override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream { + return OpenPgpMessageInputStream.create(inputStream, consumerOptions) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt new file mode 100644 index 00000000..c15f301e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPException +import java.io.IOException +import java.io.InputStream + +interface DecryptionBuilderInterface { + + /** + * Create a [DecryptionStream] on an [InputStream] which contains the encrypted and/or signed data. + * + * @param inputStream encrypted and/or signed data. + * @return api handle + */ + fun onInputStream(inputStream: InputStream): DecryptWith + + interface DecryptWith { + + /** + * Add options for decryption / signature verification, such as keys, passphrases etc. + * + * @param consumerOptions consumer options + * @return decryption stream + * @throws PGPException in case of an OpenPGP related error + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream + } +} \ No newline at end of file From 23f8777c3461d81c0c7e0a3cc8d7d45101b6a9ff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 14:12:05 +0200 Subject: [PATCH 085/351] Kotlin conversion: DecryptionStream --- .../DecryptionStream.java | 33 -------------- .../DecryptionStream.kt | 33 ++++++++++++++ .../OpenPgpMessageInputStream.kt | 45 ++++++++++--------- 3 files changed, 56 insertions(+), 55 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java deleted file mode 100644 index 28642bbf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.InputStream; - -/** - * Abstract definition of an {@link InputStream} which can be used to decrypt / verify OpenPGP messages. - */ -public abstract class DecryptionStream extends InputStream { - - /** - * Return {@link MessageMetadata metadata} about the decrypted / verified message. - * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. - * - * @return message metadata - */ - public abstract MessageMetadata getMetadata(); - - /** - * Return a {@link OpenPgpMetadata} object containing information about the decrypted / verified message. - * The {@link DecryptionStream} MUST be closed via {@link #close()} before the metadata object can be accessed. - * - * @return message metadata - * @deprecated use {@link #getMetadata()} instead. - */ - @Deprecated - public OpenPgpMetadata getResult() { - return getMetadata().toLegacyMetadata(); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt new file mode 100644 index 00000000..2800db09 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.InputStream + +/** + * Abstract definition of an [InputStream] which can be used to decrypt / verify OpenPGP messages. + */ +abstract class DecryptionStream: InputStream() { + + /** + * Return [MessageMetadata] about the decrypted / verified message. + * The [DecryptionStream] MUST be closed via [close] before the metadata object can be accessed. + * + * @return message metadata + */ + abstract val metadata: MessageMetadata + + /** + * Return a [OpenPgpMetadata] object containing information about the decrypted / verified message. + * The [DecryptionStream] MUST be closed via [close] before the metadata object can be accessed. + * + * @return message metadata + * @deprecated use [metadata] instead. + */ + @Deprecated("Use of OpenPgpMetadata is discouraged.", + ReplaceWith("metadata")) + val result: OpenPgpMetadata + get() = metadata.toLegacyMetadata() +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 49a6edf5..a255e90e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -40,7 +40,7 @@ class OpenPgpMessageInputStream( type: Type, inputStream: InputStream, private val options: ConsumerOptions, - private val metadata: Layer, + private val layerMetadata: Layer, private val policy: Policy) : DecryptionStream() { private val signatures: Signatures = Signatures(options) @@ -52,7 +52,7 @@ class OpenPgpMessageInputStream( init { // Add detached signatures only on the outermost OpenPgpMessageInputStream - if (metadata is Message) { + if (layerMetadata is Message) { signatures.addDetachedSignatures(options.getDetachedSignatures()) } @@ -151,12 +151,12 @@ class OpenPgpMessageInputStream( } private fun processLiteralData() { - LOGGER.debug("Literal Data Packet at depth ${metadata.depth} encountered.") + LOGGER.debug("Literal Data Packet at depth ${layerMetadata.depth} encountered.") syntaxVerifier.next(InputSymbol.LITERAL_DATA) val literalData = packetInputStream!!.readLiteralData() // Extract Metadata - metadata.setChild(LiteralData( + layerMetadata.setChild(LiteralData( literalData.fileName, literalData.modificationTime, StreamEncoding.requireFromCode(literalData.format))) @@ -171,16 +171,16 @@ class OpenPgpMessageInputStream( // Extract Metadata val compressionLayer = CompressedData( CompressionAlgorithm.requireFromId(compressedData.algorithm), - metadata.depth + 1) + layerMetadata.depth + 1) - LOGGER.debug("Compressed Data Packet (${compressionLayer.algorithm}) at depth ${metadata.depth} encountered.") + LOGGER.debug("Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") nestedInputStream = OpenPgpMessageInputStream(compressedData.dataStream, options, compressionLayer, policy) } private fun processOnePassSignature() { syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) val ops = packetInputStream!!.readOnePassSignature() - LOGGER.debug("One-Pass-Signature Packet by key ${KeyIdUtil.formatKeyId(ops.keyID)} at depth ${metadata.depth} encountered.") + LOGGER.debug("One-Pass-Signature Packet by key ${KeyIdUtil.formatKeyId(ops.keyID)} at depth ${layerMetadata.depth} encountered.") signatures.addOnePassSignature(ops) } @@ -191,23 +191,23 @@ class OpenPgpMessageInputStream( val signature = try { packetInputStream!!.readSignature() } catch (e : UnsupportedPacketVersionException) { - LOGGER.debug("Unsupported Signature at depth ${metadata.depth} encountered.", e) + LOGGER.debug("Unsupported Signature at depth ${layerMetadata.depth} encountered.", e) return } val keyId = SignatureUtils.determineIssuerKeyId(signature) if (isSigForOps) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${metadata.depth} encountered.") + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${layerMetadata.depth} encountered.") signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with - signatures.addCorrespondingOnePassSignature(signature, metadata, policy) + signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) } else { - LOGGER.debug("Prepended Signature Packet by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${metadata.depth} encountered.") + LOGGER.debug("Prepended Signature Packet by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${layerMetadata.depth} encountered.") signatures.addPrependedSignature(signature) } } private fun processEncryptedData(): Boolean { - LOGGER.debug("Symmetrically Encrypted Data Packet at depth ${metadata.depth} encountered.") + LOGGER.debug("Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) val encDataList = packetInputStream!!.readEncryptedDataList() if (!encDataList.isIntegrityProtected) { @@ -244,7 +244,7 @@ class OpenPgpMessageInputStream( val decryptorFactory = ImplementationFactory.getInstance() .getSessionKeyDataDecryptorFactory(sk) - val layer = EncryptedData(sk.algorithm, metadata.depth + 1) + val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1) val skEncData = encDataList.extractSessionKeyEncryptedData() try { val decrypted = skEncData.getDataStream(decryptorFactory) @@ -392,7 +392,7 @@ class OpenPgpMessageInputStream( val decrypted = skesk.getDataStream(decryptorFactory) val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) throwIfUnacceptable(sessionKey.algorithm) - val encryptedData = EncryptedData(sessionKey.algorithm, metadata.depth + 1) + val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) encryptedData.sessionKey = sessionKey encryptedData.recipients = esks.pkesks.map { it.keyID } LOGGER.debug("Successfully decrypted data with passphrase") @@ -418,7 +418,7 @@ class OpenPgpMessageInputStream( val encryptedData = EncryptedData( SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), - metadata.depth + 1) + layerMetadata.depth + 1) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey encryptedData.recipients = esks.pkesks.plus(esks.anonPkesks).map { it.keyID } @@ -460,7 +460,7 @@ class OpenPgpMessageInputStream( throw RuntimeException(e) } } - signatures.finish(metadata, policy) + signatures.finish(layerMetadata, policy) } return r } @@ -487,7 +487,7 @@ class OpenPgpMessageInputStream( throw RuntimeException(e) } } - signatures.finish(metadata, policy) + signatures.finish(layerMetadata, policy) } return r } @@ -522,15 +522,16 @@ class OpenPgpMessageInputStream( private fun collectMetadata() { if (nestedInputStream is OpenPgpMessageInputStream) { val child = nestedInputStream as OpenPgpMessageInputStream - metadata.setChild(child.metadata as Nested) + layerMetadata.setChild(child.layerMetadata as Nested) } } - override fun getMetadata(): MessageMetadata { - check(closed) { "Stream must be closed before access to metadata can be granted." } + override val metadata: MessageMetadata + get() { + check(closed) { "Stream must be closed before access to metadata can be granted." } - return MessageMetadata((metadata as Message)) - } + return MessageMetadata((layerMetadata as Message)) + } private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { it.any { From 8c25b59c8bc7ec780fc35ed2c2f5dbbeaab4a675 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 14:36:54 +0200 Subject: [PATCH 086/351] Add missing utility methods to MessageMetadata class --- .../MessageMetadata.java | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java index 041a5437..1f7a5b03 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -21,6 +21,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.authentication.CertificateAuthenticity; import org.pgpainless.authentication.CertificateAuthority; import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.util.SessionKey; @@ -35,42 +36,6 @@ public class MessageMetadata { this.message = message; } - /** - * Convert this {@link MessageMetadata} object into a legacy {@link OpenPgpMetadata} object. - * This method is intended to be used for a transition period between the 1.3 / 1.4+ branches. - * TODO: Remove in 1.6.X - * - * @return converted {@link OpenPgpMetadata} object - */ - public @Nonnull OpenPgpMetadata toLegacyMetadata() { - OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - resultBuilder.setCompressionAlgorithm(getCompressionAlgorithm()); - resultBuilder.setModificationDate(getModificationDate()); - resultBuilder.setFileName(getFilename()); - resultBuilder.setFileEncoding(getLiteralDataEncoding()); - resultBuilder.setSessionKey(getSessionKey()); - resultBuilder.setDecryptionKey(getDecryptionKey()); - - for (SignatureVerification accepted : getVerifiedDetachedSignatures()) { - resultBuilder.addVerifiedDetachedSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedDetachedSignatures()) { - resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - - for (SignatureVerification accepted : getVerifiedInlineSignatures()) { - resultBuilder.addVerifiedInbandSignature(accepted); - } - for (SignatureVerification.Failure rejected : getRejectedInlineSignatures()) { - resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); - } - if (message.isCleartextSigned()) { - resultBuilder.setCleartextSigned(); - } - - return resultBuilder.build(); - } - public boolean isUsingCleartextSignatureFramework() { return message.isCleartextSigned(); } @@ -240,6 +205,28 @@ public class MessageMetadata { return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); } + /** + * Return true, if the message was verifiable signed by a certificate that either has the given fingerprint + * as primary key, or as the signing subkey. + * + * @param fingerprint fingerprint + * @return true if message was signed by a cert identified by the given fingerprint + */ + public boolean isVerifiedSignedBy(@Nonnull OpenPgpFingerprint fingerprint) { + List verifications = getVerifiedSignatures(); + for (SignatureVerification verification : verifications) { + if (verification.getSigningKey() == null) { + continue; + } + + if (fingerprint.equals(verification.getSigningKey().getPrimaryKeyFingerprint()) || + fingerprint.equals(verification.getSigningKey().getSubkeyFingerprint())) { + return true; + } + } + return false; + } + public List getVerifiedSignatures() { List allVerifiedSignatures = getVerifiedInlineSignatures(); allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); @@ -269,6 +256,30 @@ public class MessageMetadata { return message.getRejectedDetachedSignatures(); } + /** + * Return a list of all rejected signatures. + * + * @return rejected signatures + */ + public @Nonnull List getRejectedSignatures() { + List rejected = new ArrayList<>(); + rejected.addAll(getRejectedInlineSignatures()); + rejected.addAll(getRejectedDetachedSignatures()); + return rejected; + } + + public boolean hasRejectedSignatures() { + return !getRejectedSignatures().isEmpty(); + } + + /** + * Return true, if the message contains any (verified or rejected) signature. + * @return true if message has signature + */ + public boolean hasSignature() { + return isVerifiedSigned() || hasRejectedSignatures(); + } + public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { return containsSignatureBy(getVerifiedInlineSignatures(), keys); } From 1a701333e3c0339cf9fbf0fe7ebbfcd55e72304a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 14:38:50 +0200 Subject: [PATCH 087/351] Remove deprecated OpenPgpMetadata class --- .../OpenPgpMetadata.java | 380 ------------------ .../DecryptionStream.kt | 12 - ...artialLengthLiteralDataRegressionTest.java | 2 +- .../CanonicalizedDataEncryptionTest.java | 56 +-- .../CleartextSignatureVerificationTest.java | 20 +- .../DecryptAndVerifyMessageTest.java | 26 +- .../DecryptHiddenRecipientMessageTest.java | 5 +- .../IgnoreUnknownSignatureVersionsTest.java | 16 +- ...ntDecryptionUsingNonEncryptionKeyTest.java | 4 +- ...ificationWithoutCertIsStillSignedTest.java | 8 +- .../VerifyDetachedSignatureTest.java | 8 +- .../VerifyNotBeforeNotAfterTest.java | 44 +- .../VerifyVersion3SignaturePacketTest.java | 8 +- ...erifyWithMissingPublicKeyCallbackTest.java | 4 +- .../WrongSignerUserIdTest.java | 6 +- .../EncryptDecryptTest.java | 13 +- .../encryption_signing/SigningTest.java | 11 +- .../GenerateKeyWithoutUserIdTest.java | 8 +- .../signature/IgnoreMarkerPacketsTest.java | 10 +- 19 files changed, 123 insertions(+), 518 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java deleted file mode 100644 index e2d5f1ca..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMetadata.java +++ /dev/null @@ -1,380 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.SessionKey; - -/** - * Legacy class containing metadata about an OpenPGP message. - * It is advised to use {@link MessageMetadata} instead. - * - * TODO: Remove in 1.6.X - */ -public class OpenPgpMetadata { - - private final Set recipientKeyIds; - private final SubkeyIdentifier decryptionKey; - private final List verifiedInbandSignatures; - private final List invalidInbandSignatures; - private final List verifiedDetachedSignatures; - private final List invalidDetachedSignatures; - private final SessionKey sessionKey; - private final CompressionAlgorithm compressionAlgorithm; - private final String fileName; - private final Date modificationDate; - private final StreamEncoding fileEncoding; - private final boolean cleartextSigned; - - public OpenPgpMetadata(Set recipientKeyIds, - SubkeyIdentifier decryptionKey, - SessionKey sessionKey, - CompressionAlgorithm algorithm, - List verifiedInbandSignatures, - List invalidInbandSignatures, - List verifiedDetachedSignatures, - List invalidDetachedSignatures, - String fileName, - Date modificationDate, - StreamEncoding fileEncoding, - boolean cleartextSigned) { - - this.recipientKeyIds = Collections.unmodifiableSet(recipientKeyIds); - this.decryptionKey = decryptionKey; - this.sessionKey = sessionKey; - this.compressionAlgorithm = algorithm; - this.verifiedInbandSignatures = Collections.unmodifiableList(verifiedInbandSignatures); - this.invalidInbandSignatures = Collections.unmodifiableList(invalidInbandSignatures); - this.verifiedDetachedSignatures = Collections.unmodifiableList(verifiedDetachedSignatures); - this.invalidDetachedSignatures = Collections.unmodifiableList(invalidDetachedSignatures); - this.fileName = fileName; - this.modificationDate = modificationDate; - this.fileEncoding = fileEncoding; - this.cleartextSigned = cleartextSigned; - } - - /** - * Return a set of key-ids the messages was encrypted for. - * - * @return recipient ids - */ - public @Nonnull Set getRecipientKeyIds() { - return recipientKeyIds; - } - - /** - * Return true, if the message was encrypted. - * - * @return true if encrypted, false otherwise - */ - public boolean isEncrypted() { - return sessionKey != null && sessionKey.getAlgorithm() != SymmetricKeyAlgorithm.NULL; - } - - /** - * Return the {@link SubkeyIdentifier} of the key that was used to decrypt the message. - * This can be null if the message was decrypted using a {@link org.pgpainless.util.Passphrase}, or if it was not - * encrypted at all (e.g. signed only). - * - * @return subkey identifier of decryption key - */ - public @Nullable SubkeyIdentifier getDecryptionKey() { - return decryptionKey; - } - - /** - * Return the algorithm that was used to symmetrically encrypt the message. - * - * @return encryption algorithm - */ - public @Nullable SymmetricKeyAlgorithm getSymmetricKeyAlgorithm() { - return sessionKey == null ? null : sessionKey.getAlgorithm(); - } - - public @Nullable SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Return the {@link CompressionAlgorithm} that was used to compress the message. - * - * @return compression algorithm - */ - public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - return compressionAlgorithm; - } - - /** - * Return a set of all signatures on the message. - * Note: This method returns just the signatures. There is no guarantee that the signatures are verified or even correct. - * - * Use {@link #getVerifiedSignatures()} instead to get all verified signatures. - * @return unverified and verified signatures - */ - public @Nonnull Set getSignatures() { - Set signatures = new HashSet<>(); - for (SignatureVerification v : getVerifiedDetachedSignatures()) { - signatures.add(v.getSignature()); - } - for (SignatureVerification v : getVerifiedInbandSignatures()) { - signatures.add(v.getSignature()); - } - for (SignatureVerification.Failure f : getInvalidDetachedSignatures()) { - signatures.add(f.getSignatureVerification().getSignature()); - } - for (SignatureVerification.Failure f : getInvalidInbandSignatures()) { - signatures.add(f.getSignatureVerification().getSignature()); - } - return signatures; - } - - /** - * Return true if the message contained at least one signature. - * - * Note: This method does not reflect, whether the signature on the message is correct. - * Use {@link #isVerified()} instead to determine, if the message carries a verifiable signature. - * - * @return true if message contains at least one unverified or verified signature, false otherwise. - */ - public boolean isSigned() { - return !getSignatures().isEmpty(); - } - - /** - * Return a map of all verified signatures on the message. - * The map contains verified signatures as value, with the {@link SubkeyIdentifier} of the key that was used to verify - * the signature as the maps keys. - * - * @return verified detached and one-pass signatures - */ - public Map getVerifiedSignatures() { - Map verifiedSignatures = new ConcurrentHashMap<>(); - for (SignatureVerification detachedSignature : getVerifiedDetachedSignatures()) { - verifiedSignatures.put(detachedSignature.getSigningKey(), detachedSignature.getSignature()); - } - for (SignatureVerification inbandSignatures : verifiedInbandSignatures) { - verifiedSignatures.put(inbandSignatures.getSigningKey(), inbandSignatures.getSignature()); - } - - return verifiedSignatures; - } - - public List getVerifiedInbandSignatures() { - return verifiedInbandSignatures; - } - - public List getVerifiedDetachedSignatures() { - return verifiedDetachedSignatures; - } - - public List getInvalidInbandSignatures() { - return invalidInbandSignatures; - } - - public List getInvalidDetachedSignatures() { - return invalidDetachedSignatures; - } - - /** - * Return true, if the message is signed and at least one signature on the message was verified successfully. - * - * @return true if message is verified, false otherwise - */ - public boolean isVerified() { - return !getVerifiedSignatures().isEmpty(); - } - - /** - * Return true, if the message contains at least one verified signature made by a key in the - * given certificate. - * - * @param certificate certificate - * @return true if message was signed by the certificate (and the signature is valid), false otherwise - */ - public boolean containsVerifiedSignatureFrom(PGPPublicKeyRing certificate) { - for (PGPPublicKey key : certificate) { - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(key); - if (containsVerifiedSignatureFrom(fingerprint)) { - return true; - } - } - return false; - } - - /** - * Return true, if the message contains at least one valid signature made by the key with the given - * fingerprint, false otherwise. - * - * The fingerprint might be of the signing subkey, or the primary key of the signing certificate. - * - * @param fingerprint fingerprint of primary key or signing subkey - * @return true if validly signed, false otherwise - */ - public boolean containsVerifiedSignatureFrom(OpenPgpFingerprint fingerprint) { - for (SubkeyIdentifier verifiedSigningKey : getVerifiedSignatures().keySet()) { - if (verifiedSigningKey.getPrimaryKeyFingerprint().equals(fingerprint) || - verifiedSigningKey.getSubkeyFingerprint().equals(fingerprint)) { - return true; - } - } - return false; - } - - /** - * Return the name of the encrypted / signed file. - * - * @return file name - */ - public String getFileName() { - return fileName; - } - - /** - * Return true, if the encrypted data is intended for your eyes only. - * - * @return true if for-your-eyes-only - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFileName()); - } - - /** - * Return the modification date of the encrypted / signed file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Return the encoding format of the encrypted / signed file. - * - * @return encoding - */ - public StreamEncoding getFileEncoding() { - return fileEncoding; - } - - /** - * Return true if the message was signed using the cleartext signature framework. - * - * @return true if cleartext signed. - */ - public boolean isCleartextSigned() { - return cleartextSigned; - } - - public static Builder getBuilder() { - return new Builder(); - } - - public static class Builder { - - private final Set recipientFingerprints = new HashSet<>(); - private SessionKey sessionKey; - private SubkeyIdentifier decryptionKey; - private CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.UNCOMPRESSED; - private String fileName; - private StreamEncoding fileEncoding; - private Date modificationDate; - private boolean cleartextSigned = false; - - private final List verifiedInbandSignatures = new ArrayList<>(); - private final List verifiedDetachedSignatures = new ArrayList<>(); - private final List invalidInbandSignatures = new ArrayList<>(); - private final List invalidDetachedSignatures = new ArrayList<>(); - - - public Builder addRecipientKeyId(Long keyId) { - this.recipientFingerprints.add(keyId); - return this; - } - - public Builder setDecryptionKey(SubkeyIdentifier decryptionKey) { - this.decryptionKey = decryptionKey; - return this; - } - - public Builder setSessionKey(SessionKey sessionKey) { - this.sessionKey = sessionKey; - return this; - } - - public Builder setCompressionAlgorithm(CompressionAlgorithm algorithm) { - this.compressionAlgorithm = algorithm; - return this; - } - - public Builder setFileName(@Nullable String fileName) { - this.fileName = fileName; - return this; - } - - public Builder setModificationDate(Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - public Builder setFileEncoding(StreamEncoding encoding) { - this.fileEncoding = encoding; - return this; - } - - public Builder addVerifiedInbandSignature(SignatureVerification signatureVerification) { - this.verifiedInbandSignatures.add(signatureVerification); - return this; - } - - public Builder addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - this.verifiedDetachedSignatures.add(signatureVerification); - return this; - } - - public Builder addInvalidInbandSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidInbandSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - return this; - } - - public Builder addInvalidDetachedSignature(SignatureVerification signatureVerification, SignatureValidationException e) { - this.invalidDetachedSignatures.add(new SignatureVerification.Failure(signatureVerification, e)); - return this; - } - - public Builder setCleartextSigned() { - this.cleartextSigned = true; - return this; - } - - public OpenPgpMetadata build() { - return new OpenPgpMetadata( - recipientFingerprints, decryptionKey, - sessionKey, compressionAlgorithm, - verifiedInbandSignatures, invalidInbandSignatures, - verifiedDetachedSignatures, invalidDetachedSignatures, - fileName, modificationDate, fileEncoding, cleartextSigned); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt index 2800db09..b9499784 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt @@ -18,16 +18,4 @@ abstract class DecryptionStream: InputStream() { * @return message metadata */ abstract val metadata: MessageMetadata - - /** - * Return a [OpenPgpMetadata] object containing information about the decrypted / verified message. - * The [DecryptionStream] MUST be closed via [close] before the metadata object can be accessed. - * - * @return message metadata - * @deprecated use [metadata] instead. - */ - @Deprecated("Use of OpenPgpMetadata is discouraged.", - ReplaceWith("metadata")) - val result: OpenPgpMetadata - get() = metadata.toLegacyMetadata() } \ No newline at end of file diff --git a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java index a9b20e9e..7ec53edb 100644 --- a/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java +++ b/pgpainless-core/src/test/java/investigations/OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionTest.java @@ -130,6 +130,6 @@ public class OnePassSignatureVerificationWithPartialLengthLiteralDataRegressionT Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - decryptionStream.getResult(); + decryptionStream.getMetadata(); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index c427af99..36e473ac 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -122,9 +122,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -136,9 +136,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -150,9 +150,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -164,9 +164,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -178,9 +178,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -192,9 +192,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -207,9 +207,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -221,9 +221,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -235,9 +235,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -249,9 +249,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingTextDataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -263,9 +263,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -277,9 +277,9 @@ public class CanonicalizedDataEncryptionTest { @Test public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); - OpenPgpMetadata metadata = decryptAndVerify(msg); + MessageMetadata metadata = decryptAndVerify(msg); - if (!metadata.isVerified()) { + if (!metadata.isVerifiedSigned()) { // CHECKSTYLE:OFF System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); System.out.println(msg); @@ -360,7 +360,7 @@ public class CanonicalizedDataEncryptionTest { return msg; } - private OpenPgpMetadata decryptAndVerify(String msg) throws PGPException, IOException { + private MessageMetadata decryptAndVerify(String msg) throws PGPException, IOException { ByteArrayInputStream in = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(in) @@ -371,7 +371,7 @@ public class CanonicalizedDataEncryptionTest { Streams.drain(decryptionStream); decryptionStream.close(); - return decryptionStream.getResult(); + return decryptionStream.getMetadata(); } @Test @@ -439,8 +439,8 @@ public class CanonicalizedDataEncryptionTest { Streams.pipeAll(decryptionStream, decrypted); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.isVerified(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned(), "Not verified! Sig Type: " + sigType + " StreamEncoding: " + streamEncoding); assertArrayEquals(msg, decrypted.toByteArray()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java index cabfdbb1..720a0d53 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CleartextSignatureVerificationTest.java @@ -96,11 +96,11 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isVerified()); - assertTrue(result.isCleartextSigned()); + MessageMetadata result = decryptionStream.getMetadata(); + assertTrue(result.isVerifiedSigned()); + assertTrue(result.isUsingCleartextSignatureFramework()); - PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); + PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); assertArrayEquals(MESSAGE_BODY, out.toByteArray()); @@ -125,10 +125,10 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata result = decryptionStream.getResult(); - assertTrue(result.isVerified()); + MessageMetadata result = decryptionStream.getMetadata(); + assertTrue(result.isVerifiedSigned()); - PGPSignature signature = result.getVerifiedSignatures().values().iterator().next(); + PGPSignature signature = result.getVerifiedSignatures().iterator().next().getSignature(); assertEquals(signature.getKeyID(), signingKeys.getPublicKey().getKeyID()); FileInputStream fileIn = new FileInputStream(file); @@ -178,7 +178,7 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertEquals(1, metadata.getVerifiedSignatures().size()); } @@ -210,8 +210,8 @@ public class CleartextSignatureVerificationTest { Streams.pipeAll(verificationStream, msgOut); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index e939de0a..ad61e132 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -64,7 +64,7 @@ public class DecryptAndVerifyMessageTest { Streams.pipeAll(decryptor, toPlain); decryptor.close(); toPlain.close(); - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); byte[] actual = toPlain.toByteArray(); @@ -72,14 +72,13 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertFalse(metadata.isCleartextSigned()); - assertTrue(metadata.isVerified()); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.isVerifiedSigned()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); - assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); - assertEquals(1, metadata.getSignatures().size()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertEquals(1, metadata.getVerifiedSignatures().size()); - assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } @@ -104,7 +103,7 @@ public class DecryptAndVerifyMessageTest { decryptor.close(); toPlain.close(); - OpenPgpMetadata metadata = decryptor.getResult(); + MessageMetadata metadata = decryptor.getMetadata(); byte[] expected = TestKeys.TEST_MESSAGE_01_PLAIN.getBytes(UTF8); byte[] actual = toPlain.toByteArray(); @@ -112,14 +111,13 @@ public class DecryptAndVerifyMessageTest { assertArrayEquals(expected, actual); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertFalse(metadata.isCleartextSigned()); - assertTrue(metadata.isVerified()); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.isVerifiedSigned()); assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); - assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getSymmetricKeyAlgorithm()); - assertEquals(1, metadata.getSignatures().size()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); assertEquals(1, metadata.getVerifiedSignatures().size()); - assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.JULIET_FINGERPRINT)); + assertEquals(1, metadata.getVerifiedSignatures().size()); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.JULIET_FINGERPRINT)); assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java index 5400d17c..4eb7b203 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessageTest.java @@ -139,8 +139,9 @@ public class DecryptHiddenRecipientMessageTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(0, metadata.getRecipientKeyIds().size()); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertEquals(1, metadata.getRecipientKeyIds().size()); + assertEquals(0L, metadata.getRecipientKeyIds().get(0)); KeyRingInfo info = new KeyRingInfo(secretKeys); List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java index aa1da741..2b222c83 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/IgnoreUnknownSignatureVersionsTest.java @@ -104,9 +104,9 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=uHRc\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, BASE_CASE); + MessageMetadata metadata = verifySignature(cert, BASE_CASE); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -137,9 +137,9 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=/JL1\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, SIG4SIG23); + MessageMetadata metadata = verifySignature(cert, SIG4SIG23); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -170,12 +170,12 @@ public class IgnoreUnknownSignatureVersionsTest { "ou1uiXJaDzZ6wQfB\n" + "=Yc8d\n" + "-----END PGP SIGNATURE-----\n"; - OpenPgpMetadata metadata = verifySignature(cert, SIG23SIG4); + MessageMetadata metadata = verifySignature(cert, SIG23SIG4); - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } - private OpenPgpMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { + private MessageMetadata verifySignature(PGPPublicKeyRing cert, String BASE_CASE) throws PGPException, IOException { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions() .addVerificationCert(cert) @@ -184,6 +184,6 @@ public class IgnoreUnknownSignatureVersionsTest { Streams.drain(decryptionStream); decryptionStream.close(); - return decryptionStream.getResult(); + return decryptionStream.getMetadata(); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 04a98265..f06f0233 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -184,7 +184,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @@ -200,7 +200,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { Streams.drain(decryptionStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java index 27bc9954..9f85b241 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/SignedMessageVerificationWithoutCertIsStillSignedTest.java @@ -39,10 +39,10 @@ public class SignedMessageVerificationWithoutCertIsStillSignedTest { Streams.pipeAll(verificationStream, out); verificationStream.close(); - OpenPgpMetadata metadata = verificationStream.getResult(); + MessageMetadata metadata = verificationStream.getMetadata(); - assertFalse(metadata.isCleartextSigned()); - assertTrue(metadata.isSigned(), "Message is signed, even though we miss the verification cert."); - assertFalse(metadata.isVerified(), "Message is not verified because we lack the verification cert."); + assertFalse(metadata.isUsingCleartextSignatureFramework()); + assertTrue(metadata.hasRejectedSignatures(), "Message is signed, even though we miss the verification cert."); + assertFalse(metadata.isVerifiedSigned(), "Message is not verified because we lack the verification cert."); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java index fa1427d3..e1406f87 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyDetachedSignatureTest.java @@ -65,8 +65,8 @@ public class VerifyDetachedSignatureTest { Streams.drain(verifier); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verifier.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } @Test @@ -140,7 +140,7 @@ public class VerifyDetachedSignatureTest { Streams.drain(verifier); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); - assertTrue(metadata.isVerified()); + MessageMetadata metadata = verifier.getMetadata(); + assertTrue(metadata.isVerifiedSigned()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java index d67b3d95..069a5f2d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyNotBeforeNotAfterTest.java @@ -68,8 +68,8 @@ public class VerifyNotBeforeNotAfterTest { .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(new SubkeyIdentifier(certificate))); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -81,8 +81,8 @@ public class VerifyNotBeforeNotAfterTest { .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -93,8 +93,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -106,8 +106,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -118,8 +118,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedInlineSignedBy(certificate)); } @Test @@ -131,8 +131,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -143,8 +143,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -156,8 +156,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -168,8 +168,8 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(inlineSigned)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } @Test @@ -181,13 +181,13 @@ public class VerifyNotBeforeNotAfterTest { DecryptionStream verifier = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(data)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertFalse(metadata.getVerifiedSignatures().containsKey(signingKey)); + MessageMetadata metadata = processSignedData(verifier); + assertFalse(metadata.isVerifiedSignedBy(certificate)); } - private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { + private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { Streams.drain(verifier); verifier.close(); - return verifier.getResult(); + return verifier.getMetadata(); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java index 2a12e74a..6b9d9cab 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyVersion3SignaturePacketTest.java @@ -43,8 +43,8 @@ class VerifyVersion3SignaturePacketTest { .onInputStream(new ByteArrayInputStream(DATA)) .withOptions(options); - OpenPgpMetadata metadata = processSignedData(verifier); - assertTrue(metadata.containsVerifiedSignatureFrom(TestKeys.getEmilPublicKeyRing())); + MessageMetadata metadata = processSignedData(verifier); + assertTrue(metadata.isVerifiedSignedBy(TestKeys.getEmilPublicKeyRing())); } private static PGPSignature generateV3Signature() throws IOException, PGPException { @@ -61,9 +61,9 @@ class VerifyVersion3SignaturePacketTest { return signatureGenerator.generate(); } - private OpenPgpMetadata processSignedData(DecryptionStream verifier) throws IOException { + private MessageMetadata processSignedData(DecryptionStream verifier) throws IOException { Streams.drain(verifier); verifier.close(); - return verifier.getResult(); + return verifier.getMetadata(); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index 2821ca89..52877626 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -74,7 +74,7 @@ public class VerifyWithMissingPublicKeyCallbackTest { verificationStream.close(); assertArrayEquals(msg.getBytes(StandardCharsets.UTF_8), plainOut.toByteArray()); - OpenPgpMetadata metadata = verificationStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(signingPubKeys)); + MessageMetadata metadata = verificationStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(signingPubKeys)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java index b3ecabc8..f3336373 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/WrongSignerUserIdTest.java @@ -102,12 +102,12 @@ public class WrongSignerUserIdTest { Streams.pipeAll(decryptionStream, out); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); if (expectSuccessfulVerification) { - assertTrue(metadata.isVerified()); + assertTrue(metadata.isVerifiedSigned()); } else { - assertFalse(metadata.isVerified()); + assertFalse(metadata.isVerifiedSigned()); } } 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 216d0c65..3e620386 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 @@ -33,7 +33,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; @@ -185,11 +185,10 @@ public class EncryptDecryptTest { decryptor.close(); assertArrayEquals(secretMessage, decryptedSecretMessage.toByteArray()); - OpenPgpMetadata result = decryptor.getResult(); - assertTrue(result.containsVerifiedSignatureFrom(senderPub)); - assertTrue(result.isSigned()); + MessageMetadata result = decryptor.getMetadata(); + assertTrue(result.isVerifiedSignedBy(senderPub)); assertTrue(result.isEncrypted()); - assertTrue(result.isVerified()); + assertTrue(result.isVerifiedSigned()); } @TestTemplate @@ -233,7 +232,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, dummyOut); verifier.close(); - OpenPgpMetadata decryptionResult = verifier.getResult(); + MessageMetadata decryptionResult = verifier.getMetadata(); assertFalse(decryptionResult.getVerifiedSignatures().isEmpty()); } @@ -263,7 +262,7 @@ public class EncryptDecryptTest { Streams.pipeAll(verifier, signOut); verifier.close(); - OpenPgpMetadata metadata = verifier.getResult(); + MessageMetadata metadata = verifier.getMetadata(); assertFalse(metadata.getVerifiedSignatures().isEmpty()); } 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 156e6b57..bfd330aa 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 @@ -34,7 +34,7 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; @@ -106,12 +106,11 @@ public class SigningTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); assertTrue(metadata.isEncrypted()); - assertTrue(metadata.isSigned()); - assertTrue(metadata.isVerified()); - assertTrue(metadata.containsVerifiedSignatureFrom(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); - assertFalse(metadata.containsVerifiedSignatureFrom(julietKeys)); + assertTrue(metadata.isVerifiedSigned()); + assertTrue(metadata.isVerifiedSignedBy(KeyRingUtils.publicKeyRingFrom(cryptieKeys))); + assertFalse(metadata.isVerifiedSignedBy(julietKeys)); } @TestTemplate diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java index 24484cd0..e7884f3f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -14,7 +14,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionResult; @@ -88,10 +88,10 @@ public class GenerateKeyWithoutUserIdTest { Streams.pipeAll(decryptionStream, plaintextOut); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); + MessageMetadata metadata = decryptionStream.getMetadata(); - assertTrue(metadata.containsVerifiedSignatureFrom(certificate), - failuresToString(metadata.getInvalidInbandSignatures())); + assertTrue(metadata.isVerifiedSignedBy(certificate), + failuresToString(metadata.getRejectedInlineSignatures())); assertTrue(metadata.isEncrypted()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java index 6ece093b..ff26506d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/IgnoreMarkerPacketsTest.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.decryption_verification.MessageMetadata; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.util.KeyRingUtils; @@ -154,8 +154,8 @@ public class IgnoreMarkerPacketsTest { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); } @Test @@ -204,8 +204,8 @@ public class IgnoreMarkerPacketsTest { decryptionStream.close(); assertArrayEquals(data.getBytes(StandardCharsets.UTF_8), outputStream.toByteArray()); - OpenPgpMetadata metadata = decryptionStream.getResult(); - assertTrue(metadata.containsVerifiedSignatureFrom(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); + MessageMetadata metadata = decryptionStream.getMetadata(); + assertTrue(metadata.isVerifiedSignedBy(new OpenPgpV4Fingerprint("D1A66E1A23B182C9980F788CFBFCC82A015E7330"))); } @Test From 145555997c8e50bb98b063beff60a5b086a1e28e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 14:43:33 +0200 Subject: [PATCH 088/351] Kotlin conversion: SignatureCheck --- .../signature/consumer/SignatureCheck.java | 73 ------------------- .../signature/consumer/SignatureCheck.kt | 23 ++++++ 2 files changed, 23 insertions(+), 73 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java deleted file mode 100644 index bc9f1f0b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCheck.java +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple-class which bundles together a signature, the signing key that created the signature, - * an identifier of the signing key and a record of whether the signature was verified. - */ -public class SignatureCheck { - private final PGPSignature signature; - private final PGPKeyRing signingKeyRing; - private final SubkeyIdentifier signingKeyIdentifier; - - /** - * Create a new {@link SignatureCheck} object. - * - * @param signature signature - * @param signingKeyRing signing key that created the signature - * @param signingKeyIdentifier identifier of the used signing key - */ - public SignatureCheck(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) { - this.signature = signature; - this.signingKeyRing = signingKeyRing; - this.signingKeyIdentifier = signingKeyIdentifier; - } - - /** - * Return the OpenPGP signature. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return an identifier pointing to the exact signing key which was used to create this signature. - * - * @return signing key identifier - */ - public SubkeyIdentifier getSigningKeyIdentifier() { - return signingKeyIdentifier; - } - - /** - * Return the key ring that contains the signing key that created this signature. - * - * @return key ring - */ - public PGPKeyRing getSigningKeyRing() { - return signingKeyRing; - } - - /** - * Return the {@link OpenPgpFingerprint} of the key that created the signature. - * - * @return fingerprint of the signing key - * @deprecated use {@link #getSigningKeyIdentifier()} instead. - * - * TODO: Remove in 1.2.X - */ - @Deprecated - public OpenPgpFingerprint getFingerprint() { - return signingKeyIdentifier.getSubkeyFingerprint(); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt new file mode 100644 index 00000000..48a3aa96 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.SubkeyIdentifier + +/** + * Tuple-class which bundles together a signature, the signing key that created the signature, + * an identifier of the signing key and a record of whether the signature was verified. + * + * @param signature OpenPGP signature + * @param signingKeyIdentifier identifier pointing to the exact signing key which was used to create the signature + * @param signingKeyRing certificate or key ring that contains the signing key that created the signature + */ +data class SignatureCheck( + val signature: PGPSignature, + val signingKeyRing: PGPKeyRing, + val signingKeyIdentifier: SubkeyIdentifier) { +} \ No newline at end of file From 8d67820f50435d179c83467906170604a56de503 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 15:14:21 +0200 Subject: [PATCH 089/351] Kotlin conversion: OnePassSignatureCheck --- .../consumer/OnePassSignatureCheck.java | 72 ------------------- .../consumer/OnePassSignatureCheck.kt | 33 +++++++++ 2 files changed, 33 insertions(+), 72 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java deleted file mode 100644 index 7a6a5b10..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/OnePassSignatureCheck.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple-class that bundles together a {@link PGPOnePassSignature} object, a {@link PGPPublicKeyRing} - * destined to verify the signature, the {@link PGPSignature} itself and a record of whether the signature - * was verified. - */ -public class OnePassSignatureCheck { - private final PGPOnePassSignature onePassSignature; - private final PGPPublicKeyRing verificationKeys; - private PGPSignature signature; - - /** - * Create a new {@link OnePassSignatureCheck}. - * - * @param onePassSignature one-pass signature packet used to initialize the signature verifier. - * @param verificationKeys verification keys - */ - public OnePassSignatureCheck(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) { - this.onePassSignature = onePassSignature; - this.verificationKeys = verificationKeys; - } - - public void setSignature(PGPSignature signature) { - this.signature = signature; - } - - /** - * Return the {@link PGPOnePassSignature} object. - * - * @return onePassSignature - */ - public PGPOnePassSignature getOnePassSignature() { - return onePassSignature; - } - - /** - * Return an identifier for the signing key. - * - * @return signing key fingerprint - */ - public SubkeyIdentifier getSigningKey() { - return new SubkeyIdentifier(verificationKeys, onePassSignature.getKeyID()); - } - - /** - * Return the signature. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return the key ring used to verify the signature. - * - * @return verification keys - */ - public PGPPublicKeyRing getVerificationKeys() { - return verificationKeys; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt new file mode 100644 index 00000000..a315f577 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.SubkeyIdentifier + +/** + * Tuple-class that bundles together a [PGPOnePassSignature] object, a [PGPPublicKeyRing] + * destined to verify the signature, the [PGPSignature] itself and a record of whether the signature + * was verified. + * + * @param onePassSignature the one-pass-signature packet + * @param verificationKeys certificate containing the signing subkey + * @param signature the signature packet + */ +data class OnePassSignatureCheck( + val onePassSignature: PGPOnePassSignature, + val verificationKeys: PGPPublicKeyRing, + var signature: PGPSignature? = null) { + + /** + * Return an identifier for the signing key. + * + * @return signing key fingerprint + */ + val signingKey: SubkeyIdentifier + get() = SubkeyIdentifier(verificationKeys, onePassSignature.keyID) +} \ No newline at end of file From 48af91efbf060a53ee32d26c5b0af7f884027448 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 15:59:25 +0200 Subject: [PATCH 090/351] Kotlin conversion: Cleartext Signature Framework --- .../ClearsignedMessageUtil.java | 169 ------------------ .../InMemoryMultiPassStrategy.java | 35 ---- .../MultiPassStrategy.java | 70 -------- .../WriteToFileMultiPassStrategy.java | 54 ------ .../cleartext_signatures/package-info.java | 8 - .../ClearsignedMessageUtil.kt | 153 ++++++++++++++++ .../InMemoryMultiPassStrategy.kt | 29 +++ .../cleartext_signatures/MultiPassStrategy.kt | 71 ++++++++ .../WriteToFileMultiPassStrategy.kt | 43 +++++ .../org/pgpainless/sop/InlineDetachImpl.java | 2 +- 10 files changed, 297 insertions(+), 337 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java deleted file mode 100644 index cd0f6b35..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.java +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.Strings; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmoredInputStreamFactory; - -/** - * Utility class to deal with cleartext-signed messages. - * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}. - */ -public final class ClearsignedMessageUtil { - - private ClearsignedMessageUtil() { - - } - - /** - * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided - * messageOutputStream. - * - * @param clearsignedInputStream input stream containing a clearsigned message - * @param messageOutputStream output stream to which the dearmored message shall be written - * @return signatures - * - * @throws IOException if the message is not clearsigned or some other IO error happens - * @throws WrongConsumingMethodException in case the armored message is not cleartext signed - */ - public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream, - OutputStream messageOutputStream) - throws IOException, WrongConsumingMethodException { - ArmoredInputStream in; - if (clearsignedInputStream instanceof ArmoredInputStream) { - in = (ArmoredInputStream) clearsignedInputStream; - } else { - in = ArmoredInputStreamFactory.get(clearsignedInputStream); - } - - if (!in.isClearText()) { - throw new WrongConsumingMethodException("Message is not using the Cleartext Signature Framework."); - } - - OutputStream out = new BufferedOutputStream(messageOutputStream); - try { - ByteArrayOutputStream lineOut = new ByteArrayOutputStream(); - int lookAhead = readInputLine(lineOut, in); - byte[] lineSep = getLineSeparator(); - - if (lookAhead != -1 && in.isClearText()) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - - while (lookAhead != -1 && in.isClearText()) { - lookAhead = readInputLine(lineOut, lookAhead, in); - line = lineOut.toByteArray(); - out.write(lineSep); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - } - } else { - if (lookAhead != -1) { - byte[] line = lineOut.toByteArray(); - out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)); - } - } - } finally { - out.close(); - } - - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(in); - PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject(); - - return signatures; - } - - public static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn) - throws IOException { - bOut.reset(); - - int lookAhead = -1; - int ch; - - while ((ch = fIn.read()) >= 0) { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - - return lookAhead; - } - - public static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn) - throws IOException { - bOut.reset(); - - int ch = lookAhead; - - do { - bOut.write(ch); - if (ch == '\r' || ch == '\n') { - lookAhead = readPassedEOL(bOut, ch, fIn); - break; - } - } - while ((ch = fIn.read()) >= 0); - - if (ch < 0) { - lookAhead = -1; - } - - return lookAhead; - } - - private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn) - throws IOException { - int lookAhead = fIn.read(); - - if (lastCh == '\r' && lookAhead == '\n') { - bOut.write(lookAhead); - lookAhead = fIn.read(); - } - - return lookAhead; - } - - - private static byte[] getLineSeparator() { - String nl = Strings.lineSeparator(); - byte[] nlBytes = new byte[nl.length()]; - - for (int i = 0; i != nlBytes.length; i++) { - nlBytes[i] = (byte) nl.charAt(i); - } - - return nlBytes; - } - - private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) { - int end = line.length - 1; - - while (end >= 0 && isWhiteSpace(line[end])) { - end--; - } - - return end + 1; - } - - private static boolean isLineEnding(byte b) { - return b == '\r' || b == '\n'; - } - - private static boolean isWhiteSpace(byte b) { - return isLineEnding(b) || b == '\t' || b == ' '; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java deleted file mode 100644 index 62433a2a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.java +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; - -/** - * Implementation of the {@link MultiPassStrategy}. - * This class keeps the read data in memory by caching the data inside a {@link ByteArrayOutputStream}. - * - * Note, that this class is suitable and efficient for processing small amounts of data. - * For larger data like encrypted files, use of the {@link WriteToFileMultiPassStrategy} is recommended to - * prevent {@link OutOfMemoryError OutOfMemoryErrors} and other issues. - */ -public class InMemoryMultiPassStrategy implements MultiPassStrategy { - - private final ByteArrayOutputStream cache = new ByteArrayOutputStream(); - - @Override - public ByteArrayOutputStream getMessageOutputStream() { - return cache; - } - - @Override - public ByteArrayInputStream getMessageInputStream() { - return new ByteArrayInputStream(getBytes()); - } - - public byte[] getBytes() { - return getMessageOutputStream().toByteArray(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java deleted file mode 100644 index 5aa9f548..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures, - * a strategy for how to cache the read data is required. - * Otherwise, large data kept in memory could cause {@link OutOfMemoryError OutOfMemoryErrors} or other issues. - * - * This is an Interface that describes a strategy to deal with the fact that detached signatures require multiple passes - * to do verification. - * - * This interface can be used to write the signed data stream out via {@link #getMessageOutputStream()} and later - * get access to the data again via {@link #getMessageInputStream()}. - * Thereby the detail where the data is being stored (memory, file, etc.) can be abstracted away. - */ -public interface MultiPassStrategy { - - /** - * Provide an {@link OutputStream} into which the signed data can be read into. - * - * @return output stream - * @throws IOException io error - */ - OutputStream getMessageOutputStream() throws IOException; - - /** - * Provide an {@link InputStream} which contains the data that was previously written away in - * {@link #getMessageOutputStream()}. - * - * As there may be multiple signatures that need to be processed, each call of this method MUST return - * a new {@link InputStream}. - * - * @return input stream - * @throws IOException io error - */ - InputStream getMessageInputStream() throws IOException; - - /** - * Write the message content out to a file and re-read it to verify signatures. - * This strategy is best suited for larger messages (e.g. plaintext signed files) which might not fit into memory. - * After the message has been processed completely, the messages content are available at the provided file. - * - * @param file target file - * @return strategy - */ - static MultiPassStrategy writeMessageToFile(File file) { - return new WriteToFileMultiPassStrategy(file); - } - - /** - * Read the message content into memory. - * This strategy is best suited for small messages which fit into memory. - * After the message has been processed completely, the message content can be accessed by calling - * {@link ByteArrayOutputStream#toByteArray()} on {@link #getMessageOutputStream()}. - * - * @return strategy - */ - static InMemoryMultiPassStrategy keepMessageInMemory() { - return new InMemoryMultiPassStrategy(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java deleted file mode 100644 index 6c8d03ca..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification.cleartext_signatures; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Implementation of the {@link MultiPassStrategy}. - * When processing signed data the first time, the data is being written out into a file. - * For the second pass, that file is being read again. - * - * This strategy is recommended when larger amounts of data need to be processed. - * For smaller files, {@link InMemoryMultiPassStrategy} yields higher efficiency. - */ -public class WriteToFileMultiPassStrategy implements MultiPassStrategy { - - private final File file; - - /** - * Create a {@link MultiPassStrategy} which writes data to a file. - * Note that {@link #getMessageOutputStream()} will create the file if necessary. - * - * @param file file to write the data to and read from - */ - public WriteToFileMultiPassStrategy(File file) { - this.file = file; - } - - @Override - public OutputStream getMessageOutputStream() throws IOException { - if (!file.exists()) { - boolean created = file.createNewFile(); - if (!created) { - throw new IOException("New file '" + file.getAbsolutePath() + "' was not created."); - } - } - return new FileOutputStream(file); - } - - @Override - public InputStream getMessageInputStream() throws IOException { - if (!file.exists()) { - throw new IOException("File '" + file.getAbsolutePath() + "' does no longer exist."); - } - return new FileInputStream(file); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java deleted file mode 100644 index 3123338f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/cleartext_signatures/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to cleartext signature verification. - */ -package org.pgpainless.decryption_verification.cleartext_signatures; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt new file mode 100644 index 00000000..7a3b93ee --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.util.Strings +import org.pgpainless.exception.WrongConsumingMethodException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredInputStreamFactory +import java.io.* +import kotlin.jvm.Throws + +/** + * Utility class to deal with cleartext-signed messages. + * Based on Bouncycastle's [org.bouncycastle.openpgp.examples.ClearSignedFileProcessor]. + */ +class ClearsignedMessageUtil { + + companion object { + + /** + * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided + * messageOutputStream. + * + * @param clearsignedInputStream input stream containing a clearsigned message + * @param messageOutputStream output stream to which the dearmored message shall be written + * @return signatures + * + * @throws IOException if the message is not clearsigned or some other IO error happens + * @throws WrongConsumingMethodException in case the armored message is not cleartext signed + */ + @JvmStatic + @Throws(WrongConsumingMethodException::class, IOException::class) + fun detachSignaturesFromInbandClearsignedMessage( + clearsignedInputStream: InputStream, + messageOutputStream: OutputStream): PGPSignatureList { + val input: ArmoredInputStream = if (clearsignedInputStream is ArmoredInputStream) { + clearsignedInputStream + } else { + ArmoredInputStreamFactory.get(clearsignedInputStream) + } + + if (!input.isClearText) { + throw WrongConsumingMethodException("Message isn't using the Cleartext Signature Framework.") + } + + BufferedOutputStream(messageOutputStream).use { output -> + val lineOut = ByteArrayOutputStream() + var lookAhead = readInputLine(lineOut, input) + val lineSep = getLineSeparator() + + if (lookAhead != -1 && input.isClearText) { + var line = lineOut.toByteArray() + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + + while (lookAhead != -1 && input.isClearText) { + lookAhead = readInputLine(lineOut, lookAhead, input) + line = lineOut.toByteArray() + output.write(lineSep) + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + } + } else { + if (lookAhead != -1) { + val line = lineOut.toByteArray() + output.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line)) + } + } + } + + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(input) + val next = objectFactory.nextObject() ?: PGPSignatureList(arrayOf()) + return next as PGPSignatureList + } + + @JvmStatic + private fun readInputLine(bOut: ByteArrayOutputStream, fIn: InputStream): Int { + bOut.reset() + + var lookAhead = -1 + var ch: Int + + while (fIn.read().also { ch = it } >= 0) { + bOut.write(ch) + if (ch == '\r'.code || ch == '\n'.code) { + lookAhead = readPassedEOL(bOut, ch, fIn) + break + } + } + + return lookAhead + } + + @JvmStatic + private fun readInputLine(bOut: ByteArrayOutputStream, lookAhead: Int, fIn: InputStream): Int { + var mLookAhead = lookAhead + bOut.reset() + var ch = mLookAhead + do { + bOut.write(ch) + if (ch == '\r'.code || ch == '\n'.code) { + mLookAhead = readPassedEOL(bOut, ch, fIn) + break + } + } while (fIn.read().also { ch = it } >= 0) + if (ch < 0) { + mLookAhead = -1 + } + return mLookAhead + } + + @JvmStatic + private fun readPassedEOL(bOut: ByteArrayOutputStream, lastCh: Int, fIn: InputStream): Int { + var lookAhead = fIn.read() + if (lastCh == '\r'.code && lookAhead == '\n'.code) { + bOut.write(lookAhead) + lookAhead = fIn.read() + } + return lookAhead + } + + @JvmStatic + private fun getLineSeparator(): ByteArray { + val nl = Strings.lineSeparator() + val nlBytes = ByteArray(nl.length) + for (i in nlBytes.indices) { + nlBytes[i] = nl[i].code.toByte() + } + return nlBytes + } + + @JvmStatic + private fun getLengthWithoutSeparatorOrTrailingWhitespace(line: ByteArray): Int { + var end = line.size - 1 + while (end >= 0 && isWhiteSpace(line[end])) { + end-- + } + return end + 1 + } + + @JvmStatic + private fun isLineEnding(b: Byte): Boolean { + return b == '\r'.code.toByte() || b == '\n'.code.toByte() + } + + @JvmStatic + private fun isWhiteSpace(b: Byte): Boolean { + return isLineEnding(b) || b == '\t'.code.toByte() || b == ' '.code.toByte() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt new file mode 100644 index 00000000..eed6438f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + +/** + * Implementation of the [MultiPassStrategy]. + * This class keeps the read data in memory by caching the data inside a [ByteArrayOutputStream]. + * + * Note, that this class is suitable and efficient for processing small amounts of data. + * For larger data like encrypted files, use of the [WriteToFileMultiPassStrategy] is recommended to + * prevent [OutOfMemoryError] and other issues. + */ +class InMemoryMultiPassStrategy : MultiPassStrategy { + + private val cache = ByteArrayOutputStream() + + override val messageOutputStream: ByteArrayOutputStream + get() = cache + + override val messageInputStream: ByteArrayInputStream + get() = ByteArrayInputStream(getBytes()) + + fun getBytes(): ByteArray = messageOutputStream.toByteArray() +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt new file mode 100644 index 00000000..ae69b9c5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.* + +/** + * Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures, + * a strategy for how to cache the read data is required. + * Otherwise, large data kept in memory could cause an [OutOfMemoryError] or other issues. + * + * This is an Interface that describes a strategy to deal with the fact that detached signatures require multiple passes + * to do verification. + * + * This interface can be used to write the signed data stream out via [messageOutputStream] and later + * get access to the data again via [messageInputStream]. + * Thereby the detail where the data is being stored (memory, file, etc.) can be abstracted away. + */ +interface MultiPassStrategy { + + /** + * Provide an [OutputStream] into which the signed data can be read into. + * + * @return output stream + * @throws IOException io error + */ + val messageOutputStream: OutputStream + + /** + * Provide an [InputStream] which contains the data that was previously written away in + * [messageOutputStream]. + * + * As there may be multiple signatures that need to be processed, each call of this method MUST return + * a new [InputStream]. + * + * @return input stream + * @throws IOException io error + */ + val messageInputStream: InputStream + + companion object { + + /** + * Write the message content out to a file and re-read it to verify signatures. + * This strategy is best suited for larger messages (e.g. plaintext signed files) which might not fit into memory. + * After the message has been processed completely, the messages content are available at the provided file. + * + * @param file target file + * @return strategy + */ + @JvmStatic + fun writeMessageToFile(file: File): MultiPassStrategy { + return WriteToFileMultiPassStrategy(file) + } + + /** + * Read the message content into memory. + * This strategy is best suited for small messages which fit into memory. + * After the message has been processed completely, the message content can be accessed by calling + * [ByteArrayOutputStream.toByteArray] on [messageOutputStream]. + * + * @return strategy + */ + @JvmStatic + fun keepMessageInMemory(): InMemoryMultiPassStrategy { + return InMemoryMultiPassStrategy() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt new file mode 100644 index 00000000..5d6567a4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.cleartext_signatures + +import java.io.* + +/** + * Implementation of the [MultiPassStrategy]. + * When processing signed data the first time, the data is being written out into a file. + * For the second pass, that file is being read again. + * + * This strategy is recommended when larger amounts of data need to be processed. + * For smaller files, [InMemoryMultiPassStrategy] yields higher efficiency. + * + * @param file file to write the data to and read from + */ +class WriteToFileMultiPassStrategy( + private val file: File +) : MultiPassStrategy { + + override val messageOutputStream: OutputStream + @Throws(IOException::class) + get() { + if (!file.exists()) { + if (!file.createNewFile()) { + throw IOException("New file '${file.absolutePath}' could not be created.") + } + } + return FileOutputStream(file) + } + + override val messageInputStream: InputStream + @Throws(IOException::class) + get() { + if (!file.exists()) { + throw IOException("File '${file.absolutePath}' does no longer exist.") + } + return FileInputStream(file) + } + +} \ No newline at end of file diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index bafc2794..da6e0917 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -69,7 +69,7 @@ public class InlineDetachImpl implements InlineDetach { if (armorIn.isClearText()) { try { signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); - if (signatures == null) { + if (signatures.isEmpty()) { throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); } } catch (WrongConsumingMethodException e) { From 02511ac1ca6125e190bccdf139f7eb56f17714f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 16:10:51 +0200 Subject: [PATCH 091/351] Kotlin conversion: SignatureVerification --- .../SignatureVerification.java | 106 ------------------ .../SignatureVerification.kt | 48 ++++++++ 2 files changed, 48 insertions(+), 106 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java deleted file mode 100644 index 4a810eb9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureVerification.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Tuple of a signature and an identifier of its corresponding verification key. - * Semantic meaning of the signature verification (success, failure) is merely given by context. - * E.g. {@link OpenPgpMetadata#getVerifiedInbandSignatures()} contains verified verifications, - * while the class {@link Failure} contains failed verifications. - */ -public class SignatureVerification { - - private final PGPSignature signature; - private final SubkeyIdentifier signingKey; - - /** - * Construct a verification tuple. - * - * @param signature PGPSignature object - * @param signingKey identifier of the signing key - */ - public SignatureVerification(PGPSignature signature, @Nullable SubkeyIdentifier signingKey) { - this.signature = signature; - this.signingKey = signingKey; - } - - /** - * Return the {@link PGPSignature}. - * - * @return signature - */ - public PGPSignature getSignature() { - return signature; - } - - /** - * Return a {@link SubkeyIdentifier} of the (sub-) key that is used for signature verification. - * Note, that this method might return null, e.g. in case of a {@link Failure} due to missing verification key. - * - * @return verification key identifier - */ - @Nullable - public SubkeyIdentifier getSigningKey() { - return signingKey; - } - - @Override - public String toString() { - return "Signature: " + (signature != null ? Hex.toHexString(signature.getDigestPrefix()) : "null") - + "; Key: " + (signingKey != null ? signingKey.toString() : "null") + ";"; - } - - /** - * Tuple object of a {@link SignatureVerification} and the corresponding {@link SignatureValidationException} - * that caused the verification to fail. - */ - public static class Failure { - - private final SignatureVerification signatureVerification; - private final SignatureValidationException validationException; - - /** - * Construct a signature verification failure object. - * - * @param verification verification - * @param validationException exception that caused the verification to fail - */ - public Failure(SignatureVerification verification, SignatureValidationException validationException) { - this.signatureVerification = verification; - this.validationException = validationException; - } - - /** - * Return the verification (tuple of {@link PGPSignature} and corresponding {@link SubkeyIdentifier}) - * of the signing/verification key. - * - * @return verification - */ - public SignatureVerification getSignatureVerification() { - return signatureVerification; - } - - /** - * Return the {@link SignatureValidationException} that caused the verification to fail. - * - * @return exception - */ - public SignatureValidationException getValidationException() { - return validationException; - } - - @Override - public String toString() { - return signatureVerification.toString() + " Failure: " + getValidationException().getMessage(); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt new file mode 100644 index 00000000..a188f6a5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.SignatureVerification.Failure +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.signature.SignatureUtils + +/** + * Tuple of a signature and an identifier of its corresponding verification key. + * Semantic meaning of the signature verification (success, failure) is merely given by context. + * E.g. [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, + * while the class [Failure] contains failed verifications. + * + * @param signature PGPSignature object + * @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification. + * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. + */ +data class SignatureVerification( + val signature: PGPSignature, + val signingKey: SubkeyIdentifier? +) { + + override fun toString(): String { + return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + + " Key: ${signingKey?.toString() ?: "null"};" + } + + /** + * Tuple object of a [SignatureVerification] and the corresponding [SignatureValidationException] + * that caused the verification to fail. + * + * @param signatureVerification verification (tuple of [PGPSignature] and corresponding [SubkeyIdentifier]) + * @param validationException exception that caused the verification to fail + */ + data class Failure( + val signatureVerification: SignatureVerification, + val validationException: SignatureValidationException + ) { + override fun toString(): String { + return "$signatureVerification Failure: ${validationException.message}" + } + } +} \ No newline at end of file From 43335cbcd3b646d49102c558001adaa55d2a5c0a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 16:22:21 +0200 Subject: [PATCH 092/351] Kotlin conversion: SessionKey --- .../java/org/pgpainless/util/SessionKey.java | 66 ------------------- .../kotlin/org/pgpainless/util/SessionKey.kt | 48 ++++++++++++++ 2 files changed, 48 insertions(+), 66 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java b/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java deleted file mode 100644 index 1e71bb03..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/SessionKey.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -/** - * A {@link SessionKey} is the symmetric key that is used to encrypt/decrypt an OpenPGP message. - * The OpenPGP message header contains a copy of the session key, encrypted for the public key of each recipient. - */ -public class SessionKey { - - private final SymmetricKeyAlgorithm algorithm; - private final byte[] key; - - /** - * Constructor to create a session key from a BC {@link PGPSessionKey} object. - * - * @param sessionKey BC session key - */ - public SessionKey(@Nonnull PGPSessionKey sessionKey) { - this(SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), sessionKey.getKey()); - } - - /** - * Create a session key object from an algorithm and a key. - * - * @param algorithm algorithm - * @param key key - */ - public SessionKey(@Nonnull SymmetricKeyAlgorithm algorithm, @Nonnull byte[] key) { - this.algorithm = algorithm; - this.key = key; - } - - /** - * Return the symmetric key algorithm. - * - * @return algorithm - */ - public SymmetricKeyAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * Return the bytes of the key. - * - * @return key - */ - public byte[] getKey() { - byte[] copy = new byte[key.length]; - System.arraycopy(key, 0, copy, 0, copy.length); - return copy; - } - - @Override - public String toString() { - return "" + getAlgorithm().getAlgorithmId() + ":" + Hex.toHexString(getKey()); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt new file mode 100644 index 00000000..894d0869 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.util.encoders.Hex +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * A [SessionKey] is the symmetric key that is used to encrypt/decrypt an OpenPGP message payload. + * The OpenPGP message header contains a copy of the session key, encrypted for the public key of each recipient. + * + * @param algorithm symmetric key algorithm + * @param key bytes of the key + */ +data class SessionKey(val algorithm: SymmetricKeyAlgorithm, + val key: ByteArray) { + + /** + * Constructor to create a session key from a BC [PGPSessionKey] object. + * + * @param sessionKey BC session key + */ + constructor(sessionKey: PGPSessionKey): + this(SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm), sessionKey.key) + + override fun toString(): String { + return "${algorithm.algorithmId}:${Hex.toHexString(key)}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SessionKey + + if (algorithm != other.algorithm) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + return 31 * algorithm.hashCode() + key.contentHashCode() + } +} \ No newline at end of file From eaef1fe44aeb57d281d89ea14157d824a794e810 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:33:45 +0200 Subject: [PATCH 093/351] Kotlin conversion: KeyRingBuilder --- .../key/generation/KeyRingBuilder.java | 313 ------------------ .../generation/KeyRingBuilderInterface.java | 45 --- .../generation/KeySpecBuilderInterface.java | 26 -- .../key/generation/KeyRingBuilder.kt | 236 +++++++++++++ .../key/generation/KeyRingBuilderInterface.kt | 34 ++ .../key/generation/KeySpecBuilderInterface.kt | 23 ++ 6 files changed, 293 insertions(+), 384 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java deleted file mode 100644 index 84251c12..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ /dev/null @@ -1,313 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPKeyRingGenerator; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.util.Strings; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; -import org.pgpainless.provider.ProviderFactory; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; -import org.pgpainless.util.Passphrase; - -public class KeyRingBuilder implements KeyRingBuilderInterface { - - private static final long YEAR_IN_SECONDS = 1000L * 60 * 60 * 24 * 365; - - private KeySpec primaryKeySpec; - private final List subkeySpecs = new ArrayList<>(); - private final Map userIds = new LinkedHashMap<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private Date expirationDate = new Date(System.currentTimeMillis() + YEAR_IN_SECONDS * 5); // Expiration in 5 years - - @Override - public KeyRingBuilder setPrimaryKey(@Nonnull KeySpec keySpec) { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()); - verifyMasterKeyCanCertify(keySpec); - this.primaryKeySpec = keySpec; - return this; - } - - @Override - public KeyRingBuilder addSubkey(@Nonnull KeySpec keySpec) { - verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()); - this.subkeySpecs.add(keySpec); - return this; - } - - @Override - public KeyRingBuilder addUserId(@Nonnull String userId) { - this.userIds.put(userId.trim(), null); - return this; - } - - public KeyRingBuilder addUserId( - @Nonnull String userId, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback) { - this.userIds.put(userId.trim(), subpacketsCallback); - return this; - } - - @Override - public KeyRingBuilder addUserId(@Nonnull byte[] userId) { - return addUserId(Strings.fromUTF8ByteArray(userId)); - } - - @Override - public KeyRingBuilder setExpirationDate(@Nullable Date expirationDate) { - if (expirationDate == null) { - // No expiration - this.expirationDate = null; - return this; - } - - Date now = new Date(); - if (now.after(expirationDate)) { - throw new IllegalArgumentException("Expiration date must be in the future."); - } - this.expirationDate = expirationDate; - return this; - } - - @Override - public KeyRingBuilder setPassphrase(@Nonnull Passphrase passphrase) { - this.passphrase = passphrase; - return this; - } - - private void verifyKeySpecCompliesToPolicy(KeySpec keySpec, Policy policy) { - PublicKeyAlgorithm publicKeyAlgorithm = keySpec.getKeyType().getAlgorithm(); - int bitStrength = keySpec.getKeyType().getBitStrength(); - - if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); - } - } - - private void verifyMasterKeyCanCertify(KeySpec spec) { - if (!keyIsCertificationCapable(spec)) { - throw new IllegalArgumentException("Key algorithm " + spec.getKeyType().getName() + " is not capable of creating certifications."); - } - } - - private boolean keyIsCertificationCapable(KeySpec keySpec) { - return keySpec.getKeyType().canCertify(); - } - - @Override - public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException { - PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator(); - PBESecretKeyEncryptor secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator); - PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor(); - - passphrase.clear(); - - // Generate Primary Key - PGPKeyPair certKey = generateKeyPair(primaryKeySpec); - PGPContentSignerBuilder signer = buildContentSigner(certKey); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signer); - - SignatureSubpackets hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator(); - hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.getPublicKey()); - if (expirationDate != null) { - hashedSubPacketGenerator.setKeyExpirationTime(certKey.getPublicKey(), expirationDate); - } - if (!userIds.isEmpty()) { - hashedSubPacketGenerator.setPrimaryUserId(); - } - - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator); - PGPSignatureSubpacketVector hashedSubPackets = generator.generate(); - PGPKeyRingGenerator ringGenerator; - if (userIds.isEmpty()) { - ringGenerator = new PGPKeyRingGenerator( - certKey, - keyFingerprintCalculator, - hashedSubPackets, - null, - signer, - secretKeyEncryptor); - } else { - String primaryUserId = userIds.entrySet().iterator().next().getKey(); - ringGenerator = new PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, - primaryUserId, keyFingerprintCalculator, - hashedSubPackets, null, signer, secretKeyEncryptor); - } - - addSubKeys(certKey, ringGenerator); - - // Generate secret key ring with only primary user id - PGPSecretKeyRing secretKeyRing = ringGenerator.generateSecretKeyRing(); - - Iterator secretKeys = secretKeyRing.getSecretKeys(); - - // Attempt to add additional user-ids to the primary public key - PGPPublicKey primaryPubKey = secretKeys.next().getPublicKey(); - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.getSecretKey(), secretKeyDecryptor); - Iterator> userIdIterator = - this.userIds.entrySet().iterator(); - if (userIdIterator.hasNext()) { - userIdIterator.next(); // Skip primary user id - } - while (userIdIterator.hasNext()) { - Map.Entry additionalUserId = userIdIterator.next(); - String userIdString = additionalUserId.getKey(); - SelfSignatureSubpackets.Callback callback = additionalUserId.getValue(); - SelfSignatureSubpackets subpackets = null; - if (callback == null) { - subpackets = hashedSubPacketGenerator; - subpackets.setPrimaryUserId(null); - // additional user-ids are not primary - } else { - subpackets = SignatureSubpackets.createHashedSubpackets(primaryPubKey); - callback.modifyHashedSubpackets(subpackets); - } - signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.getCode(), privateKey); - signatureGenerator.setHashedSubpackets( - SignatureSubpacketsHelper.toVector((SignatureSubpackets) subpackets)); - PGPSignature additionalUserIdSignature = - signatureGenerator.generateCertification(userIdString, primaryPubKey); - primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, - userIdString, additionalUserIdSignature); - } - - // "reassemble" secret key ring with modified primary key - PGPSecretKey primarySecKey = new PGPSecretKey( - privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor); - List secretKeyList = new ArrayList<>(); - secretKeyList.add(primarySecKey); - while (secretKeys.hasNext()) { - secretKeyList.add(secretKeys.next()); - } - secretKeyRing = new PGPSecretKeyRing(secretKeyList); - return secretKeyRing; - } - - private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator) - throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { - for (KeySpec subKeySpec : subkeySpecs) { - PGPKeyPair subKey = generateKeyPair(subKeySpec); - if (subKeySpec.isInheritedSubPackets()) { - ringGenerator.addSubKey(subKey); - } else { - PGPSignatureSubpacketVector hashedSubpackets = subKeySpec.getSubpackets(); - try { - hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary( - primaryKey, subKey, hashedSubpackets); - } catch (IOException e) { - throw new PGPException("Exception while adding primary key binding signature to signing subkey", e); - } - ringGenerator.addSubKey(subKey, hashedSubpackets, null); - } - } - } - - private PGPSignatureSubpacketVector addPrimaryKeyBindingSignatureIfNecessary( - PGPKeyPair primaryKey, PGPKeyPair subKey, PGPSignatureSubpacketVector hashedSubpackets) - throws PGPException, IOException { - int keyFlagMask = hashedSubpackets.getKeyFlags(); - if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) && - !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { - return hashedSubpackets; - } - - PGPSignatureGenerator bindingSignatureGenerator = new PGPSignatureGenerator(buildContentSigner(subKey)); - bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.getCode(), subKey.getPrivateKey()); - PGPSignature primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.getPublicKey(), subKey.getPublicKey()); - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(hashedSubpackets); - subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig); - return subpacketGenerator.generate(); - } - - private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) { - HashAlgorithm hashAlgorithm = PGPainless.getPolicy() - .getSignatureHashAlgorithmPolicy().defaultHashAlgorithm(); - return ImplementationFactory.getInstance().getPGPContentSignerBuilder( - certKey.getPublicKey().getAlgorithm(), - hashAlgorithm.getAlgorithmId()); - } - - private PBESecretKeyEncryptor buildSecretKeyEncryptor(PGPDigestCalculator keyFingerprintCalculator) { - SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy() - .getSymmetricKeyEncryptionAlgorithmPolicy() - .getDefaultSymmetricKeyAlgorithm(); - if (!passphrase.isValid()) { - throw new IllegalStateException("Passphrase was cleared."); - } - return passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase); - } - - private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException { - if (!passphrase.isValid()) { - throw new IllegalStateException("Passphrase was cleared."); - } - return passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - } - - public static PGPKeyPair generateKeyPair(KeySpec spec) - throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException { - KeyType type = spec.getKeyType(); - KeyPairGenerator certKeyGenerator = KeyPairGenerator.getInstance(type.getName(), - ProviderFactory.getProvider()); - certKeyGenerator.initialize(type.getAlgorithmSpec()); - - // Create raw Key Pair - KeyPair keyPair = certKeyGenerator.generateKeyPair(); - - Date keyCreationDate = spec.getKeyCreationDate() != null ? spec.getKeyCreationDate() : new Date(); - - // Form PGP key pair - PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance() - .getPGPKeyPair(type.getAlgorithm(), keyPair, keyCreationDate); - return pgpKeyPair; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java deleted file mode 100644 index ecff123b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilderInterface.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.key.util.UserId; -import org.pgpainless.util.Passphrase; - -public interface KeyRingBuilderInterface> { - - B setPrimaryKey(@Nonnull KeySpec keySpec); - - default B setPrimaryKey(@Nonnull KeySpecBuilder builder) { - return setPrimaryKey(builder.build()); - } - - B addSubkey(@Nonnull KeySpec keySpec); - - default B addSubkey(@Nonnull KeySpecBuilder builder) { - return addSubkey(builder.build()); - } - - default B addUserId(UserId userId) { - return addUserId(userId.toString()); - } - - B addUserId(@Nonnull String userId); - - B addUserId(@Nonnull byte[] userId); - - B setExpirationDate(@Nonnull Date expirationDate); - - B setPassphrase(@Nonnull Passphrase passphrase); - - PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, - InvalidAlgorithmParameterException; -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java deleted file mode 100644 index 4a99bc8d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilderInterface.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -import java.util.Date; - -public interface KeySpecBuilderInterface { - - KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms); - - KeySpecBuilder overridePreferredHashAlgorithms(@Nonnull HashAlgorithm... preferredHashAlgorithms); - - KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms); - - KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate); - - KeySpec build(); -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt new file mode 100644 index 00000000..af650c4b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -0,0 +1,236 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.PGPDigestCalculator +import org.bouncycastle.util.Strings +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.policy.Policy +import org.pgpainless.provider.ProviderFactory +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper +import org.pgpainless.util.Passphrase +import java.io.IOException +import java.security.KeyPairGenerator +import java.util.* + +const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365 + +class KeyRingBuilder : KeyRingBuilderInterface { + + private var primaryKeySpec: KeySpec? = null + private val subKeySpecs = mutableListOf() + private val userIds = mutableMapOf() + private var passphrase = Passphrase.emptyPassphrase() + private var expirationDate: Date? = Date(System.currentTimeMillis() + (5 * MILLIS_IN_YEAR)) + + override fun setPrimaryKey(keySpec: KeySpec): KeyRingBuilder = apply { + verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + verifyPrimaryKeyCanCertify(keySpec) + this.primaryKeySpec = keySpec + } + + override fun addSubkey(keySpec: KeySpec): KeyRingBuilder = apply { + verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy()) + subKeySpecs.add(keySpec) + } + + override fun addUserId(userId: CharSequence): KeyRingBuilder = apply { + userIds[userId.toString().trim()] = null + } + + override fun addUserId(userId: ByteArray): KeyRingBuilder = addUserId(Strings.fromUTF8ByteArray(userId)) + + override fun setExpirationDate(expirationDate: Date?): KeyRingBuilder = apply { + if (expirationDate == null) { + this.expirationDate = null + return@apply + } + this.expirationDate = expirationDate.let { + require(Date() < expirationDate) { + "Expiration date must be in the future." + } + expirationDate + } + } + + override fun setPassphrase(passphrase: Passphrase): KeyRingBuilder = apply { + this.passphrase = passphrase + } + + private fun verifyKeySpecCompliesToPolicy(keySpec: KeySpec, policy: Policy) { + val algorithm = keySpec.keyType.algorithm + val bitStrength = keySpec.keyType.bitStrength + require(policy.publicKeyAlgorithmPolicy.isAcceptable(algorithm, bitStrength)) { + "Public key algorithm policy violation: $algorithm with bit strength $bitStrength is not acceptable." + } + } + + private fun verifyPrimaryKeyCanCertify(spec: KeySpec) { + require(keyIsCertificationCapable(spec)) { + "Key algorithm ${spec.keyType.name} is not capable of creation certifications." + } + } + + private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify() + + override fun build(): PGPSecretKeyRing { + val keyFingerprintCalculator = ImplementationFactory.getInstance() + .v4FingerprintCalculator + val secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator) + val secretKeyDecryptor = buildSecretKeyDecryptor() + + passphrase.clear() // Passphrase was used above, so we can get rid of it + + // generate primary key + requireNotNull(primaryKeySpec) { + "Primary Key spec required." + } + val certKey = generateKeyPair(primaryKeySpec!!) + val signer = buildContentSigner(certKey) + val signatureGenerator = PGPSignatureGenerator(signer) + + val hashedSubPacketGenerator = primaryKeySpec!!.subpacketGenerator + hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.publicKey) + expirationDate?.let { hashedSubPacketGenerator.setKeyExpirationTime(certKey.publicKey, it) } + if (userIds.isNotEmpty()) { + hashedSubPacketGenerator.setPrimaryUserId() + } + + val generator = PGPSignatureSubpacketGenerator() + SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator) + val hashedSubPackets = generator.generate() + val ringGenerator = if (userIds.isEmpty()) { + PGPKeyRingGenerator(certKey, keyFingerprintCalculator, hashedSubPackets, null, signer, + secretKeyEncryptor) + } else { + userIds.keys.first().let { primaryUserId -> + PGPKeyRingGenerator(SignatureType.POSITIVE_CERTIFICATION.code, certKey, primaryUserId, + keyFingerprintCalculator, hashedSubPackets, null, signer, + secretKeyEncryptor) + } + } + + addSubKeys(certKey, ringGenerator) + + // Generate secret key ring with only primary userId + val secretKeyRing = ringGenerator.generateSecretKeyRing() + val secretKeys = secretKeyRing.secretKeys + + // Attempt to add additional user-ids to the primary public key + var primaryPubKey = secretKeys.next().publicKey + val privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.secretKey, secretKeyDecryptor) + val userIdIterator = userIds.entries.iterator() + if (userIdIterator.hasNext()) { + userIdIterator.next() // Skip primary userId + } + while (userIdIterator.hasNext()) { + val additionalUserId = userIdIterator.next() + val userIdString = additionalUserId.key + val callback = additionalUserId.value + val subpackets = if (callback == null) { + hashedSubPacketGenerator.also { it.setPrimaryUserId(null) } + } else { + SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { callback.modifyHashedSubpackets(it) } + } + signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.code, privateKey) + signatureGenerator.setHashedSubpackets( + SignatureSubpacketsHelper.toVector(subpackets)) + val additionalUserIdSignature = signatureGenerator.generateCertification(userIdString, primaryPubKey) + primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, userIdString, additionalUserIdSignature) + } + + // Reassemble secret key ring with modified primary key + val primarySecretKey = PGPSecretKey( + privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor) + val secretKeyList = mutableListOf(primarySecretKey) + while (secretKeys.hasNext()) { + secretKeyList.add(secretKeys.next()) + } + return PGPSecretKeyRing(secretKeyList) + } + + private fun addSubKeys(primaryKey: PGPKeyPair, ringGenerator: PGPKeyRingGenerator) { + for (subKeySpec in subKeySpecs) { + val subKey = generateKeyPair(subKeySpec) + if (subKeySpec.isInheritedSubPackets) { + ringGenerator.addSubKey(subKey) + } else { + var hashedSubpackets = subKeySpec.subpackets + try { + hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(primaryKey, subKey, hashedSubpackets) + } catch (e: IOException) { + throw PGPException("Exception while adding primary key binding signature to signing subkey.", e) + } + ringGenerator.addSubKey(subKey, hashedSubpackets, null) + } + } + } + + private fun addPrimaryKeyBindingSignatureIfNecessary(primaryKey: PGPKeyPair, subKey: PGPKeyPair, hashedSubpackets: PGPSignatureSubpacketVector): PGPSignatureSubpacketVector { + val keyFlagMask = hashedSubpackets.keyFlags + if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) && + !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { + return hashedSubpackets + } + + val bindingSignatureGenerator = PGPSignatureGenerator(buildContentSigner(subKey)) + bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.code, subKey.privateKey) + val primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey) + val subpacketGenerator = PGPSignatureSubpacketGenerator(hashedSubpackets) + subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig) + return subpacketGenerator.generate() + } + + private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder { + val hashAlgorithm = PGPainless.getPolicy().signatureHashAlgorithmPolicy.defaultHashAlgorithm + return ImplementationFactory.getInstance().getPGPContentSignerBuilder( + certKey.publicKey.algorithm, hashAlgorithm.algorithmId) + } + + private fun buildSecretKeyEncryptor(keyFingerprintCalculator: PGPDigestCalculator): PBESecretKeyEncryptor? { + val keyEncryptionAlgorithm = PGPainless.getPolicy().symmetricKeyEncryptionAlgorithmPolicy.defaultSymmetricKeyAlgorithm + check(passphrase.isValid) { + "Passphrase was cleared." + } + return if (passphrase.isEmpty) null else ImplementationFactory.getInstance() + .getPBESecretKeyEncryptor(keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase) + } + + private fun buildSecretKeyDecryptor(): PBESecretKeyDecryptor? { + check(passphrase.isValid) { + "Passphrase was cleared." + } + return if (passphrase.isEmpty) null else ImplementationFactory.getInstance() + .getPBESecretKeyDecryptor(passphrase) + } + + companion object { + @JvmStatic + fun generateKeyPair(spec: KeySpec): PGPKeyPair { + spec.keyType.let { type -> + // Create raw Key Pair + val keyPair = KeyPairGenerator.getInstance(type.name, ProviderFactory.getProvider()) + .also { it.initialize(type.algorithmSpec) } + .generateKeyPair() + + val keyCreationDate = spec.keyCreationDate ?: Date() + // Form PGP Key Pair + return ImplementationFactory.getInstance() + .getPGPKeyPair(type.algorithm, keyPair, keyCreationDate) + } + } + } + +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt new file mode 100644 index 00000000..0ed301fe --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.util.Passphrase +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* + +interface KeyRingBuilderInterface> { + + fun setPrimaryKey(keySpec: KeySpec): B + + fun setPrimaryKey(builder: KeySpecBuilder): B = setPrimaryKey(builder.build()) + + fun addSubkey(keySpec: KeySpec): B + + fun addSubkey(builder: KeySpecBuilder): B = addSubkey(builder.build()) + + fun addUserId(userId: CharSequence): B + + fun addUserId(userId: ByteArray): B + + fun setExpirationDate(expirationDate: Date?): B + + fun setPassphrase(passphrase: Passphrase): B + + @Throws(NoSuchAlgorithmException::class, PGPException::class, InvalidAlgorithmParameterException::class) + fun build(): PGPSecretKeyRing +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt new file mode 100644 index 00000000..fcafb9c1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import java.util.* + +interface KeySpecBuilderInterface { + + fun overridePreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): KeySpecBuilder + + fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder + + fun overridePreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): KeySpecBuilder + + fun setKeyCreationDate(creationDate: Date): KeySpecBuilder + + fun build(): KeySpec +} \ No newline at end of file From bb17c627ce1417b26f1bd585786587e65239c869 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:43:16 +0200 Subject: [PATCH 094/351] Kotlin conversion: KeySpecBuilder --- .../key/generation/KeySpecBuilder.java | 88 ------------------- .../key/generation/KeySpecBuilder.kt | 63 +++++++++++++ 2 files changed, 63 insertions(+), 88 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java deleted file mode 100644 index 1abd7dcd..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpecBuilder.java +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import java.util.Arrays; -import java.util.Date; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; - -public class KeySpecBuilder implements KeySpecBuilderInterface { - - private final KeyType type; - private final KeyFlag[] keyFlags; - private final SelfSignatureSubpackets hashedSubpackets = new SignatureSubpackets(); - private final AlgorithmSuite algorithmSuite = PGPainless.getPolicy().getKeyGenerationAlgorithmSuite(); - private Set preferredCompressionAlgorithms = algorithmSuite.getCompressionAlgorithms(); - private Set preferredHashAlgorithms = algorithmSuite.getHashAlgorithms(); - private Set preferredSymmetricAlgorithms = algorithmSuite.getSymmetricKeyAlgorithms(); - private Date keyCreationDate; - - KeySpecBuilder(@Nonnull KeyType type, KeyFlag... flags) { - if (flags == null) { - this.keyFlags = new KeyFlag[0]; - } else { - SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, flags); - this.keyFlags = flags; - } - this.type = type; - } - - @Override - public KeySpecBuilder overridePreferredCompressionAlgorithms( - @Nonnull CompressionAlgorithm... compressionAlgorithms) { - this.preferredCompressionAlgorithms = new LinkedHashSet<>(Arrays.asList(compressionAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder overridePreferredHashAlgorithms( - @Nonnull HashAlgorithm... preferredHashAlgorithms) { - this.preferredHashAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredHashAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder overridePreferredSymmetricKeyAlgorithms( - @Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms) { - for (SymmetricKeyAlgorithm algo : preferredSymmetricKeyAlgorithms) { - if (algo == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("NULL (unencrypted) is an invalid symmetric key algorithm preference."); - } - } - this.preferredSymmetricAlgorithms = new LinkedHashSet<>(Arrays.asList(preferredSymmetricKeyAlgorithms)); - return this; - } - - @Override - public KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate) { - this.keyCreationDate = creationDate; - return this; - } - - @Override - public KeySpec build() { - this.hashedSubpackets.setKeyFlags(keyFlags); - this.hashedSubpackets.setPreferredCompressionAlgorithms(preferredCompressionAlgorithms); - this.hashedSubpackets.setPreferredHashAlgorithms(preferredHashAlgorithms); - this.hashedSubpackets.setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms); - this.hashedSubpackets.setFeatures(Feature.MODIFICATION_DETECTION); - - return new KeySpec(type, (SignatureSubpackets) hashedSubpackets, false, keyCreationDate); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt new file mode 100644 index 00000000..a430e796 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.* +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import java.util.* + +class KeySpecBuilder constructor( + private val type: KeyType, + private val keyFlags: List, +) : KeySpecBuilderInterface { + + private val hashedSubpackets: SelfSignatureSubpackets = SignatureSubpackets() + private val algorithmSuite: AlgorithmSuite = PGPainless.getPolicy().keyGenerationAlgorithmSuite + private var preferredCompressionAlgorithms: Set = algorithmSuite.compressionAlgorithms + private var preferredHashAlgorithms: Set = algorithmSuite.hashAlgorithms + private var preferredSymmetricAlgorithms: Set = algorithmSuite.symmetricKeyAlgorithms + private var keyCreationDate = Date() + + constructor(type: KeyType, vararg keyFlags: KeyFlag): this(type, listOf(*keyFlags)) + + init { + SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, *keyFlags.toTypedArray()) + } + + override fun overridePreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): KeySpecBuilder = apply { + this.preferredCompressionAlgorithms = algorithms.toSet() + } + + override fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder = apply { + this.preferredHashAlgorithms = algorithms.toSet() + } + + override fun overridePreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): KeySpecBuilder = apply { + require(!algorithms.contains(SymmetricKeyAlgorithm.NULL)) { + "NULL (unencrypted) is an invalid symmetric key algorithm preference." + } + this.preferredSymmetricAlgorithms = algorithms.toSet() + } + + override fun setKeyCreationDate(creationDate: Date): KeySpecBuilder = apply { + this.keyCreationDate = creationDate + } + + override fun build(): KeySpec { + return hashedSubpackets.apply { + setKeyFlags(keyFlags) + setPreferredCompressionAlgorithms(preferredCompressionAlgorithms) + setPreferredHashAlgorithms(preferredHashAlgorithms) + setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms) + setFeatures(Feature.MODIFICATION_DETECTION) + }.let { + KeySpec(type, hashedSubpackets as SignatureSubpackets, false, keyCreationDate) + } + } +} \ No newline at end of file From 41f56bdf99ab7db8d992f8bc9a447d95228f7a7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:44:15 +0200 Subject: [PATCH 095/351] Kotlin conversion: KeySpec --- .../pgpainless/key/generation/KeySpec.java | 62 ------------------- .../org/pgpainless/key/generation/KeySpec.kt | 28 +++++++++ 2 files changed, 28 insertions(+), 62 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java deleted file mode 100644 index 6ac08aa7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeySpec.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; - -import java.util.Date; - -public class KeySpec { - - private final KeyType keyType; - private final SignatureSubpackets subpacketGenerator; - private final boolean inheritedSubPackets; - private final Date keyCreationDate; - - KeySpec(@Nonnull KeyType type, - @Nonnull SignatureSubpackets subpacketGenerator, - boolean inheritedSubPackets, - @Nullable Date keyCreationDate) { - this.keyType = type; - this.subpacketGenerator = subpacketGenerator; - this.inheritedSubPackets = inheritedSubPackets; - this.keyCreationDate = keyCreationDate; - } - - @Nonnull - public KeyType getKeyType() { - return keyType; - } - - @Nonnull - public PGPSignatureSubpacketVector getSubpackets() { - return SignatureSubpacketsHelper.toVector(subpacketGenerator); - } - - @Nonnull - public SignatureSubpackets getSubpacketGenerator() { - return subpacketGenerator; - } - - boolean isInheritedSubPackets() { - return inheritedSubPackets; - } - - @Nullable - public Date getKeyCreationDate() { - return keyCreationDate; - } - - public static KeySpecBuilder getBuilder(KeyType type, KeyFlag... flags) { - return new KeySpecBuilder(type, flags); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt new file mode 100644 index 00000000..fd0b8635 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper +import java.util.* + +data class KeySpec( + val keyType: KeyType, + val subpacketGenerator: SignatureSubpackets, + val isInheritedSubPackets: Boolean, + val keyCreationDate: Date +) { + + val subpackets: PGPSignatureSubpacketVector + get() = SignatureSubpacketsHelper.toVector(subpacketGenerator) + + companion object { + @JvmStatic + fun getBuilder(type: KeyType, vararg flags: KeyFlag) = KeySpecBuilder(type, *flags) + } +} \ No newline at end of file From b343b4ad1725b30da2375580d9a86d7de6e162a9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 16:41:00 +0200 Subject: [PATCH 096/351] Kotlin conversion: KeyRingTemplates --- .../key/generation/KeyRingTemplates.java | 255 ------------------ .../key/generation/KeyRingTemplates.kt | 185 +++++++++++++ 2 files changed, 185 insertions(+), 255 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java deleted file mode 100644 index 6966232b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingTemplates.java +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation; - -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.util.Passphrase; - -public final class KeyRingTemplates { - - public KeyRingTemplates() { - - } - - /** - * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, - * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. - * - * @param userId userId or null - * @param length length of the RSA keys - * @return key - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return rsaKeyRing(userId, length, Passphrase.emptyPassphrase()); - } - - /** - * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, - * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. - * - * @param userId userId or null - * @param length length of the RSA keys - * @param password passphrase to encrypt the key with - * @return key - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length, - @Nonnull String password) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return rsaKeyRing(userId, length, passphrase); - } - - /** - * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, - * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. - * - * @param userId userId or null - * @param length length of the RSA keys - * @param passphrase passphrase to encrypt the key with - * @return key - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing rsaKeyRing(@Nullable CharSequence userId, - @Nonnull RsaLength length, - @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - - if (userId != null) { - builder.addUserId(userId.toString()); - } - - if (!passphrase.isEmpty()) { - builder.setPassphrase(passphrase); - } - - return builder.build(); - } - - /** - * Creates a simple, unencrypted RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()); - } - - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId.toString()); - } - return builder.build(); - } - - /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. - * - * @param userId user id. - * @param length length in bits. - * @param password Password of the key. Can be null for unencrypted keys. - * - * @return {@link PGPSecretKeyRing} containing the KeyPair. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleRsaKeyRing(@Nullable CharSequence userId, @Nonnull RsaLength length, @Nullable String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return simpleRsaKeyRing(userId, length, passphrase); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a curve25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - return simpleEcKeyRing(userId, Passphrase.emptyPassphrase()); - } - - /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. - * - * @param userId user-id - * @param password Password of the private key. Can be null for an unencrypted key. - * - * @return {@link PGPSecretKeyRing} containing the key pairs. - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, String password) - throws PGPException, NoSuchAlgorithmException, InvalidAlgorithmParameterException { - Passphrase passphrase = Passphrase.emptyPassphrase(); - if (!isNullOrEmpty(password)) { - passphrase = Passphrase.fromPassword(password); - } - return simpleEcKeyRing(userId, passphrase); - } - - public PGPSecretKeyRing simpleEcKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId.toString()); - } - return builder.build(); - } - - /** - * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify - * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. - * - * @param userId primary user id - * @return key ring - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId) throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - return modernKeyRing(userId, Passphrase.emptyPassphrase()); - } - - /** - * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify - * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. - * - * @param userId primary user id - * @param password passphrase or null if the key should be unprotected. - * @return key ring - * - * @throws InvalidAlgorithmParameterException in case of invalid key generation parameters - * @throws NoSuchAlgorithmException in case of missing algorithm implementation in the crypto provider - * @throws PGPException in case of an OpenPGP related error - */ - public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nullable String password) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { - Passphrase passphrase = (password != null ? Passphrase.fromPassword(password) : Passphrase.emptyPassphrase()); - return modernKeyRing(userId, passphrase); - } - - public PGPSecretKeyRing modernKeyRing(@Nullable CharSequence userId, @Nonnull Passphrase passphrase) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder builder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .setPassphrase(passphrase); - if (userId != null) { - builder.addUserId(userId.toString()); - } - return builder.build(); - } - - private static boolean isNullOrEmpty(String password) { - return password == null || password.trim().isEmpty(); - } - -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt new file mode 100644 index 00000000..82215971 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation + +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless.Companion.buildKeyRing +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.KeySpec.Companion.getBuilder +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.util.Passphrase + +class KeyRingTemplates { + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param passphrase passphrase to encrypt the key with. Can be empty for an unencrytped key. + * @return key + */ + @JvmOverloads + fun rsaKeyRing(userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = buildKeyRing().apply { + setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) + addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + }.build() + + /** + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, + * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * + * @param userId userId or null + * @param length length of the RSA keys + * @param password passphrase to encrypt the key with. Can be null or blank for unencrypted keys. + * @return key + */ + fun rsaKeyRing(userId: CharSequence?, + length: RsaLength, + password: String? + ): PGPSecretKeyRing = password.let { + if (it.isNullOrBlank()) { + rsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + rsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } + } + + /** + * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. + * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. + * + * @param userId user id. + * @param length length in bits. + * @param password Password of the key. Can be empty for unencrypted keys. + * + * @return {@link PGPSecretKeyRing} containing the KeyPair. + */ + @JvmOverloads + fun simpleRsaKeyRing(userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = buildKeyRing().apply { + setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + }.build() + + /** + * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. + * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. + * + * @param userId user id. + * @param length length in bits. + * @param password Password of the key. Can be null or blank for unencrypted keys. + * + * @return {@link PGPSecretKeyRing} containing the KeyPair. + */ + fun simpleRsaKeyRing(userId: CharSequence?, + length: RsaLength, + password: String? + ) = password.let { + if (it.isNullOrBlank()) { + simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + simpleRsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } + } + + /** + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. + * The EdDSA primary key is used for signing messages and certifying the sub key. + * The XDH subkey is used for encryption and decryption of messages. + * + * @param userId user-id + * @param passphrase Password of the private key. Can be empty for an unencrypted key. + * + * @return {@link PGPSecretKeyRing} containing the key pairs. + */ + @JvmOverloads + fun simpleEcKeyRing(userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = buildKeyRing().apply { + setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + addSubkey(getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + }.build() + + /** + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. + * The EdDSA primary key is used for signing messages and certifying the sub key. + * The XDH subkey is used for encryption and decryption of messages. + * + * @param userId user-id + * @param passphrase Password of the private key. Can be null or blank for an unencrypted key. + * + * @return {@link PGPSecretKeyRing} containing the key pairs. + */ + fun simpleEcKeyRing(userId: CharSequence?, + password: String? + ): PGPSecretKeyRing = password.let { + if (it.isNullOrBlank()) { + simpleEcKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + simpleEcKeyRing(userId, Passphrase.fromPassword(it)) + } + } + + /** + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify + * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * + * @param userId primary user id + * @param passphrase passphrase for the private key. Can be empty for an unencrypted key. + * @return key ring + */ + @JvmOverloads + fun modernKeyRing(userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = buildKeyRing().apply { + setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + addSubkey(getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + addSubkey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + }.build() + + /** + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify + * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * + * @param userId primary user id + * @param password passphrase for the private key. Can be null or blank for an unencrypted key. + * @return key ring + */ + fun modernKeyRing(userId: CharSequence?, + password: String? + ): PGPSecretKeyRing = password.let { + if (it.isNullOrBlank()) { + modernKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + modernKeyRing(userId, Passphrase.fromPassword(it)) + } + } +} \ No newline at end of file From c40e2ba6c22e692ab33fa37d2c6d87a5641ff853 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 16:44:26 +0200 Subject: [PATCH 097/351] Move const to companion object --- .../kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index af650c4b..c101e2c0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -25,7 +25,6 @@ import java.io.IOException import java.security.KeyPairGenerator import java.util.* -const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365 class KeyRingBuilder : KeyRingBuilderInterface { @@ -217,6 +216,8 @@ class KeyRingBuilder : KeyRingBuilderInterface { } companion object { + const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365 + @JvmStatic fun generateKeyPair(spec: KeySpec): PGPKeyPair { spec.keyType.let { type -> From 5fce443ad9c73efbdcbe8ea5b8530624da0f4f1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 17:15:36 +0200 Subject: [PATCH 098/351] Kotlin conversion: SecretKeyRingProtector and subclasses --- .../BaseSecretKeyRingProtector.java | 6 +- .../CachingSecretKeyRingProtector.java | 18 +- .../PasswordBasedSecretKeyRingProtector.java | 8 +- .../protection/SecretKeyRingProtector.java | 168 ------------------ .../key/protection/UnlockSecretKey.java | 3 +- .../protection/UnprotectedKeysProtector.java | 6 +- .../MapBasedPassphraseProvider.java | 4 +- .../SecretKeyPassphraseProvider.java | 4 +- .../SolitaryPassphraseProvider.java | 4 +- .../key/protection/SecretKeyRingProtector.kt | 150 ++++++++++++++++ .../MissingPassphraseForDecryptionTest.java | 8 +- ...tionUsingKeyWithMissingPassphraseTest.java | 12 +- .../CachingSecretKeyRingProtectorTest.java | 4 +- .../PassphraseProtectedKeyTest.java | 8 +- .../SecretKeyRingProtectorTest.java | 4 +- .../MatchMakingSecretKeyRingProtector.java | 6 +- 16 files changed, 198 insertions(+), 215 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java index 1a31d0e8..fead3938 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java @@ -44,13 +44,13 @@ public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { } @Override - public boolean hasPassphraseFor(Long keyId) { + public boolean hasPassphraseFor(long keyId) { return passphraseProvider.hasPassphrase(keyId); } @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { + public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null || passphrase.isEmpty() ? null : ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); @@ -58,7 +58,7 @@ public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { @Override @Nullable - public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { + public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); return passphrase == null || passphrase.isEmpty() ? null : ImplementationFactory.getInstance().getPBESecretKeyEncryptor( diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java index 8cdb3efb..0b4fe084 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java @@ -60,12 +60,12 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se * containing a key with the same key-id but different passphrases. * * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, you can use - * {@link #replacePassphrase(Long, Passphrase)} to replace the passphrase. + * {@link #replacePassphrase(long, Passphrase)} to replace the passphrase. * * @param keyId id of the key * @param passphrase passphrase */ - public void addPassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) { + public void addPassphrase(long keyId, @Nonnull Passphrase passphrase) { if (this.cache.containsKey(keyId)) { throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" + "If you want to replace the passphrase, use replacePassphrase(Long, Passphrase) instead."); @@ -79,7 +79,7 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se * @param keyId keyId * @param passphrase passphrase */ - public void replacePassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) { + public void replacePassphrase(long keyId, @Nonnull Passphrase passphrase) { this.cache.put(keyId, passphrase); } @@ -152,7 +152,7 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se * * @param keyId id of the key */ - public void forgetPassphrase(@Nonnull Long keyId) { + public void forgetPassphrase(long keyId) { Passphrase passphrase = cache.remove(keyId); if (passphrase != null) { passphrase.clear(); @@ -183,7 +183,7 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se @Override @Nullable - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { Passphrase passphrase = cache.get(keyId); if (passphrase == null || !passphrase.isValid()) { if (provider == null) { @@ -198,25 +198,25 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { Passphrase passphrase = cache.get(keyId); return passphrase != null && passphrase.isValid(); } @Override - public boolean hasPassphraseFor(Long keyId) { + public boolean hasPassphraseFor(long keyId) { return hasPassphrase(keyId); } @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { + public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { return protector.getDecryptor(keyId); } @Override @Nullable - public PBESecretKeyEncryptor getEncryptor(@Nonnull Long keyId) throws PGPException { + public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { return protector.getEncryptor(keyId); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java index c5745068..0e387085 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java @@ -40,12 +40,12 @@ public class PasswordBasedSecretKeyRingProtector extends BaseSecretKeyRingProtec SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { @Override @Nullable - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { return hasPassphrase(keyId) ? passphrase : null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return keyRing.getPublicKey(keyId) != null; } }; @@ -60,7 +60,7 @@ public class PasswordBasedSecretKeyRingProtector extends BaseSecretKeyRingProtec SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { @Nullable @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { if (keyId == singleKeyId) { return passphrase; } @@ -68,7 +68,7 @@ public class PasswordBasedSecretKeyRingProtector extends BaseSecretKeyRingProtec } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return keyId == singleKeyId; } }; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java deleted file mode 100644 index d7ed5c85..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Task of the {@link SecretKeyRingProtector} is to map encryptor/decryptor objects to key-ids. - * {@link PBESecretKeyEncryptor PBESecretKeyEncryptors}/{@link PBESecretKeyDecryptor PBESecretKeyDecryptors} are used - * to encrypt/decrypt secret keys using a passphrase. - * - * While it is easy to create an implementation of this interface that fits your needs, there are a bunch of - * implementations ready for use. - */ -public interface SecretKeyRingProtector { - - /** - * Returns true, if the protector has a passphrase for the key with the given key-id. - * - * @param keyId key id - * @return true if it has a passphrase, false otherwise - */ - boolean hasPassphraseFor(Long keyId); - - /** - * Return a decryptor for the key of id {@code keyId}. - * This method returns null if the key is unprotected. - * - * @param keyId id of the key - * @return decryptor for the key - * - * @throws PGPException if the decryptor cannot be created for some reason - */ - @Nullable PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException; - - /** - * Return an encryptor for the key of id {@code keyId}. - * This method returns null if the key is unprotected. - * - * @param keyId id of the key - * @return encryptor for the key - * - * @throws PGPException if the encryptor cannot be created for some reason - */ - @Nullable PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException; - - /** - * Return a protector for secret keys. - * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases - * at runtime. - * - * See {@link CachingSecretKeyRingProtector} for how to memorize/forget additional passphrases during runtime. - * - * @param missingPassphraseCallback callback that is used to provide missing passphrases. - * @return caching secret key protector - */ - static CachingSecretKeyRingProtector defaultSecretKeyRingProtector(SecretKeyPassphraseProvider missingPassphraseCallback) { - return new CachingSecretKeyRingProtector( - new HashMap<>(), - KeyRingProtectionSettings.secureDefaultSettings(), - missingPassphraseCallback); - } - - /** - * Use the provided passphrase to lock/unlock all keys in the provided key ring. - * - * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. - * For other keys that are not present in the ring, it will return null. - * - * @param passphrase passphrase - * @param keys key ring - * @return protector - * @deprecated use {@link #unlockEachKeyWith(Passphrase, PGPSecretKeyRing)} instead. - * - * TODO: Remove in 1.2.X - */ - @Deprecated - static SecretKeyRingProtector unlockAllKeysWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKeyRing keys) { - return unlockEachKeyWith(passphrase, keys); - } - - /** - * Use the provided passphrase to lock/unlock all keys in the provided key ring. - * - * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. - * For other keys that are not present in the ring, it will return null. - * - * @param passphrase passphrase - * @param keys key ring - * @return protector - */ - static SecretKeyRingProtector unlockEachKeyWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKeyRing keys) { - Map map = new ConcurrentHashMap<>(); - for (PGPSecretKey secretKey : keys) { - map.put(secretKey.getKeyID(), passphrase); - } - return fromPassphraseMap(map); - } - - /** - * Use the provided passphrase to unlock any key. - * - * @param passphrase passphrase - * @return protector - */ - static SecretKeyRingProtector unlockAnyKeyWith(@Nonnull Passphrase passphrase) { - return new BaseSecretKeyRingProtector(new SolitaryPassphraseProvider(passphrase)); - } - - /** - * Use the provided passphrase to lock/unlock only the provided (sub-)key. - * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if - * {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} is getting called with the key-id of the provided key. - * - * Otherwise, this protector will always return null. - * - * @param passphrase passphrase - * @param key key to lock/unlock - * @return protector - */ - static SecretKeyRingProtector unlockSingleKeyWith(@Nonnull Passphrase passphrase, @Nonnull PGPSecretKey key) { - return PasswordBasedSecretKeyRingProtector.forKey(key, passphrase); - } - - static SecretKeyRingProtector unlockSingleKeyWith(@Nonnull Passphrase passphrase, long keyId) { - return PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase); - } - - /** - * Protector for unprotected keys. - * This protector returns null for all {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} calls, - * no matter what the key-id is. - * - * As a consequence, this protector can only "unlock" keys which are not protected using a passphrase, and it will - * leave keys unprotected, should it be used to "protect" a key - * (e.g. in {@link org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor#changePassphraseFromOldPassphrase(Passphrase)}). - * - * @return protector - */ - static SecretKeyRingProtector unprotectedKeys() { - return new UnprotectedKeysProtector(); - } - - /** - * Use the provided map of key-ids and passphrases to unlock keys. - * - * @param passphraseMap map of key ids and their respective passphrases - * @return protector - */ - static SecretKeyRingProtector fromPassphraseMap(@Nonnull Map passphraseMap) { - return new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java index 78c849ab..154a1aa1 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java @@ -61,6 +61,7 @@ public final class UnlockSecretKey { public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) throws PGPException, KeyIntegrityException { - return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)); + return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith( + passphrase == null ? Passphrase.emptyPassphrase() : passphrase, secretKey)); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java index 26b5c596..5d3f3361 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java @@ -15,19 +15,19 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; public class UnprotectedKeysProtector implements SecretKeyRingProtector { @Override - public boolean hasPassphraseFor(Long keyId) { + public boolean hasPassphraseFor(long keyId) { return true; } @Override @Nullable - public PBESecretKeyDecryptor getDecryptor(Long keyId) { + public PBESecretKeyDecryptor getDecryptor(long keyId) { return null; } @Override @Nullable - public PBESecretKeyEncryptor getEncryptor(Long keyId) { + public PBESecretKeyEncryptor getEncryptor(long keyId) { return null; } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java index 859758b1..3f0ee2f9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java @@ -31,12 +31,12 @@ public class MapBasedPassphraseProvider implements SecretKeyPassphraseProvider { @Nullable @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { return map.get(keyId); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return map.containsKey(keyId); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java index 59cb39ce..91c2bc95 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java @@ -34,7 +34,7 @@ public interface SecretKeyPassphraseProvider { * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. */ - @Nullable Passphrase getPassphraseFor(Long keyId); + @Nullable Passphrase getPassphraseFor(long keyId); - boolean hasPassphrase(Long keyId); + boolean hasPassphrase(long keyId); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java index b439ef56..9400229b 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java @@ -21,13 +21,13 @@ public class SolitaryPassphraseProvider implements SecretKeyPassphraseProvider { @Nullable @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { // always return the same passphrase. return passphrase; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return true; } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt new file mode 100644 index 00000000..8bdac8d6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider +import org.pgpainless.util.Passphrase +import kotlin.jvm.Throws + +/** + * Task of the [SecretKeyRingProtector] is to map encryptor/decryptor objects to key-ids. + * [PBESecretKeyEncryptor]/[PBESecretKeyDecryptor] are used to encrypt/decrypt secret keys using a passphrase. + * + * While it is easy to create an implementation of this interface that fits your needs, there are a bunch of + * implementations ready for use. + */ +interface SecretKeyRingProtector { + + /** + * Returns true, if the protector has a passphrase for the key with the given key-id. + * + * @param keyId key id + * @return true if it has a passphrase, false otherwise + */ + fun hasPassphraseFor(keyId: Long): Boolean + + /** + * Return a decryptor for the key of id `keyId`. + * This method returns null if the key is unprotected. + * + * @param keyId id of the key + * @return decryptor for the key + */ + @Throws(PGPException::class) + fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? + + /** + * Return an encryptor for the key of id `keyId`. + * This method returns null if the key is unprotected. + * + * @param keyId id of the key + * @return encryptor for the key + */ + @Throws(PGPException::class) + fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? + + companion object { + + /** + * Return a protector for secret keys. + * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases + * at runtime. + * + * See [CachingSecretKeyRingProtector] for how to memorize/forget additional passphrases during runtime. + * + * @param missingPassphraseCallback callback that is used to provide missing passphrases. + * @return caching secret key protector + */ + @JvmStatic + fun defaultSecretKeyRingProtector( + missingPassphraseCallback: SecretKeyPassphraseProvider? + ): CachingSecretKeyRingProtector = CachingSecretKeyRingProtector( + mapOf(), KeyRingProtectionSettings.secureDefaultSettings(), missingPassphraseCallback) + + /** + * Use the provided passphrase to lock/unlock all keys in the provided key ring. + * + * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. + * For other keys that are not present in the ring, it will return null. + * + * @param passphrase passphrase + * @param keys key ring + * @return protector + */ + @JvmStatic + fun unlockEachKeyWith(passphrase: Passphrase, keys: PGPSecretKeyRing): SecretKeyRingProtector = + fromPassphraseMap(keys.map { it.keyID }.associateWith { passphrase }) + + /** + * Use the provided passphrase to unlock any key. + * + * @param passphrase passphrase + * @return protector + */ + @JvmStatic + fun unlockAnyKeyWith(passphrase: Passphrase): SecretKeyRingProtector = + BaseSecretKeyRingProtector(SolitaryPassphraseProvider(passphrase)) + + /** + * Use the provided passphrase to lock/unlock only the provided (sub-)key. + * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if + * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. + * + * Otherwise, this protector will always return null. + * + * @param passphrase passphrase + * @param key key to lock/unlock + * @return protector + */ + @JvmStatic + fun unlockSingleKeyWith(passphrase: Passphrase, key: PGPSecretKey): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKey(key, passphrase) + + /** + * Use the provided passphrase to lock/unlock only the provided (sub-)key. + * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if + * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. + * + * Otherwise, this protector will always return null. + * + * @param passphrase passphrase + * @param keyId id of the key to lock/unlock + * @return protector + */ + @JvmStatic + fun unlockSingleKeyWith(passphrase: Passphrase, keyId: Long): SecretKeyRingProtector = + PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase) + + /** + * Protector for unprotected keys. + * This protector returns null for all [getEncryptor]/[getDecryptor] calls, + * no matter what the key-id is. + * + * As a consequence, this protector can only "unlock" keys which are not protected using a passphrase, and it will + * leave keys unprotected, should it be used to "protect" a key + * (e.g. in [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor.changePassphraseFromOldPassphrase]). + * + * @return protector + */ + @JvmStatic + fun unprotectedKeys() = UnprotectedKeysProtector() + + /** + * Use the provided map of key-ids and passphrases to unlock keys. + * + * @param passphraseMap map of key ids and their respective passphrases + * @return protector + */ + @JvmStatic + fun fromPassphraseMap(passphraseMap: Map): SecretKeyRingProtector = + CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null) + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index 42562713..1cf55a4d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -63,13 +63,13 @@ public class MissingPassphraseForDecryptionTest { // interactive callback SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { // is called in interactive mode return Passphrase.fromPassword(passphrase); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return true; } }; @@ -95,13 +95,13 @@ public class MissingPassphraseForDecryptionTest { SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { fail("MUST NOT get called in non-interactive mode."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return true; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index c86a823c..914f477e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -120,13 +120,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void missingPassphraseFirst() throws PGPException, IOException { SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return false; } }); @@ -150,13 +150,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockEachKeyWith(p1, k1); SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return false; } }); @@ -178,13 +178,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void messagePassphraseFirst() throws PGPException, IOException { SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { fail("Since we provide a decryption passphrase, we should not try to decrypt any key."); return null; } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return false; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java index 3f7a9e6e..9869f7b5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java @@ -32,13 +32,13 @@ public class CachingSecretKeyRingProtectorTest { // Dummy passphrase callback that returns the doubled key-id as passphrase private final SecretKeyPassphraseProvider dummyCallback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { long doubled = keyId * 2; return Passphrase.fromPassword(Long.toString(doubled)); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return true; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index 45aaf20f..b5703eef 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -31,8 +31,8 @@ public class PassphraseProtectedKeyTest { new SecretKeyPassphraseProvider() { @Nullable @Override - public Passphrase getPassphraseFor(Long keyId) { - if (keyId.equals(TestKeys.CRYPTIE_KEY_ID)) { + public Passphrase getPassphraseFor(long keyId) { + if (keyId == TestKeys.CRYPTIE_KEY_ID) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { return null; @@ -40,8 +40,8 @@ public class PassphraseProtectedKeyTest { } @Override - public boolean hasPassphrase(Long keyId) { - return keyId.equals(TestKeys.CRYPTIE_KEY_ID); + public boolean hasPassphrase(long keyId) { + return keyId == TestKeys.CRYPTIE_KEY_ID; } }); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index a5030f74..b6781355 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -108,12 +108,12 @@ public class SecretKeyRingProtectorTest { CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(Long keyId) { + public Passphrase getPassphraseFor(long keyId) { return Passphrase.fromPassword("missingP455w0rd"); } @Override - public boolean hasPassphrase(Long keyId) { + public boolean hasPassphrase(long keyId) { return true; } }); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java index 5badf1f1..67386bde 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java @@ -88,19 +88,19 @@ public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector } @Override - public boolean hasPassphraseFor(Long keyId) { + public boolean hasPassphraseFor(long keyId) { return protector.hasPassphrase(keyId); } @Nullable @Override - public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { + public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { return protector.getDecryptor(keyId); } @Nullable @Override - public PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException { + public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { return protector.getEncryptor(keyId); } From a8bc929f2440d20722ba6d7b3d8a5316eb1a8867 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 17:26:13 +0200 Subject: [PATCH 099/351] Kotlin conversion: BaseSecretKeyRingProtector --- .../BaseSecretKeyRingProtector.java | 70 ------------------- .../protection/BaseSecretKeyRingProtector.kt | 40 +++++++++++ 2 files changed, 40 insertions(+), 70 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java deleted file mode 100644 index fead3938..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/BaseSecretKeyRingProtector.java +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -import javax.annotation.Nullable; - -/** - * Basic {@link SecretKeyRingProtector} implementation that respects the users {@link KeyRingProtectionSettings} when - * encrypting keys. - */ -public class BaseSecretKeyRingProtector implements SecretKeyRingProtector { - - private final SecretKeyPassphraseProvider passphraseProvider; - private final KeyRingProtectionSettings protectionSettings; - - /** - * Constructor that uses the given {@link SecretKeyPassphraseProvider} to retrieve passphrases and PGPainless' - * default {@link KeyRingProtectionSettings}. - * - * @param passphraseProvider provider for passphrases - */ - public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider) { - this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()); - } - - /** - * Constructor that uses the given {@link SecretKeyPassphraseProvider} and {@link KeyRingProtectionSettings}. - * - * @param passphraseProvider provider for passphrases - * @param protectionSettings protection settings - */ - public BaseSecretKeyRingProtector(SecretKeyPassphraseProvider passphraseProvider, KeyRingProtectionSettings protectionSettings) { - this.passphraseProvider = passphraseProvider; - this.protectionSettings = protectionSettings; - } - - @Override - public boolean hasPassphraseFor(long keyId) { - return passphraseProvider.hasPassphrase(keyId); - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { - Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); - return passphrase == null || passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { - Passphrase passphrase = passphraseProvider.getPassphraseFor(keyId); - return passphrase == null || passphrase.isEmpty() ? null : - ImplementationFactory.getInstance().getPBESecretKeyEncryptor( - protectionSettings.getEncryptionAlgorithm(), - protectionSettings.getHashAlgorithm(), - protectionSettings.getS2kCount(), - passphrase); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt new file mode 100644 index 00000000..450ca7f5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider + +/** + * Basic [SecretKeyRingProtector] implementation that respects the users [KeyRingProtectionSettings] when encrypting keys. + */ +open class BaseSecretKeyRingProtector( + private val passphraseProvider: SecretKeyPassphraseProvider, + private val protectionSettings: KeyRingProtectionSettings +) : SecretKeyRingProtector { + + constructor(passphraseProvider: SecretKeyPassphraseProvider): + this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()) + + override fun hasPassphraseFor(keyId: Long): Boolean = passphraseProvider.hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(it) + } + + override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + protectionSettings.encryptionAlgorithm, + protectionSettings.hashAlgorithm, + protectionSettings.s2kCount, + it) + } +} \ No newline at end of file From b125333c89e06eb05a8951baa04f23205f17d835 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 17:28:08 +0200 Subject: [PATCH 100/351] Kotlin conversion: UnprotectedKeysProtector --- .../protection/UnprotectedKeysProtector.java | 33 ------------------- .../protection/UnprotectedKeysProtector.kt | 15 +++++++++ 2 files changed, 15 insertions(+), 33 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java deleted file mode 100644 index 5d3f3361..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; - -/** - * Implementation of the {@link SecretKeyRingProtector} which assumes that all handled keys are not password protected. - */ -public class UnprotectedKeysProtector implements SecretKeyRingProtector { - - @Override - public boolean hasPassphraseFor(long keyId) { - return true; - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(long keyId) { - return null; - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(long keyId) { - return null; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt new file mode 100644 index 00000000..f87c6d3d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 +package org.pgpainless.key.protection + +/** + * Implementation of the [SecretKeyRingProtector] which assumes that all handled keys are not password protected. + */ +class UnprotectedKeysProtector : SecretKeyRingProtector { + override fun hasPassphraseFor(keyId: Long) = true + + override fun getDecryptor(keyId: Long) = null + + override fun getEncryptor(keyId: Long) = null +} \ No newline at end of file From b9c601b99676c29ed62f49e576201b491d91274c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 17:40:37 +0200 Subject: [PATCH 101/351] Kotlin conversion: PasswordBasedSecretKeyRingProtector --- .../PasswordBasedSecretKeyRingProtector.java | 78 ------------------- .../PasswordBasedSecretKeyRingProtector.kt | 63 +++++++++++++++ 2 files changed, 63 insertions(+), 78 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java deleted file mode 100644 index 0e387085..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Provides {@link PBESecretKeyDecryptor} and {@link PBESecretKeyEncryptor} objects while getting the passphrases - * from a {@link SecretKeyPassphraseProvider} and using settings from an {@link KeyRingProtectionSettings}. - */ -public class PasswordBasedSecretKeyRingProtector extends BaseSecretKeyRingProtector { - - public PasswordBasedSecretKeyRingProtector(@Nonnull SecretKeyPassphraseProvider passphraseProvider) { - super(passphraseProvider); - } - - /** - * Constructor. - * Passphrases for keys are sourced from the {@code passphraseProvider} and decryptors/encryptors are constructed - * following the settings given in {@code settings}. - * - * @param settings S2K settings etc. - * @param passphraseProvider provider which provides passphrases. - */ - public PasswordBasedSecretKeyRingProtector(@Nonnull KeyRingProtectionSettings settings, @Nonnull SecretKeyPassphraseProvider passphraseProvider) { - super(passphraseProvider, settings); - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPKeyRing keyRing, Passphrase passphrase) { - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Override - @Nullable - public Passphrase getPassphraseFor(long keyId) { - return hasPassphrase(keyId) ? passphrase : null; - } - - @Override - public boolean hasPassphrase(long keyId) { - return keyRing.getPublicKey(keyId) != null; - } - }; - return new PasswordBasedSecretKeyRingProtector(passphraseProvider); - } - - public static PasswordBasedSecretKeyRingProtector forKey(PGPSecretKey key, Passphrase passphrase) { - return forKeyId(key.getPublicKey().getKeyID(), passphrase); - } - - public static PasswordBasedSecretKeyRingProtector forKeyId(long singleKeyId, Passphrase passphrase) { - SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() { - @Nullable - @Override - public Passphrase getPassphraseFor(long keyId) { - if (keyId == singleKeyId) { - return passphrase; - } - return null; - } - - @Override - public boolean hasPassphrase(long keyId) { - return keyId == singleKeyId; - } - }; - return new PasswordBasedSecretKeyRingProtector(passphraseProvider); - } - -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt new file mode 100644 index 00000000..1b8df815 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.util.Passphrase + +/** + * Provides [PBESecretKeyDecryptor] and [PBESecretKeyEncryptor] objects while getting the passphrases + * from a [SecretKeyPassphraseProvider] and using settings from an [KeyRingProtectionSettings]. + */ +class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { + + constructor(passphraseProvider: SecretKeyPassphraseProvider): super(passphraseProvider) + + /** + * Constructor. + * Passphrases for keys are sourced from the `passphraseProvider` and decryptors/encryptors are constructed + * following the settings given in `settings`. + * + * @param settings S2K settings etc. + * @param passphraseProvider provider which provides passphrases. + */ + constructor(settings: KeyRingProtectionSettings, + passphraseProvider: SecretKeyPassphraseProvider): super(passphraseProvider, settings) + + companion object { + @JvmStatic + fun forKey(keyRing: PGPKeyRing, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector { + return object : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } + + override fun hasPassphrase(keyId: Long): Boolean { + return keyRing.getPublicKey(keyId) != null + } + }.let { PasswordBasedSecretKeyRingProtector(it) } + } + + @JvmStatic + fun forKey(key: PGPSecretKey, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector = + forKeyId(key.publicKey.keyID, passphrase) + + @JvmStatic + fun forKeyId(singleKeyId: Long, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector { + return object : SecretKeyPassphraseProvider { + override fun getPassphraseFor(keyId: Long): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } + + override fun hasPassphrase(keyId: Long): Boolean { + return keyId == singleKeyId + } + }.let { PasswordBasedSecretKeyRingProtector(it) } + } + } +} \ No newline at end of file From ee32d9e446187af2e8829ef2708325df52eb52a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 18:28:29 +0200 Subject: [PATCH 102/351] Kotlin conversion: CachingSecretKeyRingProtector --- .../CachingSecretKeyRingProtector.java | 222 ------------------ .../CachingSecretKeyRingProtector.kt | 174 ++++++++++++++ 2 files changed, 174 insertions(+), 222 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java deleted file mode 100644 index 0b4fe084..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyRingProtector} which holds a map of key ids and their passwords. - * In case the needed passphrase is not contained in the map, the {@code missingPassphraseCallback} will be consulted, - * and the passphrase is added to the map. - * - * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate - * {@link CachingSecretKeyRingProtector} instance for each ring. - */ -public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, SecretKeyPassphraseProvider { - - private final Map cache = new HashMap<>(); - private final SecretKeyRingProtector protector; - private final SecretKeyPassphraseProvider provider; - - public CachingSecretKeyRingProtector() { - this(null); - } - - public CachingSecretKeyRingProtector(@Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { - this( - new HashMap<>(), - KeyRingProtectionSettings.secureDefaultSettings(), - missingPassphraseCallback - ); - } - - public CachingSecretKeyRingProtector(@Nonnull Map passphrases, - @Nonnull KeyRingProtectionSettings protectionSettings, - @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { - this.cache.putAll(passphrases); - this.protector = new PasswordBasedSecretKeyRingProtector(protectionSettings, this); - this.provider = missingPassphraseCallback; - } - - /** - * Add a passphrase to the cache. - * If the cache already contains a passphrase for the given key-id, a {@link IllegalArgumentException} is thrown. - * The reason for this is to prevent accidental override of passphrases when dealing with multiple key rings - * containing a key with the same key-id but different passphrases. - * - * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, you can use - * {@link #replacePassphrase(long, Passphrase)} to replace the passphrase. - * - * @param keyId id of the key - * @param passphrase passphrase - */ - public void addPassphrase(long keyId, @Nonnull Passphrase passphrase) { - if (this.cache.containsKey(keyId)) { - throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" + - "If you want to replace the passphrase, use replacePassphrase(Long, Passphrase) instead."); - } - this.cache.put(keyId, passphrase); - } - - /** - * Replace the passphrase for the given key-id in the cache. - * - * @param keyId keyId - * @param passphrase passphrase - */ - public void replacePassphrase(long keyId, @Nonnull Passphrase passphrase) { - this.cache.put(keyId, passphrase); - } - - /** - * Remember the given passphrase for all keys in the given key ring. - * If for the key-id of any key on the key ring the cache already contains a passphrase, a - * {@link IllegalArgumentException} is thrown before any changes are committed to the cache. - * This is to prevent accidental passphrase override when dealing with multiple key rings containing - * keys with conflicting key-ids. - * - * If you can ensure that there will be no key-id clashes, and you want to replace the passphrases for the key ring, - * use {@link #replacePassphrase(PGPKeyRing, Passphrase)} instead. - * - * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate - * {@link CachingSecretKeyRingProtector} instance for each ring. - * - * @param keyRing key ring - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) { - Iterator keys = keyRing.getPublicKeys(); - // check for existing passphrases before doing anything - while (keys.hasNext()) { - long keyId = keys.next().getKeyID(); - if (cache.containsKey(keyId)) { - throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" + - "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead."); - } - } - - // only then insert - keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - addPassphrase(publicKey, passphrase); - } - } - - /** - * Replace the cached passphrases for all keys in the key ring with the provided passphrase. - * - * @param keyRing key ring - * @param passphrase passphrase - */ - public void replacePassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) { - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - replacePassphrase(publicKey.getKeyID(), passphrase); - } - } - - /** - * Remember the given passphrase for the given (sub-)key. - * - * @param key key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull PGPPublicKey key, @Nonnull Passphrase passphrase) { - addPassphrase(key.getKeyID(), passphrase); - } - - public void addPassphrase(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Passphrase passphrase) { - addPassphrase(fingerprint.getKeyId(), passphrase); - } - - /** - * Remove a passphrase from the cache. - * The passphrase will be cleared and then removed. - * - * @param keyId id of the key - */ - public void forgetPassphrase(long keyId) { - Passphrase passphrase = cache.remove(keyId); - if (passphrase != null) { - passphrase.clear(); - } - } - - /** - * Forget the passphrase to all keys in the provided key ring. - * - * @param keyRing key ring - */ - public void forgetPassphrase(@Nonnull PGPKeyRing keyRing) { - Iterator keys = keyRing.getPublicKeys(); - while (keys.hasNext()) { - PGPPublicKey publicKey = keys.next(); - forgetPassphrase(publicKey); - } - } - - /** - * Forget the passphrase of the given public key. - * - * @param key key - */ - public void forgetPassphrase(@Nonnull PGPPublicKey key) { - forgetPassphrase(key.getKeyID()); - } - - @Override - @Nullable - public Passphrase getPassphraseFor(long keyId) { - Passphrase passphrase = cache.get(keyId); - if (passphrase == null || !passphrase.isValid()) { - if (provider == null) { - return null; - } - passphrase = provider.getPassphraseFor(keyId); - if (passphrase != null) { - cache.put(keyId, passphrase); - } - } - return passphrase; - } - - @Override - public boolean hasPassphrase(long keyId) { - Passphrase passphrase = cache.get(keyId); - return passphrase != null && passphrase.isValid(); - } - - @Override - public boolean hasPassphraseFor(long keyId) { - return hasPassphrase(keyId); - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { - return protector.getEncryptor(keyId); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt new file mode 100644 index 00000000..4451aa0f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider +import org.pgpainless.key.util.KeyIdUtil +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyRingProtector] which holds a map of key ids and their passwords. + * In case the needed passphrase is not contained in the map, the `missingPassphraseCallback` will be consulted, + * and the passphrase is added to the map. + * + * If you need to unlock multiple [PGPKeyRing] instances, it is advised to use a separate + * [CachingSecretKeyRingProtector] instance for each ring. + */ +class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphraseProvider { + + private val cache: MutableMap + private val protector: SecretKeyRingProtector + private val provider: SecretKeyPassphraseProvider? + + constructor(): this(null) + + constructor(missingPassphraseCallback: SecretKeyPassphraseProvider?): this( + mapOf(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback) + + constructor(passphrases: Map, + protectionSettings: KeyRingProtectionSettings, + missingPassphraseCallback: SecretKeyPassphraseProvider?) { + this.cache = passphrases.toMutableMap() + this.protector = PasswordBasedSecretKeyRingProtector(protectionSettings, this) + this.provider = missingPassphraseCallback + } + + /** + * Add a passphrase to the cache. + * If the cache already contains a passphrase for the given key-id, a [IllegalArgumentException] is thrown. + * The reason for this is to prevent accidental override of passphrases when dealing with multiple key rings + * containing a key with the same key-id but different passphrases. + * + * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, you can use + * [replacePassphrase] to replace the passphrase. + * + * @param keyId id of the key + * @param passphrase passphrase + */ + fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { + require(!cache.containsKey(keyId)) { + "The cache already holds a passphrase for ID ${KeyIdUtil.formatKeyId(keyId)}.\n" + + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." + } + cache[keyId] = passphrase + } + + /** + * Replace the passphrase for the given key-id in the cache. + * + * @param keyId keyId + * @param passphrase passphrase + */ + fun replacePassphrase(keyId: Long, passphrase: Passphrase) = apply { + cache[keyId] = passphrase + } + + /** + * Remember the given passphrase for all keys in the given key ring. + * If for the key-id of any key on the key ring the cache already contains a passphrase, a + * [IllegalArgumentException] is thrown before any changes are committed to the cache. + * This is to prevent accidental passphrase override when dealing with multiple key rings containing + * keys with conflicting key-ids. + * + * If you can ensure that there will be no key-id clashes, and you want to replace the passphrases for the key ring, + * use [replacePassphrase] instead. + * + * If you need to unlock multiple [PGPKeyRing], it is advised to use a separate [CachingSecretKeyRingProtector] + * instance for each ring. + * + * @param keyRing key ring + * @param passphrase passphrase + */ + fun addPassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { + // check for existing passphrases before doing anything + keyRing.publicKeys.forEach { + require(!cache.containsKey(it.keyID)) { + "The cache already holds a passphrase for the key with ID ${KeyIdUtil.formatKeyId(it.keyID)}.\n" + + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." + } + } + + // only then instert + keyRing.publicKeys.forEach { + cache[it.keyID] = passphrase + } + } + + /** + * Replace the cached passphrases for all keys in the key ring with the provided passphrase. + * + * @param keyRing key ring + * @param passphrase passphrase + */ + fun replacePassphrase(keyRing: PGPKeyRing, passphrase: Passphrase) = apply { + keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } + } + + /** + * Remember the given passphrase for the given (sub-)key. + * + * @param key key + * @param passphrase passphrase + */ + fun addPassphrase(key: PGPPublicKey, passphrase: Passphrase) = + addPassphrase(key.keyID, passphrase) + + /** + * Remember the given passphrase for the key with the given fingerprint. + * + * @param fingerprint fingerprint + * @param passphrase passphrase + */ + fun addPassphrase(fingerprint: OpenPgpFingerprint, passphrase: Passphrase) = + addPassphrase(fingerprint.keyId, passphrase) + + /** + * Remove a passphrase from the cache. + * The passphrase will be cleared and then removed. + * + * @param keyId id of the key + */ + fun forgetPassphrase(keyId: Long) = apply { + cache.remove(keyId)?.clear() + } + + /** + * Forget the passphrase to all keys in the provided key ring. + * + * @param keyRing key ring + */ + fun forgetPassphrase(keyRing: PGPKeyRing) = apply { + keyRing.publicKeys.forEach { forgetPassphrase(it) } + } + + /** + * Forget the passphrase of the given public key. + * + * @param key key + */ + fun forgetPassphrase(key: PGPPublicKey) = apply { + forgetPassphrase(key.keyID) + } + + override fun getPassphraseFor(keyId: Long): Passphrase? { + return if (hasPassphrase(keyId)) + cache[keyId] + else + provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } + } + + override fun hasPassphrase(keyId: Long) = cache[keyId]?.isValid ?: false + + override fun hasPassphraseFor(keyId: Long) = hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long) = protector.getDecryptor(keyId) + + override fun getEncryptor(keyId: Long) = protector.getEncryptor(keyId) +} \ No newline at end of file From 2547f1c687c5dc57ed27a55e56425bbb79b601f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 18:28:48 +0200 Subject: [PATCH 103/351] Kotlin conversion: KeyRingProtectionSettings --- .../protection/KeyRingProtectionSettings.java | 99 ------------------- .../protection/KeyRingProtectionSettings.kt | 56 +++++++++++ 2 files changed, 56 insertions(+), 99 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java deleted file mode 100644 index a93534ab..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/KeyRingProtectionSettings.java +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import javax.annotation.Nonnull; - -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -/** - * Secret key protection settings for iterated and salted S2K. - */ -public class KeyRingProtectionSettings { - - private final SymmetricKeyAlgorithm encryptionAlgorithm; - private final HashAlgorithm hashAlgorithm; - private final int s2kCount; - - /** - * Create a {@link KeyRingProtectionSettings} object using the given encryption algorithm, SHA1 and - * 65536 iterations. - * - * @param encryptionAlgorithm encryption algorithm - */ - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60); // Same s2kCount (encoded) as used in BC. - } - - /** - * Constructor for custom salted and iterated S2K protection settings. - * The salt gets randomly chosen by the library each time. - * - * Note, that the s2kCount is the already encoded single-octet number. - * - * @see Encoding Formula - * - * @param encryptionAlgorithm encryption algorithm - * @param hashAlgorithm hash algorithm - * @param s2kCount encoded s2k iteration count - */ - public KeyRingProtectionSettings(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, int s2kCount) { - this.encryptionAlgorithm = validateEncryptionAlgorithm(encryptionAlgorithm); - this.hashAlgorithm = hashAlgorithm; - if (s2kCount < 1) { - throw new IllegalArgumentException("s2kCount cannot be less than 1."); - } - this.s2kCount = s2kCount; - } - - private static SymmetricKeyAlgorithm validateEncryptionAlgorithm(SymmetricKeyAlgorithm encryptionAlgorithm) { - switch (encryptionAlgorithm) { - case NULL: - throw new IllegalArgumentException("Unencrypted is not allowed here!"); - default: - return encryptionAlgorithm; - } - } - - /** - * Secure default settings using {@link SymmetricKeyAlgorithm#AES_256}, {@link HashAlgorithm#SHA256} - * and an iteration count of 65536. - * - * @return secure protection settings - */ - public static KeyRingProtectionSettings secureDefaultSettings() { - return new KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60); - } - - /** - * Return the encryption algorithm. - * - * @return encryption algorithm - */ - public @Nonnull SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return encryptionAlgorithm; - } - - /** - * Return the hash algorithm. - * - * @return hash algorithm - */ - public @Nonnull HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - - /** - * Return the (encoded!) s2k iteration count. - * - * @see Encoding Formula - * - * @return encoded s2k count - */ - public int getS2kCount() { - return s2kCount; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt new file mode 100644 index 00000000..6158d322 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * Secret key protection settings for iterated and salted S2K. + * The salt gets randomly chosen by the library each time. + * Note, that the s2kCount is the already encoded single-octet number. + * + * @see Encoding Formula + * + * @param encryptionAlgorithm encryption algorithm + * @param hashAlgorithm hash algorithm + * @param s2kCount encoded (!) s2k iteration count + */ +data class KeyRingProtectionSettings( + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val hashAlgorithm: HashAlgorithm, + val s2kCount: Int +) { + + /** + * Create a [KeyRingProtectionSettings] object using the given encryption algorithm, [HashAlgorithm.SHA1] and + * 65536 iterations. + * It is okay to use SHA1 here, since we don't care about collisions. + * + * @param encryptionAlgorithm encryption algorithm + */ + constructor(encryptionAlgorithm: SymmetricKeyAlgorithm): this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60) + + init { + require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { + "Unencrypted is not allowed here!" + } + require(s2kCount > 0) { + "s2kCount cannot be less than 1." + } + } + + companion object { + + /** + * Secure default settings using [SymmetricKeyAlgorithm.AES_256], [HashAlgorithm.SHA256] + * and an iteration count of 65536. + * + * @return secure protection settings + */ + @JvmStatic + fun secureDefaultSettings() = KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60) + } +} \ No newline at end of file From 873db12125187d16161dc913686b81d59e38aa67 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Aug 2023 18:45:09 +0200 Subject: [PATCH 104/351] Kotlin conversion: UnlockSecretKey --- .../key/protection/UnlockSecretKey.java | 67 ------------------ .../OpenPgpMessageInputStream.kt | 2 +- .../key/protection/UnlockSecretKey.kt | 68 +++++++++++++++++++ 3 files changed, 69 insertions(+), 68 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java deleted file mode 100644 index 154a1aa1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2021 Paul Schaub. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.KeyIntegrityException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.util.PublicKeyParameterValidationUtil; -import org.pgpainless.util.Passphrase; - -public final class UnlockSecretKey { - - private UnlockSecretKey() { - - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) - throws PGPException, KeyIntegrityException { - - PBESecretKeyDecryptor decryptor = null; - if (KeyInfo.isEncrypted(secretKey)) { - decryptor = protector.getDecryptor(secretKey.getKeyID()); - } - PGPPrivateKey privateKey = unlockSecretKey(secretKey, decryptor); - return privateKey; - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, PBESecretKeyDecryptor decryptor) - throws PGPException { - PGPPrivateKey privateKey; - try { - privateKey = secretKey.extractPrivateKey(decryptor); - } catch (PGPException e) { - throw new WrongPassphraseException(secretKey.getKeyID(), e); - } - - if (privateKey == null) { - int s2kType = secretKey.getS2K().getType(); - if (s2kType >= 100 && s2kType <= 110) { - throw new PGPException("Cannot decrypt secret key" + Long.toHexString(secretKey.getKeyID()) + ": " + - "Unsupported private S2K usage type " + s2kType); - } - - throw new PGPException("Cannot decrypt secret key."); - } - - if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { - PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); - } - - return privateKey; - } - - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) - throws PGPException, KeyIntegrityException { - return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith( - passphrase == null ? Passphrase.emptyPassphrase() : passphrase, secretKey)); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index a255e90e..1f34ec23 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -350,7 +350,7 @@ class OpenPgpMessageInputStream( } LOGGER.debug("Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") - val protector = options.getSecretKeyProtector(decryptionKeys) + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt new file mode 100644 index 00000000..533b33a7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -0,0 +1,68 @@ +// Copyright 2023 Paul Schaub. +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.PGPainless +import org.pgpainless.exception.KeyIntegrityException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.info.KeyInfo +import org.pgpainless.key.util.KeyIdUtil +import org.pgpainless.key.util.PublicKeyParameterValidationUtil +import org.pgpainless.util.Passphrase +import kotlin.jvm.Throws + +class UnlockSecretKey { + + companion object { + + @JvmStatic + @Throws(PGPException::class, KeyIntegrityException::class) + fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { + return if (KeyInfo.isEncrypted(secretKey)) { + unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyID)) + } else { + unlockSecretKey(secretKey, null as PBESecretKeyDecryptor?) + } + } + + @JvmStatic + @Throws(PGPException::class) + fun unlockSecretKey(secretKey: PGPSecretKey, decryptor: PBESecretKeyDecryptor?): PGPPrivateKey { + val privateKey = try { + secretKey.extractPrivateKey(decryptor) + } catch (e : PGPException) { + throw WrongPassphraseException(secretKey.keyID, e) + } + + if (privateKey == null) { + if (secretKey.s2K.type in 100..110) { + throw PGPException("Cannot decrypt secret key ${KeyIdUtil.formatKeyId(secretKey.keyID)}: \n" + + "Unsupported private S2K type ${secretKey.s2K.type}") + } + throw PGPException("Cannot decrypt secret key.") + } + + if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.publicKey) + } + + return privateKey + } + + @JvmStatic + fun unlockSecretKey(secretKey: PGPSecretKey, passphrase: Passphrase?): PGPPrivateKey { + return if (passphrase == null) { + unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()) + } else { + unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)) + } + } + } +} \ No newline at end of file From 5cb6d6e41d62e66af797176dfb155716eea2661c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 1 Sep 2023 13:15:22 +0200 Subject: [PATCH 105/351] Kotlin conversion: S2KUsageFix --- .../key/protection/fixes/S2KUsageFix.java | 92 ------------------- .../key/protection/fixes/package-info.java | 8 -- .../key/protection/fixes/S2KUsageFix.kt | 79 ++++++++++++++++ 3 files changed, 79 insertions(+), 100 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java deleted file mode 100644 index 24fe533a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/S2KUsageFix.java +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.fixes; - -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -/** - * Repair class to fix keys which use S2K usage of value {@link SecretKeyPacket#USAGE_CHECKSUM}. - * The method {@link #replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing, SecretKeyRingProtector)} ensures - * that such keys are encrypted using S2K usage {@link SecretKeyPacket#USAGE_SHA1} instead. - * - * @see Related PGPainless Bug Report - * @see Related PGPainless Feature Request - * @see Related upstream BC bug report - */ -public final class S2KUsageFix { - - private S2KUsageFix() { - - } - - /** - * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. - * This method fixes the private keys by changing them to
USAGE_SHA1
instead. - * - * @param keys keys - * @param protector protector to unlock and re-lock affected private keys - * @return fixed key ring - * @throws PGPException in case of a PGP error. - */ - public static PGPSecretKeyRing replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing keys, SecretKeyRingProtector protector) throws PGPException { - return replaceUsageChecksumWithUsageSha1(keys, protector, false); - } - - /** - * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. - * This method fixes the private keys by changing them to
USAGE_SHA1
instead. - * - * @param keys keys - * @param protector protector to unlock and re-lock affected private keys - * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will cause the subkey to stay unaffected. - * @return fixed key ring - * @throws PGPException in case of a PGP error. - */ - public static PGPSecretKeyRing replaceUsageChecksumWithUsageSha1(PGPSecretKeyRing keys, - SecretKeyRingProtector protector, - boolean skipKeysWithMissingPassphrase) throws PGPException { - PGPDigestCalculator digestCalculator = ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1); - for (PGPSecretKey key : keys) { - // CHECKSUM is not recommended - if (key.getS2KUsage() != SecretKeyPacket.USAGE_CHECKSUM) { - continue; - } - - long keyId = key.getKeyID(); - PBESecretKeyEncryptor encryptor = protector.getEncryptor(keyId); - if (encryptor == null) { - if (skipKeysWithMissingPassphrase) { - continue; - } - throw new WrongPassphraseException("Missing passphrase for key with ID " + Long.toHexString(keyId)); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(key, protector); - // This constructor makes use of USAGE_SHA1 by default - PGPSecretKey fixedKey = new PGPSecretKey( - privateKey, - key.getPublicKey(), - digestCalculator, - key.isMasterKey(), - protector.getEncryptor(keyId) - ); - - // replace the original key with the fixed one - keys = PGPSecretKeyRing.insertSecretKey(keys, fixedKey); - } - return keys; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java deleted file mode 100644 index 06c299b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/fixes/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Secret Key Protection Fixes. - */ -package org.pgpainless.key.protection.fixes; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt new file mode 100644 index 00000000..aeef0654 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.fixes + +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey + +/** + * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. + * The method [replaceUsageChecksumWithUsageSha1] ensures that such keys are encrypted using + * S2K usage [SecretKeyPacket.USAGE_SHA1] instead. + * + * @see Related PGPainless Bug Report + * @see Related PGPainless Feature Request + * @see Related upstream BC bug report + */ +class S2KUsageFix { + + companion object { + + /** + * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. + * This method fixes the private keys by changing them to
USAGE_SHA1
instead. + * + * @param keys keys + * @param protector protector to unlock and re-lock affected private keys + * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will cause the subkey to stay unaffected. + * @return fixed key ring + * @throws PGPException in case of a PGP error. + */ + @JvmStatic + @JvmOverloads + fun replaceUsageChecksumWithUsageSha1( + keys: PGPSecretKeyRing, + protector: SecretKeyRingProtector, + skipKeysWithMissingPassphrase: Boolean = false + ): PGPSecretKeyRing { + val digestCalculator = ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1) + val keyList = mutableListOf() + for (key in keys) { + // CHECKSUM is not recommended + if (key.s2KUsage != SecretKeyPacket.USAGE_CHECKSUM) { + keyList.add(key) + continue + } + + val keyId = key.keyID + val encryptor = protector.getEncryptor(keyId) + if (encryptor == null) { + if (skipKeysWithMissingPassphrase) { + keyList.add(key) + continue + } + throw WrongPassphraseException("Missing passphrase for key with ID " + java.lang.Long.toHexString(keyId)) + } + + val privateKey = unlockSecretKey(key, protector) + // This constructor makes use of USAGE_SHA1 by default + val fixedKey = PGPSecretKey( + privateKey, + key.publicKey, + digestCalculator, + key.isMasterKey, + protector.getEncryptor(keyId) + ) + keyList.add(fixedKey) + } + return PGPSecretKeyRing(keyList) + } + } +} \ No newline at end of file From e3f51fbf564b786c3a7e601179f8b73cdc15c840 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 1 Sep 2023 13:43:04 +0200 Subject: [PATCH 106/351] Kotlin conversion: SecretKeyPassphraseProvider and subclasses This commit also adds a workaround to build.gradle which enables proper Java interop for Kotlin interfaces with default implementations --- build.gradle | 7 ++++ .../key/protection/package-info.java | 8 ---- .../MapBasedPassphraseProvider.java | 42 ------------------- .../SecretKeyPassphraseProvider.java | 40 ------------------ .../SolitaryPassphraseProvider.java | 33 --------------- .../passphrase_provider/package-info.java | 8 ---- .../MapBasedPassphraseProvider.kt | 21 ++++++++++ .../SecretKeyPassphraseProvider.kt | 39 +++++++++++++++++ .../SolitaryPassphraseProvider.kt | 17 ++++++++ 9 files changed, 84 insertions(+), 131 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt diff --git a/build.gradle b/build.gradle index 540037a8..57cc04c4 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,13 @@ allprojects { fileMode = 0644 } + // Compatibility of default implementations in kotlin interfaces with Java implementations. + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + freeCompilerArgs += ["-Xjvm-default=all-compatibility"] + } + } + project.ext { rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java deleted file mode 100644 index b936025f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP secret key password protection. - */ -package org.pgpainless.key.protection; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java deleted file mode 100644 index 3f0ee2f9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import java.util.Map; -import javax.annotation.Nullable; - -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyPassphraseProvider} that holds a map of different {@link Passphrase passphrases}. - * It will return the right passphrase depending on the key-id. - * - * Note: This provider might return null! - * TODO: Make this null-safe and throw an exception instead? - */ -public class MapBasedPassphraseProvider implements SecretKeyPassphraseProvider { - - private final Map map; - - /** - * Create a new map based passphrase provider. - * - * @param passphraseMap map of key-ids and passphrases - */ - public MapBasedPassphraseProvider(Map passphraseMap) { - this.map = passphraseMap; - } - - @Nullable - @Override - public Passphrase getPassphraseFor(long keyId) { - return map.get(keyId); - } - - @Override - public boolean hasPassphrase(long keyId) { - return map.containsKey(keyId); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java deleted file mode 100644 index 91c2bc95..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPSecretKey; -import org.pgpainless.util.Passphrase; - -/** - * Interface to allow the user to provide a {@link Passphrase} for an encrypted OpenPGP secret key. - */ -public interface SecretKeyPassphraseProvider { - - /** - * Return a passphrase for the given secret key. - * If no record is found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with - * a content of null. - * - * @param secretKey secret key - * @return passphrase or null, if no passphrase record is found. - */ - @Nullable default Passphrase getPassphraseFor(PGPSecretKey secretKey) { - return getPassphraseFor(secretKey.getKeyID()); - } - /** - * Return a passphrase for the given key. If no record has been found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a {@link Passphrase} with - * a content of null. - * - * @param keyId if of the secret key - * @return passphrase or null, if no passphrase record has been found. - */ - @Nullable Passphrase getPassphraseFor(long keyId); - - boolean hasPassphrase(long keyId); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java deleted file mode 100644 index 9400229b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.protection.passphrase_provider; - -import javax.annotation.Nullable; - -import org.pgpainless.util.Passphrase; - -/** - * Implementation of the {@link SecretKeyPassphraseProvider} that holds a single {@link Passphrase}. - */ -public class SolitaryPassphraseProvider implements SecretKeyPassphraseProvider { - - private final Passphrase passphrase; - - public SolitaryPassphraseProvider(Passphrase passphrase) { - this.passphrase = passphrase; - } - - @Nullable - @Override - public Passphrase getPassphraseFor(long keyId) { - // always return the same passphrase. - return passphrase; - } - - @Override - public boolean hasPassphrase(long keyId) { - return true; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java deleted file mode 100644 index e70ad81f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Passphrase Provider classes. - */ -package org.pgpainless.key.protection.passphrase_provider; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt new file mode 100644 index 00000000..22260126 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyPassphraseProvider] that holds a map of key-IDs and respective [Passphrase]. + * It will return the right passphrase depending on the key-id. + * + * Note: This provider might return null! + * TODO: Make this null-safe and throw an exception instead? + */ +class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long): Passphrase? = map[keyId] + + override fun hasPassphrase(keyId: Long): Boolean = map.containsKey(keyId) +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt new file mode 100644 index 00000000..aaf24b70 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.util.Passphrase + +/** + * Interface to allow the user to provide a [Passphrase] for an encrypted OpenPGP secret key. + */ +interface SecretKeyPassphraseProvider { + + /** + * Return a passphrase for the given secret key. + * If no record is found, return null. + * Note: In case of an unprotected secret key, this method must may not return null, but a [Passphrase] with + * a content of null. + * + * @param secretKey secret key + * @return passphrase or null, if no passphrase record is found. + */ + fun getPassphraseFor(secretKey: PGPSecretKey): Passphrase? { + return getPassphraseFor(secretKey.keyID) + } + + /** + * Return a passphrase for the given key. If no record has been found, return null. + * Note: In case of an unprotected secret key, this method must may not return null, but a [Passphrase] with + * a content of null. + * + * @param keyId if of the secret key + * @return passphrase or null, if no passphrase record has been found. + */ + fun getPassphraseFor(keyId: Long): Passphrase? + + fun hasPassphrase(keyId: Long): Boolean +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt new file mode 100644 index 00000000..46f77342 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.protection.passphrase_provider + +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. + */ +class SolitaryPassphraseProvider(val passphrase: Passphrase?) : SecretKeyPassphraseProvider { + + override fun getPassphraseFor(keyId: Long): Passphrase? = passphrase + + override fun hasPassphrase(keyId: Long): Boolean = true +} \ No newline at end of file From bbd956dbb7c7eb2796df5661682f0a9e8aeebed1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 1 Sep 2023 14:29:48 +0200 Subject: [PATCH 107/351] Kotlin conversion: KeyRingReader --- .../pgpainless/key/parsing/KeyRingReader.java | 415 ------------------ .../pgpainless/key/parsing/package-info.java | 8 - .../pgpainless/key/parsing/KeyRingReader.kt | 333 ++++++++++++++ 3 files changed, 333 insertions(+), 423 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java deleted file mode 100644 index 2074e730..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ /dev/null @@ -1,415 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.parsing; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPMarker; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.collection.PGPKeyRingCollection; -import org.pgpainless.util.ArmorUtils; - -public class KeyRingReader { - - public static final int MAX_ITERATIONS = 10000; - - @SuppressWarnings("CharsetObjectCanBeUsed") - public static final Charset UTF8 = Charset.forName("UTF-8"); - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * - * @param inputStream inputStream containing the OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - @Nullable - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) - throws IOException { - return readKeyRing(inputStream); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * byte array. - * - * @param bytes byte array containing the OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - @Nullable - public PGPKeyRing keyRing(@Nonnull byte[] bytes) - throws IOException { - return keyRing(new ByteArrayInputStream(bytes)); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * ASCII armored string. - * - * @param asciiArmored ASCII armored OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - @Nullable - public PGPKeyRing keyRing(@Nonnull String asciiArmored) - throws IOException { - return keyRing(asciiArmored.getBytes(UTF8)); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRing(inputStream); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) - throws IOException { - return publicKeyRing(new ByteArrayInputStream(bytes)); - } - - @Nullable - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) - throws IOException { - return publicKeyRing(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRingCollection(inputStream); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) - throws IOException { - return publicKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - @Nonnull - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) - throws IOException { - return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRing(inputStream); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) - throws IOException { - return secretKeyRing(new ByteArrayInputStream(bytes)); - } - - @Nullable - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) - throws IOException { - return secretKeyRing(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRingCollection(inputStream); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) - throws IOException { - return secretKeyRingCollection(new ByteArrayInputStream(bytes)); - } - - @Nonnull - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) - throws IOException { - return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); - } - - @Nonnull - public PGPKeyRingCollection keyRingCollection(@Nonnull InputStream inputStream, boolean isSilent) - throws IOException, PGPException { - return readKeyRingCollection(inputStream, isSilent); - } - - @Nonnull - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) - throws IOException, PGPException { - return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); - } - - @Nonnull - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) - throws IOException, PGPException { - return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * This method will attempt to read at most {@link #MAX_ITERATIONS} objects from the stream before aborting. - * The first {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} will be returned. - * - * @param inputStream inputStream containing the OpenPGP key or certificate - * @return key ring - * @throws IOException in case of an IO error - */ - @Nullable - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a {@link PGPKeyRing} (either {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing}) from the given - * {@link InputStream}. - * This method will attempt to read at most
maxIterations
objects from the stream before aborting. - * The first {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} will be returned. - * - * @param inputStream inputStream containing the OpenPGP key or certificate - * @param maxIterations maximum number of objects that are read before the method will abort - * @return key ring - * @throws IOException in case of an IO error - */ - @Nullable - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - return (PGPSecretKeyRing) next; - } - if (next instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) next; - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - @Nullable - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a public key ring from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before a {@link PGPPublicKeyRing} is read, - * an {@link IOException} is thrown. - * - * @param inputStream input stream - * @param maxIterations max iterations before abort - * @return public key ring - * - * @throws IOException in case of an IO error or exceeding of max iterations - */ - @Nullable - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) next; - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - @Nonnull - public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); - } - - /** - * Read a public key ring collection from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an {@link IOException} is thrown. - * If the stream contain secret key packets, their public key parts are extracted and returned. - * - * @param inputStream input stream - * @param maxIterations max iterations before abort - * @return public key ring collection - * - * @throws IOException in case of an IO error or exceeding of max iterations - */ - @Nonnull - public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - - List rings = new ArrayList<>(); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return new PGPPublicKeyRingCollection(rings); - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPPublicKeyRing) { - rings.add((PGPPublicKeyRing) next); - continue; - } - // Parse public keys from secret keys - if (next instanceof PGPSecretKeyRing) { - rings.add(PGPainless.extractCertificate((PGPSecretKeyRing) next)); - continue; - } - if (next instanceof PGPPublicKeyRingCollection) { - PGPPublicKeyRingCollection collection = (PGPPublicKeyRingCollection) next; - Iterator iterator = collection.getKeyRings(); - while (iterator.hasNext()) { - rings.add(iterator.next()); - } - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - @Nullable - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRing(inputStream, MAX_ITERATIONS); - } - - /** - * Read a secret key ring from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before a {@link PGPSecretKeyRing} is read, - * an {@link IOException} is thrown. - * - * @param inputStream input stream - * @param maxIterations max iterations before abort - * @return public key ring - * - * @throws IOException in case of an IO error or exceeding of max iterations - */ - @Nullable - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) - throws IOException { - InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return null; - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - Streams.drain(decoderStream); - return (PGPSecretKeyRing) next; - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - @Nonnull - public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException { - return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); - } - - /** - * Read a secret key ring collection from the provided {@link InputStream}. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an {@link IOException} is thrown. - * - * @param inputStream input stream - * @param maxIterations max iterations before abort - * @return secret key ring collection - * - * @throws IOException in case of an IO error or exceeding of max iterations - */ - @Nonnull - public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, - int maxIterations) - throws IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)); - - List rings = new ArrayList<>(); - int i = 0; - Object next; - do { - next = objectFactory.nextObject(); - if (next == null) { - return new PGPSecretKeyRingCollection(rings); - } - if (next instanceof PGPMarker) { - continue; - } - if (next instanceof PGPSecretKeyRing) { - rings.add((PGPSecretKeyRing) next); - } - if (next instanceof PGPSecretKeyRingCollection) { - PGPSecretKeyRingCollection collection = (PGPSecretKeyRingCollection) next; - Iterator iterator = collection.getKeyRings(); - while (iterator.hasNext()) { - rings.add(iterator.next()); - } - } - } while (++i < maxIterations); - - throw new IOException("Loop exceeded max iteration count."); - } - - @Nonnull - public static PGPKeyRingCollection readKeyRingCollection(@Nonnull InputStream inputStream, boolean isSilent) - throws IOException, PGPException { - return new PGPKeyRingCollection(inputStream, isSilent); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java deleted file mode 100644 index 50030499..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP key reading. - */ -package org.pgpainless.key.parsing; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt new file mode 100644 index 00000000..388294d5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.parsing + +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.collection.PGPKeyRingCollection +import org.pgpainless.util.ArmorUtils +import java.io.IOException +import java.io.InputStream +import java.nio.charset.Charset +import kotlin.jvm.Throws + +class KeyRingReader { + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given [InputStream]. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(inputStream: InputStream): PGPKeyRing? = + readKeyRing(inputStream) + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given byte array. + * + * @param bytes byte array containing the OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(bytes: ByteArray): PGPKeyRing? = + keyRing(bytes.inputStream()) + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given + * ASCII armored string. + * + * @param asciiArmored ASCII armored OpenPGP key or certificate + * @return key ring + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) + fun keyRing(asciiArmored: String): PGPKeyRing? = + keyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun publicKeyRing(inputStream: InputStream): PGPPublicKeyRing? = + readPublicKeyRing(inputStream) + + @Throws(IOException::class) + fun publicKeyRing(bytes: ByteArray): PGPPublicKeyRing? = + publicKeyRing(bytes.inputStream()) + + @Throws(IOException::class) + fun publicKeyRing(asciiArmored: String): PGPPublicKeyRing? = + publicKeyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun publicKeyRingCollection(inputStream: InputStream): PGPPublicKeyRingCollection = + readPublicKeyRingCollection(inputStream) + + @Throws(IOException::class) + fun publicKeyRingCollection(bytes: ByteArray): PGPPublicKeyRingCollection = + publicKeyRingCollection(bytes.inputStream()) + + @Throws(IOException::class) + fun publicKeyRingCollection(asciiArmored: String): PGPPublicKeyRingCollection = + publicKeyRingCollection(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun secretKeyRing(inputStream: InputStream): PGPSecretKeyRing? = + readSecretKeyRing(inputStream) + + @Throws(IOException::class) + fun secretKeyRing(bytes: ByteArray): PGPSecretKeyRing? = + secretKeyRing(bytes.inputStream()) + + @Throws(IOException::class) + fun secretKeyRing(asciiArmored: String): PGPSecretKeyRing? = + secretKeyRing(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun secretKeyRingCollection(inputStream: InputStream): PGPSecretKeyRingCollection = + readSecretKeyRingCollection(inputStream) + + @Throws(IOException::class) + fun secretKeyRingCollection(bytes: ByteArray): PGPSecretKeyRingCollection = + secretKeyRingCollection(bytes.inputStream()) + + @Throws(IOException::class) + fun secretKeyRingCollection(asciiArmored: String): PGPSecretKeyRingCollection = + secretKeyRingCollection(asciiArmored.toByteArray(UTF8)) + + @Throws(IOException::class) + fun keyRingCollection(inptStream: InputStream, isSilent: Boolean): PGPKeyRingCollection = + readKeyRingCollection(inptStream, isSilent) + + @Throws(IOException::class) + fun keyRingCollection(bytes: ByteArray, isSilent: Boolean): PGPKeyRingCollection = + keyRingCollection(bytes.inputStream(), isSilent) + + @Throws(IOException::class) + fun keyRingCollection(asciiArmored: String, isSilent: Boolean): PGPKeyRingCollection = + keyRingCollection(asciiArmored.toByteArray(UTF8), isSilent) + + companion object { + private const val MAX_ITERATIONS = 10000 + + @JvmStatic + val UTF8: Charset = charset("UTF8") + + + /** + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given [InputStream]. + * This method will attempt to read at most
maxIterations
objects from the stream before aborting. + * The first [PGPPublicKeyRing] or [PGPSecretKeyRing] will be returned. + * + * @param inputStream inputStream containing the OpenPGP key or certificate + * @param maxIterations maximum number of objects that are read before the method will abort + * @return key ring + * @throws IOException in case of an IO error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readKeyRing(inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS): PGPKeyRing? { + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + ArmorUtils.getDecoderStream(inputStream)) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + return next + } + if (next is PGPPublicKeyRing) { + return next + } + continue + } + } catch (e : PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a public key ring from the provided [InputStream]. + * If more than maxIterations PGP packets are encountered before a [PGPPublicKeyRing] is read, + * an [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring + * + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readPublicKeyRing(inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS): PGPPublicKeyRing? { + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + ArmorUtils.getDecoderStream(inputStream)) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPPublicKeyRing) { + return next + } + continue + } + } catch (e : PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a public key ring collection from the provided [InputStream]. + * If more than maxIterations PGP packets are encountered before the stream is exhausted, + * an [IOException] is thrown. + * If the stream contain secret key packets, their public key parts are extracted and returned. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring collection + * + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readPublicKeyRingCollection(inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS): PGPPublicKeyRingCollection { + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + ArmorUtils.getDecoderStream(inputStream)) + val certificates = mutableListOf() + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPPublicKeyRing) { + certificates.add(next) + continue + } + if (next is PGPSecretKeyRing) { + certificates.add(PGPainless.extractCertificate(next)) + continue + } + if (next is PGPPublicKeyRingCollection) { + certificates.addAll(next) + continue + } + } + } catch (e : PGPRuntimeOperationException) { + throw e.cause!! + } + return PGPPublicKeyRingCollection(certificates) + } + + /** + * Read a secret key ring from the provided [InputStream]. + * If more than maxIterations PGP packets are encountered before a [PGPSecretKeyRing] is read, + * an [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return public key ring + * + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readSecretKeyRing(inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS): PGPSecretKeyRing? { + val decoderStream = ArmorUtils.getDecoderStream(inputStream) + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream) + + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + Streams.drain(decoderStream) + return next + } + } + } catch (e : PGPRuntimeOperationException) { + throw e.cause!! + } + return null + } + + /** + * Read a secret key ring collection from the provided [InputStream]. + * If more than maxIterations PGP packets are encountered before the stream is exhausted, + * an [IOException] is thrown. + * + * @param inputStream input stream + * @param maxIterations max iterations before abort + * @return secret key ring collection + * + * @throws IOException in case of an IO error or exceeding of max iterations + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun readSecretKeyRingCollection(inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS): PGPSecretKeyRingCollection { + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + ArmorUtils.getDecoderStream(inputStream)) + + val secretKeys = mutableListOf() + try { + for ((i, next) in objectFactory.withIndex()) { + if (i >= maxIterations) { + throw IOException("Loop exceeded max iteration count.") + } + if (next is PGPMarker) { + continue + } + if (next is PGPSecretKeyRing) { + secretKeys.add(next) + continue + } + if (next is PGPSecretKeyRingCollection) { + secretKeys.addAll(next) + continue + } + } + } catch (e : PGPRuntimeOperationException) { + throw e.cause!! + } + return PGPSecretKeyRingCollection(secretKeys) + } + + @JvmStatic + @Throws(IOException::class) + fun readKeyRingCollection(inputStream: InputStream, isSilent: Boolean): PGPKeyRingCollection = + PGPKeyRingCollection(inputStream, isSilent) + } + +} \ No newline at end of file From e9caa4af1f615c69196ab6aba8a584357f3797e6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 1 Sep 2023 18:23:12 +0200 Subject: [PATCH 108/351] Kotlin conversion: EncryptionBuilder + Interface --- .../encryption_signing/EncryptionBuilder.java | 77 ------------------- .../EncryptionBuilderInterface.java | 38 --------- .../encryption_signing/EncryptionBuilder.kt | 59 ++++++++++++++ .../EncryptionBuilderInterface.kt | 36 +++++++++ 4 files changed, 95 insertions(+), 115 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt 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 deleted file mode 100644 index 9490551b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilder.java +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator; -import org.pgpainless.key.SubkeyIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class EncryptionBuilder implements EncryptionBuilderInterface { - - private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionBuilder.class); - - private OutputStream outputStream; - - @Override - public WithOptions onOutputStream(@Nonnull OutputStream outputStream) { - this.outputStream = outputStream; - return new WithOptionsImpl(); - } - - class WithOptionsImpl implements WithOptions { - @Override - public EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException { - if (options == null) { - throw new NullPointerException("ProducerOptions cannot be null."); - } - return new EncryptionStream(outputStream, options); - } - } - - /** - * Negotiate the {@link SymmetricKeyAlgorithm} used for message encryption. - * - * @param encryptionOptions encryption options - * @return negotiated symmetric key algorithm - */ - public static SymmetricKeyAlgorithm negotiateSymmetricEncryptionAlgorithm(EncryptionOptions encryptionOptions) { - List> preferences = new ArrayList<>(); - for (SubkeyIdentifier key : encryptionOptions.getKeyViews().keySet()) { - preferences.add(encryptionOptions.getKeyViews().get(key).getPreferredSymmetricKeyAlgorithms()); - } - - SymmetricKeyAlgorithm algorithm = SymmetricKeyAlgorithmNegotiator - .byPopularity() - .negotiate( - PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy(), - encryptionOptions.getEncryptionAlgorithmOverride(), - preferences); - LOGGER.debug("Negotiation resulted in {} being the symmetric encryption algorithm of choice.", algorithm); - return algorithm; - } - - 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 deleted file mode 100644 index c705c0b1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionBuilderInterface.java +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.IOException; -import java.io.OutputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; - -public interface EncryptionBuilderInterface { - - /** - * Create a {@link EncryptionStream} on an {@link OutputStream} that contains the plain data that - * shall be encrypted and or signed. - * - * @param outputStream output stream of the plain data. - * @return api handle - */ - WithOptions onOutputStream(@Nonnull OutputStream outputStream); - - interface WithOptions { - - /** - * Create an {@link EncryptionStream} with the given options (recipients, signers, algorithms...). - * - * @param options options - * @return encryption stream - * - * @throws PGPException if something goes wrong during encryption stream preparation - * @throws IOException if something goes wrong during encryption stream preparation (writing headers) - */ - EncryptionStream withOptions(ProducerOptions options) throws PGPException, IOException; - - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt new file mode 100644 index 00000000..324fcf3b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.pgpainless.PGPainless.Companion.getPolicy +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.OutputStream + +class EncryptionBuilder : EncryptionBuilderInterface { + override fun onOutputStream(outputStream: OutputStream): EncryptionBuilderInterface.WithOptions { + return WithOptionsImpl(outputStream) + } + + class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions { + + override fun withOptions(options: ProducerOptions): EncryptionStream { + return EncryptionStream(outputStream, options) + } + } + + companion object { + + @JvmStatic + val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java) + + /** + * Negotiate the [SymmetricKeyAlgorithm] used for message encryption. + * + * @param encryptionOptions encryption options + * @return negotiated symmetric key algorithm + */ + @JvmStatic + fun negotiateSymmetricEncryptionAlgorithm(encryptionOptions: EncryptionOptions): SymmetricKeyAlgorithm { + val preferences = encryptionOptions.keyViews.values + .map { it.preferredSymmetricKeyAlgorithms } + .toList() + val algorithm = byPopularity().negotiate( + getPolicy().symmetricKeyEncryptionAlgorithmPolicy, + encryptionOptions.encryptionAlgorithmOverride, + preferences) + LOGGER.debug("Negotiation resulted in {} being the symmetric encryption algorithm of choice.", algorithm) + return algorithm + } + + @JvmStatic + fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm { + val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride + return compressionAlgorithmOverride ?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm() + + // TODO: Negotiation + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt new file mode 100644 index 00000000..2d42c68a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.PGPException +import java.io.IOException +import java.io.OutputStream + +fun interface EncryptionBuilderInterface { + + /** + * Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the + * [EncryptionStream] will be encrypted and/or signed. + * + * @param outputStream output stream which receives the encrypted / signed data. + * @return api handle + */ + fun onOutputStream(outputStream: OutputStream): WithOptions + + fun interface WithOptions { + + /** + * Create an [EncryptionStream] with the given options (recipients, signers, algorithms...). + * + * @param options options + * @return encryption stream + * + * @throws PGPException if something goes wrong during encryption stream preparation + * @throws IOException if something goes wrong during encryption stream preparation (writing headers) + */ + @Throws(PGPException::class, IOException::class) + fun withOptions(options: ProducerOptions): EncryptionStream + } +} \ No newline at end of file From f3ea9f62e18a4f5518e25ae8a37c25aeec180ea3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 3 Sep 2023 17:55:46 +0200 Subject: [PATCH 109/351] Kotlin conversion: EncryptionOptions --- .../encryption_signing/EncryptionOptions.java | 473 ------------------ .../encryption_signing/EncryptionOptions.kt | 294 +++++++++++ .../EncryptionOptionsTest.java | 9 +- 3 files changed, 300 insertions(+), 476 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt 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 deleted file mode 100644 index bb937a1a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -import org.pgpainless.exception.KeyException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyAccessor; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; - -/** - * Options for the encryption process. - * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc. - *

- * A typical use might look like follows: - *

- * {@code
- * EncryptionOptions opt = new EncryptionOptions();
- * opt.addRecipient(aliceKey, "Alice ");
- * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
- * }
- * 
- *

- * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}. - * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm - * by inspecting the provided recipient keys. - *

- * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate. - * This behavior can be changed per recipient, e.g. by calling - *

- * {@code
- * opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
- * }
- * 
- * when adding the recipient key. - */ -public class EncryptionOptions { - - private final EncryptionPurpose purpose; - 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 EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys(); - private boolean allowEncryptionWithMissingKeyFlags = false; - private Date evaluationDate = new Date(); - - private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; - - /** - * Encrypt to keys both carrying the key flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} - * or {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - */ - public EncryptionOptions() { - this(EncryptionPurpose.ANY); - } - - public EncryptionOptions(@Nonnull EncryptionPurpose purpose) { - this.purpose = purpose; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or - * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag. - *

- * Use this if you are not sure. - * - * @return encryption options - */ - public static EncryptionOptions get() { - return new EncryptionOptions(); - } - - /** - * Override the evaluation date for recipient keys with the given date. - * - * @param evaluationDate new evaluation date - * @return this - */ - public EncryptionOptions setEvaluationDate(@Nonnull Date evaluationDate) { - this.evaluationDate = evaluationDate; - return this; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. - * - * @return encryption options - */ - public static EncryptionOptions encryptCommunications() { - return new EncryptionOptions(EncryptionPurpose.COMMUNICATIONS); - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * @return encryption options - */ - public static EncryptionOptions encryptDataAtRest() { - return new EncryptionOptions(EncryptionPurpose.STORAGE); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for - * identifiable bindings. - * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. - * @param userId userId - * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address - * @param authority certificate authority - * @return encryption options - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority) { - return addAuthenticatableRecipients(userId, email, authority, 120); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for - * identifiable bindings. - * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. - * @param userId userId - * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address - * @param authority certificate authority - * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) - * @return encryption options - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.lookupByUserId(userId, email, evaluationDate, targetAmount); - boolean foundAcceptable = false; - for (CertificateAuthenticity candidate : identifiedCertificates) { - if (candidate.isAuthenticated()) { - addRecipient(candidate.getCertificate()); - foundAcceptable = true; - } - } - if (!foundAcceptable) { - throw new IllegalArgumentException("Could not identify any trust-worthy certificates for '" + userId + "' and target trust amount " + targetAmount); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * - * @param keys keys - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull Iterable keys) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - for (PGPPublicKeyRing key : keys) { - addRecipient(key); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * Per key ring, the selector is applied to select one or more encryption subkeys. - * - * @param keys keys - * @param selector encryption key selector - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull Iterable keys, @Nonnull EncryptionKeySelector selector) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - for (PGPPublicKeyRing key : keys) { - addRecipient(key, selector); - } - return this; - } - - /** - * 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 - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) { - return addRecipient(key, userId, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and recipient user-id, as well as a strategy for selecting one or multiple - * encryption capable subkeys from the key. - * - * @param key key - * @param userId user-id - * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, - @Nonnull CharSequence userId, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, evaluationDate); - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose)); - if (encryptionSubkeys.isEmpty()) { - throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString())); - addRecipientKey(key, encryptionSubkey, false); - } - - return this; - } - - /** - * Add a recipient by providing a key. - * - * @param key key ring - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) { - return addRecipient(key, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and an encryption key selection strategy. - * - * @param key key ring - * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys. - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, false); - } - - /** - * Add a certificate as hidden recipient. - * The recipients key-id will be obfuscated by setting a wildcard key ID. - * - * @param key recipient key - * @return this - */ - public EncryptionOptions addHiddenRecipient(@Nonnull PGPPublicKeyRing key) { - return addHiddenRecipient(key, encryptionKeySelector); - } - - /** - * Add a certificate as hidden recipient, using the provided {@link EncryptionKeySelector} to select recipient subkeys. - * The recipients key-ids will be obfuscated by setting a wildcard key ID instead. - * - * @param key recipient key - * @param encryptionKeySelectionStrategy strategy to select recipient (sub) keys. - * @return this - */ - public EncryptionOptions addHiddenRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, true); - } - - private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) { - KeyRingInfo info = new KeyRingInfo(key, evaluationDate); - - Date primaryKeyExpiration; - try { - primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); - } catch (NoSuchElementException e) { - throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)); - } - if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) { - throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration); - } - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); - - // There are some legacy keys around without key flags. - // If we allow encryption for those keys, we add valid keys without any key flags, if they are - // capable of encryption by means of their algorithm - if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { - List validSubkeys = info.getValidSubkeys(); - for (PGPPublicKey validSubkey : validSubkeys) { - if (!validSubkey.isEncryptionKey()) { - continue; - } - // only add encryption keys with no key flags. - if (info.getKeyFlagsOf(validSubkey.getKeyID()).isEmpty()) { - encryptionSubkeys.add(validSubkey); - } - } - } - - if (encryptionSubkeys.isEmpty()) { - throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId)); - addRecipientKey(key, encryptionSubkey, wildcardKeyId); - } - - return this; - } - - private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing, - @Nonnull PGPPublicKey key, - boolean wildcardKeyId) { - encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); - PublicKeyKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); - encryptionMethod.setUseWildcardKeyID(wildcardKeyId); - addEncryptionMethod(encryptionMethod); - } - - /** - * Add a symmetric passphrase which the message will be encrypted to. - * - * @param passphrase passphrase - * @return this - */ - public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) { - if (passphrase.isEmpty()) { - throw new IllegalArgumentException("Passphrase must not be empty."); - } - PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); - return 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 - * @return this - */ - public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) { - encryptionMethods.add(encryptionMethod); - return this; - } - - Set getEncryptionMethods() { - return new HashSet<>(encryptionMethods); - } - - Map getKeyRingInfo() { - return new HashMap<>(keyRingInfo); - } - - Set getEncryptionKeyIdentifiers() { - return new HashSet<>(encryptionKeys); - } - - Map getKeyViews() { - return new HashMap<>(keyViews); - } - - SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() { - return encryptionAlgorithmOverride; - } - - /** - * Override the used symmetric encryption algorithm. - * The symmetric encryption algorithm is used to encrypt the message itself, - * while the used symmetric key will be encrypted to all recipients using public key - * cryptography. - * - * If the algorithm is not overridden, a suitable algorithm will be negotiated. - * - * @param encryptionAlgorithm encryption algorithm override - * @return this - */ - public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); - } - this.encryptionAlgorithmOverride = encryptionAlgorithm; - return this; - } - - /** - * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption - * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. - * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm - * type to convey the subkeys use. - * - * @return this - */ - public EncryptionOptions setAllowEncryptionWithMissingKeyFlags() { - this.allowEncryptionWithMissingKeyFlags = true; - return this; - } - - /** - * Return

true
iff the user specified at least one encryption method, - *
false
otherwise. - * - * @return encryption methods is not empty - */ - public boolean hasEncryptionMethod() { - return !encryptionMethods.isEmpty(); - } - - public interface EncryptionKeySelector { - List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys); - } - - /** - * Only encrypt to the first valid encryption capable subkey we stumble upon. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToFirstSubkey() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { - return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0)); - } - }; - } - - /** - * Encrypt to any valid, encryption capable subkey on the key ring. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToAllCapableSubkeys() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { - return encryptionCapableKeys; - } - }; - } - - // TODO: Create encryptToBestSubkey() method -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt new file mode 100644 index 00000000..6a8c0125 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -0,0 +1,294 @@ +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator +import org.pgpainless.algorithm.EncryptionPurpose +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.info.KeyAccessor +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.util.Passphrase +import java.util.* +import javax.annotation.Nonnull + + +class EncryptionOptions( + private val purpose: EncryptionPurpose +) { + private val _encryptionMethods: MutableSet = mutableSetOf() + private val _encryptionKeyIdentifiers: MutableSet = mutableSetOf() + private val _keyRingInfo: MutableMap = mutableMapOf() + private val _keyViews: MutableMap = mutableMapOf() + private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys() + + private var allowEncryptionWithMissingKeyFlags = false + private var evaluationDate = Date() + private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null + + val encryptionMethods + get() = _encryptionMethods.toSet() + val encryptionKeyIdentifiers + get() = _encryptionKeyIdentifiers.toSet() + val keyRingInfo + get() = _keyRingInfo.toMap() + val keyViews + get() = _keyViews.toMap() + val encryptionAlgorithmOverride + get() = _encryptionAlgorithmOverride + + constructor(): this(EncryptionPurpose.ANY) + + /** + * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys + * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * + * @return encryption options + */ + fun setEvaluationDate(evaluationDate: Date) = apply { + this.evaluationDate = evaluationDate + } + + /** + * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for + * identifiable bindings. + * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. + * @param userId userId + * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address + * @param authority certificate authority + * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return encryption options + */ + @JvmOverloads + fun addAuthenticatableRecipients(userId: String, email: Boolean, authority: CertificateAuthority, targetAmount: Int = 120) = apply { + var foundAcceptable = false + authority.lookupByUserId(userId, email, evaluationDate, targetAmount) + .filter { it.isAuthenticated() } + .forEach { addRecipient(it.certificate) + .also { + foundAcceptable = true + } + } + require(foundAcceptable) { + "Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount." + } + } + + /** + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. + * + * @param keys keys + * @return this + */ + fun addRecipients(keys: Iterable) = apply { + keys.toList().let { + require(it.isNotEmpty()) { + "Set of recipient keys cannot be empty." + } + it.forEach { key -> addRecipient(key) } + } + } + + /** + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. + * Per key ring, the selector is applied to select one or more encryption subkeys. + * + * @param keys keys + * @param selector encryption key selector + * @return this + */ + fun addRecipients(keys: Iterable, selector: EncryptionKeySelector) = apply { + keys.toList().let { + require(it.isNotEmpty()) { + "Set of recipient keys cannot be empty." + } + it.forEach { key -> addRecipient(key, selector) } + } + } + + /** + * Add a recipient by providing a key. + * + * @param key key ring + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector) + + /** + * 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 + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = + addRecipient(key, userId, encryptionKeySelector) + + fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence, encryptionKeySelector: EncryptionKeySelector) = apply { + val info = KeyRingInfo(key, evaluationDate) + val subkeys = encryptionKeySelector.selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)) + if (subkeys.isEmpty()) { + throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in subkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + (_keyRingInfo as MutableMap)[keyId] = info + (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) + addRecipientKey(key, subkey, false) + } + } + + fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply { + addAsRecipient(key, encryptionKeySelector, false) + } + + @JvmOverloads + fun addHiddenRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector = encryptionKeySelector) = apply { + addAsRecipient(key, selector, true) + } + + private fun addAsRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector, wildcardKeyId: Boolean) = apply { + val info = KeyRingInfo(key, evaluationDate) + val primaryKeyExpiration = try { + info.primaryKeyExpirationDate + } catch (e: NoSuchElementException) { + throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) + } + + if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { + throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) + } + + var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) + + // There are some legacy keys around without key flags. + // If we allow encryption for those keys, we add valid keys without any key flags, if they are + // capable of encryption by means of their algorithm + if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { + encryptionSubkeys = info.validSubkeys + .filter { it.isEncryptionKey } + .filter { info.getKeyFlagsOf(it.keyID).isEmpty() } + } + + if (encryptionSubkeys.isEmpty()) { + throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in encryptionSubkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + (_keyRingInfo as MutableMap)[keyId] = info + (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaKeyId(info, keyId) + addRecipientKey(key, subkey, wildcardKeyId) + } + } + + private fun addRecipientKey(certificate: PGPPublicKeyRing, + key: PGPPublicKey, + wildcardKeyId: Boolean) { + (_encryptionKeyIdentifiers as MutableSet).add(SubkeyIdentifier(certificate, key.keyID)) + addEncryptionMethod(ImplementationFactory.getInstance() + .getPublicKeyKeyEncryptionMethodGenerator(key) + .also { it.setUseWildcardKeyID(wildcardKeyId) }) + } + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + * @return this + */ + fun addPassphrase(passphrase: Passphrase) = apply { + require(!passphrase.isEmpty) { + "Passphrase MUST NOT be empty." + } + addEncryptionMethod(ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) + } + + /** + * 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 + * @return this + */ + fun addEncryptionMethod(encryptionMethod: PGPKeyEncryptionMethodGenerator) = apply { + (_encryptionMethods as MutableSet).add(encryptionMethod) + } + + /** + * Override the used symmetric encryption algorithm. + * The symmetric encryption algorithm is used to encrypt the message itself, + * while the used symmetric key will be encrypted to all recipients using public key + * cryptography. + * + * If the algorithm is not overridden, a suitable algorithm will be negotiated. + * + * @param encryptionAlgorithm encryption algorithm override + * @return this + */ + fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { + require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { + "Encryption algorithm override cannot be NULL." + } + _encryptionAlgorithmOverride = encryptionAlgorithm + } + + /** + * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption + * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. + * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm + * type to convey the subkeys use. + * + * @return this + */ + fun setAllowEncryptionWithMissingKeyFlags() = apply { + this.allowEncryptionWithMissingKeyFlags = true + } + + fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() + + + fun interface EncryptionKeySelector { + fun selectEncryptionSubkeys(encryptionCapableKeys: List): List + } + + companion object { + @JvmStatic + fun get() = EncryptionOptions() + + @JvmStatic + fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) + + @JvmStatic + fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) + + /** + * Only encrypt to the first valid encryption capable subkey we stumble upon. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToFirstSubkey() = EncryptionKeySelector { encryptionCapableKeys -> + encryptionCapableKeys.firstOrNull()?.let { listOf(it) } ?: listOf() } + + /** + * Encrypt to any valid, encryption capable subkey on the key ring. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToAllCapableSubkeys() = EncryptionKeySelector { encryptionCapableKeys -> encryptionCapableKeys } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 7d2fa453..87dab34a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -22,6 +23,7 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -122,7 +124,7 @@ public class EncryptionOptionsTest { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), - encryptionCapableKeys -> encryptionCapableKeys)); + ArrayList::new)); } @Test @@ -150,8 +152,9 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { + @NotNull @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -159,7 +162,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } })); From 0e6a146594b15be22c46bf267ca2986d6eb7f770 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 13:44:04 +0200 Subject: [PATCH 110/351] Add missing license header --- .../org/pgpainless/encryption_signing/EncryptionOptions.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index 6a8c0125..74d2b3ca 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.encryption_signing import org.bouncycastle.openpgp.PGPPublicKey From d075ed66373ba845fa48c6397c2b894c1e6bdb1a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 13:44:18 +0200 Subject: [PATCH 111/351] Kotlin conversion: PGPKeyRingCollection --- .../key/collection/PGPKeyRingCollection.java | 114 ------------------ .../key/collection/package-info.java | 8 -- .../key/collection/PGPKeyRingCollection.kt | 98 +++++++++++++++ .../collection/PGPKeyRingCollectionTest.java | 4 +- 4 files changed, 100 insertions(+), 124 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java deleted file mode 100644 index 6cf7102d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.collection; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPMarker; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmorUtils; - -/** - * This class describes a logic of handling a collection of different {@link PGPKeyRing}. The logic was inspired by - * {@link PGPSecretKeyRingCollection} and {@link PGPPublicKeyRingCollection}. - */ -public class PGPKeyRingCollection { - - private final PGPSecretKeyRingCollection pgpSecretKeyRingCollection; - private final PGPPublicKeyRingCollection pgpPublicKeyRingCollection; - - public PGPKeyRingCollection(@Nonnull byte[] encoding, boolean isSilent) throws IOException, PGPException { - this(new ByteArrayInputStream(encoding), isSilent); - } - - /** - * Build a {@link PGPKeyRingCollection} from the passed in input stream. - * - * @param in input stream containing data - * @param isSilent flag indicating that unsupported objects will be ignored - * @throws IOException if a problem parsing the base stream occurs - * @throws PGPException if an object is encountered which isn't a {@link PGPSecretKeyRing} or {@link PGPPublicKeyRing} - */ - public PGPKeyRingCollection(@Nonnull InputStream in, boolean isSilent) throws IOException, PGPException { - // Double getDecoderStream because of #96 - InputStream decoderStream = ArmorUtils.getDecoderStream(in); - PGPObjectFactory pgpFact = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); - Object obj; - - List secretKeyRings = new ArrayList<>(); - List publicKeyRings = new ArrayList<>(); - - while ((obj = pgpFact.nextObject()) != null) { - if (obj instanceof PGPMarker) { - // Skip marker packets - continue; - } - if (obj instanceof PGPSecretKeyRing) { - secretKeyRings.add((PGPSecretKeyRing) obj); - } else if (obj instanceof PGPPublicKeyRing) { - publicKeyRings.add((PGPPublicKeyRing) obj); - } else if (!isSilent) { - throw new PGPException(obj.getClass().getName() + " found where " + - PGPSecretKeyRing.class.getSimpleName() + " or " + - PGPPublicKeyRing.class.getSimpleName() + " expected"); - } - } - - pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(secretKeyRings); - pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(publicKeyRings); - } - - public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws PGPException { - List secretKeyRings = new ArrayList<>(); - List publicKeyRings = new ArrayList<>(); - - for (PGPKeyRing pgpKeyRing : collection) { - if (pgpKeyRing instanceof PGPSecretKeyRing) { - secretKeyRings.add((PGPSecretKeyRing) pgpKeyRing); - } else if (pgpKeyRing instanceof PGPPublicKeyRing) { - publicKeyRings.add((PGPPublicKeyRing) pgpKeyRing); - } else if (!isSilent) { - throw new PGPException(pgpKeyRing.getClass().getName() + " found where " + - PGPSecretKeyRing.class.getSimpleName() + " or " + - PGPPublicKeyRing.class.getSimpleName() + " expected"); - } - } - - pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(secretKeyRings); - pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(publicKeyRings); - } - - public @Nonnull PGPSecretKeyRingCollection getPGPSecretKeyRingCollection() { - return pgpSecretKeyRingCollection; - } - - public @Nonnull PGPPublicKeyRingCollection getPgpPublicKeyRingCollection() { - return pgpPublicKeyRingCollection; - } - - /** - * Return the number of rings in this collection. - * - * @return total size of {@link PGPSecretKeyRingCollection} and {@link PGPPublicKeyRingCollection} - * in this collection - */ - public int size() { - return pgpSecretKeyRingCollection.size() + pgpPublicKeyRingCollection.size(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java deleted file mode 100644 index b2f5b153..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * OpenPGP key collections. - */ -package org.pgpainless.key.collection; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt new file mode 100644 index 00000000..63151a4c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub , 2021 Flowcrypt a.s. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.collection + +import org.bouncycastle.openpgp.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmorUtils +import java.io.InputStream + +/** + * This class describes a logic of handling a collection of different [PGPKeyRing]. The logic was inspired by + * [PGPSecretKeyRingCollection] and [PGPPublicKeyRingCollection]. + */ +class PGPKeyRingCollection( + val pgpSecretKeyRingCollection: PGPSecretKeyRingCollection, + val pgpPublicKeyRingCollection: PGPPublicKeyRingCollection +) { + + constructor(encoding: ByteArray, isSilent: Boolean): this(encoding.inputStream(), isSilent) + + constructor(inputStream: InputStream, isSilent: Boolean): this(parse(inputStream, isSilent)) + + constructor(collection: Collection, isSilent: Boolean): this(segment(collection, isSilent)) + + private constructor(arguments: Pair): this(arguments.first, arguments.second) + + /** + * The number of rings in this collection. + * + * @return total size of [PGPSecretKeyRingCollection] plus [PGPPublicKeyRingCollection] in this collection + */ + val size: Int + get() = pgpSecretKeyRingCollection.size() + pgpPublicKeyRingCollection.size() + + fun size() = size + + @Deprecated("Wrong case of PGP -> Pgp", ReplaceWith("getPgpSecretKeyRingCollection()")) + fun getPGPSecretKeyRingCollection() = pgpSecretKeyRingCollection + + companion object { + @JvmStatic + private fun parse(inputStream: InputStream, isSilent: Boolean): Pair { + val secretKeyRings = mutableListOf() + val certificates = mutableListOf() + // Double getDecoderStream because of #96 + val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + + for (obj in objectFactory) { + if (obj == null) { + break + } + + if (obj is PGPMarker) { + // Skip marker packets + continue + } + + if (obj is PGPSecretKeyRing) { + secretKeyRings.add(obj) + continue + } + + if (obj is PGPPublicKeyRing) { + certificates.add(obj) + continue + } + + if (!isSilent) { + throw PGPException("${obj.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + " or ${PGPPublicKeyRing::class.java.simpleName} expected") + } + } + + return PGPSecretKeyRingCollection(secretKeyRings) to PGPPublicKeyRingCollection(certificates) + } + + @JvmStatic + private fun segment(collection: Collection, isSilent: Boolean): Pair { + val secretKeyRings = mutableListOf() + val certificates = mutableListOf() + + for (keyRing in collection) { + if (keyRing is PGPSecretKeyRing) { + secretKeyRings.add(keyRing) + } else if (keyRing is PGPPublicKeyRing) { + certificates.add(keyRing) + } else if (!isSilent) { + throw PGPException("${keyRing.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + " or ${PGPPublicKeyRing::class.java.simpleName} expected") + } + } + + return PGPSecretKeyRingCollection(secretKeyRings) to PGPPublicKeyRingCollection(certificates) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java index 89b0fccf..fd5530ba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/collection/PGPKeyRingCollectionTest.java @@ -52,7 +52,7 @@ public class PGPKeyRingCollectionTest { // silent = true -> No exception, but not keys either PGPKeyRingCollection collection = new PGPKeyRingCollection(bytes, true); assertEquals(0, collection.getPgpPublicKeyRingCollection().size()); - assertEquals(0, collection.getPGPSecretKeyRingCollection().size()); + assertEquals(0, collection.getPgpSecretKeyRingCollection().size()); } @Test @@ -63,7 +63,7 @@ public class PGPKeyRingCollectionTest { Collection keys = Arrays.asList(first, second, secondPub); PGPKeyRingCollection collection = new PGPKeyRingCollection(keys, true); - assertEquals(2, collection.getPGPSecretKeyRingCollection().size()); + assertEquals(2, collection.getPgpSecretKeyRingCollection().size()); assertEquals(1, collection.getPgpPublicKeyRingCollection().size()); } } From 44c22f9044231d5042b75df6aefb2da1d3d02af9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 14:30:50 +0200 Subject: [PATCH 112/351] Kotlin conversion: KeyIdUtil This PR also introduces LongExtensions.kt which provides extension methods to parse Long from Hex KeyIDs and to format Longs as Hex KeyIDs. --- .../org/pgpainless/key/util/KeyIdUtil.java | 37 ------------------- .../src/main/kotlin/_kotlin/LongExtensions.kt | 25 +++++++++++++ .../org/pgpainless/key/util/KeyIdUtil.kt | 36 ++++++++++++++++++ 3 files changed, 61 insertions(+), 37 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java create mode 100644 pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java deleted file mode 100644 index 78e03f71..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.math.BigInteger; -import java.util.regex.Pattern; - -public final class KeyIdUtil { - - private KeyIdUtil() { - - } - - private static final Pattern LONG_KEY_ID = Pattern.compile("^[0-9A-Fa-f]{16}$"); - - /** - * Convert a long key-id into a key-id. - * A long key-id is a 16 digit hex string. - * - * @param longKeyId 16-digit hexadecimal string - * @return key-id converted to {@link Long}. - */ - public static long fromLongKeyId(String longKeyId) { - if (!LONG_KEY_ID.matcher(longKeyId).matches()) { - throw new IllegalArgumentException("Provided long key-id does not match expected format. " + - "A long key-id consists of 16 hexadecimal characters."); - } - - return new BigInteger(longKeyId, 16).longValue(); - } - - public static String formatKeyId(long keyId) { - return String.format("%016X", keyId); - } -} diff --git a/pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt b/pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt new file mode 100644 index 00000000..b2c64e2e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package _kotlin + +/** + * Format this Long as a 16 digit uppercase hex number. + */ +fun Long.hexKeyId(): String { + return String.format("%016X", this).uppercase() +} + +/** + * Parse a 16 digit hex number into a Long. + */ +fun Long.Companion.fromHexKeyId(hexKeyId: String): Long { + require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { + "Provided long key-id does not match expected format. " + + "A long key-id consists of 16 hexadecimal characters." + } + // Calling toLong() only fails with a NumberFormatException. + // Therefore, we call toULong(16).toLong(), which seems to work. + return hexKeyId.toULong(16).toLong() +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt new file mode 100644 index 00000000..b6ff1789 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import _kotlin.fromHexKeyId +import _kotlin.hexKeyId + +class KeyIdUtil { + + companion object { + + /** + * Convert a long key-id into a key-id. + * A long key-id is a 16 digit hex string. + * + * @param longKeyId 16-digit hexadecimal string + * @return key-id converted to {@link Long}. + */ + @JvmStatic + @Deprecated("Superseded by Long extension method.", + ReplaceWith("Long.fromHexKeyId(longKeyId)")) + fun fromLongKeyId(longKeyId: String) = Long.fromHexKeyId(longKeyId) + + /** + * Format a long key-ID as upper-case hex string. + * @param keyId keyId + * @return hex encoded key ID + */ + @JvmStatic + @Deprecated("Superseded by Long extension method.", + ReplaceWith("keyId.hexKeyId()")) + fun formatKeyId(keyId: Long) = keyId.hexKeyId() + } +} \ No newline at end of file From ab42a7503f247522f0c99d1deff128827bcb8cfd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 14:37:18 +0200 Subject: [PATCH 113/351] Replace usage of KeyIdUtil.formatKeyId() in Kotlin classes with Long.hexKeyId() --- .../OpenPgpMessageInputStream.kt | 18 +++++++++--------- .../org/pgpainless/key/SubkeyIdentifier.kt | 6 +++--- .../CachingSecretKeyRingProtector.kt | 6 +++--- .../key/protection/UnlockSecretKey.kt | 4 ++-- .../subpackets/SignatureSubpacketsUtil.kt | 4 ++-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 1f34ec23..3c056cfa 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification +import _kotlin.hexKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException import org.bouncycastle.openpgp.* @@ -21,7 +22,6 @@ import org.pgpainless.exception.* import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.protection.UnlockSecretKey -import org.pgpainless.key.util.KeyIdUtil import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.signature.SignatureUtils @@ -180,7 +180,7 @@ class OpenPgpMessageInputStream( private fun processOnePassSignature() { syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) val ops = packetInputStream!!.readOnePassSignature() - LOGGER.debug("One-Pass-Signature Packet by key ${KeyIdUtil.formatKeyId(ops.keyID)} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("One-Pass-Signature Packet by key ${ops.keyID.hexKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addOnePassSignature(ops) } @@ -197,11 +197,11 @@ class OpenPgpMessageInputStream( val keyId = SignatureUtils.determineIssuerKeyId(signature) if (isSigForOps) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${keyId.hexKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) } else { - LOGGER.debug("Prepended Signature Packet by key ${KeyIdUtil.formatKeyId(keyId)} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("Prepended Signature Packet by key ${keyId.hexKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addPrependedSignature(signature) } } @@ -282,10 +282,10 @@ class OpenPgpMessageInputStream( // try (known) secret keys for (pkesk in esks.pkesks) { val keyId = pkesk.keyID - LOGGER.debug("Encountered PKESK for recipient ${KeyIdUtil.formatKeyId(keyId)}") + LOGGER.debug("Encountered PKESK for recipient ${keyId.hexKeyId()}") val decryptionKeys = getDecryptionKey(keyId) if (decryptionKeys == null) { - LOGGER.debug("Skipping PKESK because no matching key ${KeyIdUtil.formatKeyId(keyId)} was provided.") + LOGGER.debug("Skipping PKESK because no matching key ${keyId.hexKeyId()} was provided.") continue } val secretKey = decryptionKeys.getSecretKey(keyId) @@ -618,7 +618,7 @@ class OpenPgpMessageInputStream( if (check != null) { detachedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key."))) @@ -631,7 +631,7 @@ class OpenPgpMessageInputStream( if (check != null) { prependedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key") @@ -693,7 +693,7 @@ class OpenPgpMessageInputStream( } if (!found) { - LOGGER.debug("No suitable certificate for verification of signature by key ${KeyIdUtil.formatKeyId(keyId)} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index 61b45874..c09c1ee5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -4,9 +4,9 @@ package org.pgpainless.key +import _kotlin.hexKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey -import org.pgpainless.key.util.KeyIdUtil /** * Tuple class used to identify a subkey by fingerprints of the primary key of the subkeys key ring, @@ -22,7 +22,7 @@ class SubkeyIdentifier( constructor(keys: PGPKeyRing, keyId: Long): this( OpenPgpFingerprint.of(keys.publicKey), OpenPgpFingerprint.of(keys.getPublicKey(keyId) ?: - throw NoSuchElementException("OpenPGP key does not contain subkey ${KeyIdUtil.formatKeyId(keyId)}"))) + throw NoSuchElementException("OpenPGP key does not contain subkey ${keyId.hexKeyId()}"))) constructor(keys: PGPKeyRing, subkeyFingerprint: OpenPgpFingerprint): this(OpenPgpFingerprint.of(keys), subkeyFingerprint) val keyId = subkeyFingerprint.keyId @@ -42,7 +42,7 @@ class SubkeyIdentifier( return false } - return primaryKeyFingerprint.equals(other.primaryKeyFingerprint) && subkeyFingerprint.equals(other.subkeyFingerprint) + return primaryKeyFingerprint == other.primaryKeyFingerprint && subkeyFingerprint == other.subkeyFingerprint } override fun hashCode(): Int { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt index 4451aa0f..7a2bcc26 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -4,11 +4,11 @@ package org.pgpainless.key.protection +import _kotlin.hexKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider -import org.pgpainless.key.util.KeyIdUtil import org.pgpainless.util.Passphrase /** @@ -54,7 +54,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras */ fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { require(!cache.containsKey(keyId)) { - "The cache already holds a passphrase for ID ${KeyIdUtil.formatKeyId(keyId)}.\n" + + "The cache already holds a passphrase for ID ${keyId.hexKeyId()}.\n" + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." } cache[keyId] = passphrase @@ -90,7 +90,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras // check for existing passphrases before doing anything keyRing.publicKeys.forEach { require(!cache.containsKey(it.keyID)) { - "The cache already holds a passphrase for the key with ID ${KeyIdUtil.formatKeyId(it.keyID)}.\n" + + "The cache already holds a passphrase for the key with ID ${it.keyID.hexKeyId()}.\n" + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index 533b33a7..7385b265 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -5,6 +5,7 @@ package org.pgpainless.key.protection +import _kotlin.hexKeyId import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey @@ -13,7 +14,6 @@ import org.pgpainless.PGPainless import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.info.KeyInfo -import org.pgpainless.key.util.KeyIdUtil import org.pgpainless.key.util.PublicKeyParameterValidationUtil import org.pgpainless.util.Passphrase import kotlin.jvm.Throws @@ -43,7 +43,7 @@ class UnlockSecretKey { if (privateKey == null) { if (secretKey.s2K.type in 100..110) { - throw PGPException("Cannot decrypt secret key ${KeyIdUtil.formatKeyId(secretKey.keyID)}: \n" + + throw PGPException("Cannot decrypt secret key ${secretKey.keyID.hexKeyId()}: \n" + "Unsupported private S2K type ${secretKey.s2K.type}") } throw PGPException("Cannot decrypt secret key.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index 23fe716c..1b9b432a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -4,6 +4,7 @@ package org.pgpainless.signature.subpackets +import _kotlin.hexKeyId import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature @@ -17,7 +18,6 @@ import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.OpenPgpV5Fingerprint import org.pgpainless.key.OpenPgpV6Fingerprint import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.key.util.KeyIdUtil import org.pgpainless.signature.SignatureUtils import java.util.* @@ -143,7 +143,7 @@ class SignatureSubpacketsUtil { @JvmStatic fun getKeyExpirationTimeAsDate(signature: PGPSignature, signingKey: PGPPublicKey): Date? = require(signature.keyID == signingKey.keyID) { - "Provided key (${KeyIdUtil.formatKeyId(signingKey.keyID)}) did not create the signature (${KeyIdUtil.formatKeyId(signature.keyID)})" + "Provided key (${signingKey.keyID.hexKeyId()}) did not create the signature (${signature.keyID.hexKeyId()})" }.run { getKeyExpirationTime(signature)?.let { SignatureUtils.datePlusSeconds(signingKey.creationTime, it.time) From dc064d17278f82b2bb94dd3b28bacb3bbeb6da5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 16:32:01 +0200 Subject: [PATCH 114/351] Kotlin conversion: KeyRingUtils --- .../org/pgpainless/key/util/KeyRingUtils.java | 589 ------------------ .../extensions/PGPSecretKeyRingExtensions.kt | 11 + .../org/pgpainless/key/util/KeyRingUtils.kt | 484 ++++++++++++++ 3 files changed, 495 insertions(+), 589 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java deleted file mode 100644 index 91de3be4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -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.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.util.Strings; -import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.protection.fixes.S2KUsageFix; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class KeyRingUtils { - - private KeyRingUtils() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingUtils.class); - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. - * If it has no primary secret key, throw a {@link NoSuchElementException}. - * - * @param secretKeys secret keys - * @return primary secret key - */ - @Nonnull - public static PGPSecretKey requirePrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { - PGPSecretKey primarySecretKey = getPrimarySecretKeyFrom(secretKeys); - if (primarySecretKey == null) { - throw new NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key."); - } - return primarySecretKey; - } - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing} or null if it has none. - * - * @param secretKeys secret key ring - * @return primary secret key - */ - @Nullable - public static PGPSecretKey getPrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(); - if (secretKey.isMasterKey()) { - return secretKey; - } - return null; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring. - * Throws a {@link NoSuchElementException} if the key ring has no primary public key. - * - * @param keyRing key ring - * @return primary public key - */ - @Nonnull - public static PGPPublicKey requirePrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = getPrimaryPublicKeyFrom(keyRing); - if (primaryPublicKey == null) { - throw new NoSuchElementException("Provided PGPKeyRing has no primary public key."); - } - return primaryPublicKey; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. - * - * @param keyRing key ring - * @return primary public key - */ - @Nullable - public static PGPPublicKey getPrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = keyRing.getPublicKey(); - if (primaryPublicKey.isMasterKey()) { - return primaryPublicKey; - } - return null; - } - - /** - * Return the public key with the given subKeyId from the keyRing. - * If no such subkey exists, return null. - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey or null - */ - @Nullable - public static PGPPublicKey getPublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - return keyRing.getPublicKey(subKeyId); - } - - /** - * Require the public key with the given subKeyId from the keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey - */ - @Nonnull - public static PGPPublicKey requirePublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - PGPPublicKey publicKey = getPublicKeyFrom(keyRing, subKeyId); - if (publicKey == null) { - throw new NoSuchElementException("KeyRing does not contain public key with keyID " + Long.toHexString(subKeyId)); - } - return publicKey; - } - - /** - * Require the secret key with the given secret subKeyId from the secret keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing secret key ring - * @param subKeyId subkey id - * @return secret subkey - */ - @Nonnull - public static PGPSecretKey requireSecretKeyFrom(@Nonnull PGPSecretKeyRing keyRing, long subKeyId) { - PGPSecretKey secretKey = keyRing.getSecretKey(subKeyId); - if (secretKey == null) { - throw new NoSuchElementException("KeyRing does not contain secret key with keyID " + Long.toHexString(subKeyId)); - } - return secretKey; - } - - @Nonnull - public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { - if (keys instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) keys; - } else if (keys instanceof PGPSecretKeyRing) { - return publicKeyRingFrom((PGPSecretKeyRing) keys); - } else { - throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); - } - } - - /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return public key ring - */ - @Nonnull - public static PGPPublicKeyRing publicKeyRingFrom(@Nonnull PGPSecretKeyRing secretKeys) { - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = secretKeys.getPublicKeys(); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); - return publicKeyRing; - } - - /** - * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in - * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. - * - * @param secretKeyRings secret key ring collection - * @return public key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { - List certificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKey : secretKeyRings) { - certificates.add(PGPainless.extractCertificate(secretKey)); - } - return new PGPPublicKeyRingCollection(certificates); - } - - /** - * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. - * - * @param secretKey secret key - * @param protector protector to unlock the secret key - * @return private key - * - * @throws PGPException if something goes wrong (e.g. wrong passphrase) - */ - @Nonnull - public static PGPPrivateKey unlockSecretKey(@Nonnull PGPSecretKey secretKey, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - return UnlockSecretKey.unlockSecretKey(secretKey, protector); - } - - /** - * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. - * - * @param rings array of public key rings - * @return key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { - return new PGPPublicKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. - * - * @param rings array of secret key rings - * @return secret key ring collection - */ - @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { - return new PGPSecretKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. - * - * @param ring public key ring - * @param keyId id of the key in question - * @return true if ring contains said key, false otherwise - */ - public static boolean keyRingContainsKeyWithId(@Nonnull PGPPublicKeyRing ring, - long keyId) { - return ring.getPublicKey(keyId) != null; - } - - /** - * Inject a key certification for the primary key into the given key ring. - * - * @param keyRing key ring - * @param certification key signature - * @return key ring with injected signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPSignature certification) { - return injectCertification(keyRing, keyRing.getPublicKey(), certification); - } - - /** - * Inject a key certification for the given key into the given key ring. - * - * @param keyRing key ring - * @param certifiedKey signed public key - * @param certification key signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected signature - * - * @throws NoSuchElementException in case that the signed key is not part of the key ring - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPPublicKey certifiedKey, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - certifiedKey = PGPPublicKey.addCertification(certifiedKey, certification); - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = publicKeys.iterator(); - boolean added = false; - while (publicKeyIterator.hasNext()) { - PGPPublicKey key = publicKeyIterator.next(); - if (key.getKeyID() == certifiedKey.getKeyID()) { - added = true; - publicKeyList.add(certifiedKey); - } else { - publicKeyList.add(key); - } - } - if (!added) { - throw new NoSuchElementException("Cannot find public key with id " + Long.toHexString(certifiedKey.getKeyID()) + " in the provided key ring."); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a user-id certification into the given key ring. - * - * @param keyRing key ring - * @param userId signed user-id - * @param certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull String userId, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userId, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a user-attribute vector certification into the given key ring. - * - * @param keyRing key ring - * @param userAttributes certified user attributes - * @param certification certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected user-attribute certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPUserAttributeSubpacketVector userAttributes, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userAttributes, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPPublicKey} into the given key ring. - * - * @param keyRing key ring - * @param publicKey public key - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected public key - */ - @Nonnull - public static T keysPlusPublicKey(@Nonnull T keyRing, - @Nonnull PGPPublicKey publicKey) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, publicKey); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @return secret key ring with injected secret key - */ - @Nonnull - public static PGPSecretKeyRing keysPlusSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - @Nonnull PGPSecretKey secretKey) { - return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey); - } - - /** - * Inject the given signature into the public part of the given secret key. - * @param secretKey secret key - * @param signature signature - * @return secret key with the signature injected in its public key - */ - @Nonnull - public static PGPSecretKey secretKeyPlusSignature(@Nonnull PGPSecretKey secretKey, - @Nonnull PGPSignature signature) { - PGPPublicKey publicKey = secretKey.getPublicKey(); - publicKey = PGPPublicKey.addCertification(publicKey, signature); - PGPSecretKey newSecretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); - return newSecretKey; - } - - /** - * Remove the secret key of the subkey identified by the given secret key id from the key ring. - * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. - * - * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. - * - * @param secretKeys secret key ring - * @param secretKeyId id of the secret key to remove - * @return secret key ring with removed secret key - * - * @throws IOException in case of an error during serialization / deserialization of the key - * @throws PGPException in case of a broken key - */ - @Nonnull - public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - - if (secretKeys.getPublicKey().getKeyID() == secretKeyId) { - throw new IllegalArgumentException("Bouncy Castle currently cannot deal with stripped secret primary keys."); - } - - if (secretKeys.getSecretKey(secretKeyId) == null) { - throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); - } - - // Since BCs constructors for secret key rings are mostly private, we need to encode the key ring how we want it - // and then parse it again. - ByteArrayOutputStream encoded = new ByteArrayOutputStream(); - for (PGPSecretKey secretKey : secretKeys) { - if (secretKey.getKeyID() == secretKeyId) { - // only encode the public part of the target key - secretKey.getPublicKey().encode(encoded); - } else { - // otherwise, encode secret + public key - secretKey.encode(encoded); - } - } - for (Iterator it = secretKeys.getExtraPublicKeys(); it.hasNext(); ) { - PGPPublicKey extra = it.next(); - extra.encode(encoded); - } - // Parse the key back into an object - return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - /** - * Strip all user-ids, user-attributes and signatures from the given public key. - * - * @param bloatedKey public key - * @return stripped public key - * @throws PGPException if the packet is faulty or the required calculations fail - */ - public static PGPPublicKey getStrippedDownPublicKey(PGPPublicKey bloatedKey) throws PGPException { - return new PGPPublicKey(bloatedKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - public static List getUserIdsIgnoringInvalidUTF8(PGPPublicKey key) { - List userIds = new ArrayList<>(); - Iterator it = key.getRawUserIDs(); - while (it.hasNext()) { - byte[] rawUserId = it.next(); - try { - userIds.add(Strings.fromUTF8ByteArray(rawUserId)); - } catch (IllegalArgumentException e) { - LOGGER.warn("Invalid UTF-8 user-ID encountered: " + new String(rawUserId)); - } - } - return userIds; - } - - public static PGPSecretKeyRing changePassphrase(Long keyId, - PGPSecretKeyRing secretKeys, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - List secretKeyList = new ArrayList<>(); - if (keyId == null) { - // change passphrase of whole key ring - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); - secretKeyList.add(secretKey); - } - } else { - // change passphrase of selected subkey only - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - if (secretKey.getPublicKey().getKeyID() == keyId) { - // Re-encrypt only the selected subkey - secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); - } - secretKeyList.add(secretKey); - } - } - - PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); - newRing = s2kUsageFixIfNecessary(newRing, newProtector); - return newRing; - } - - - public static PGPSecretKey reencryptPrivateKey( - PGPSecretKey secretKey, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - S2K s2k = secretKey.getS2K(); - // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block - if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) { - long secretKeyId = secretKey.getKeyID(); - PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId); - PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId); - secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor); - } - return secretKey; - } - - - public static PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector) - throws PGPException { - boolean hasS2KUsageChecksum = false; - for (PGPSecretKey secKey : secretKeys) { - if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) { - hasS2KUsageChecksum = true; - break; - } - } - if (hasS2KUsageChecksum) { - secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1( - secretKeys, protector, true); - } - return secretKeys; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt new file mode 100644 index 00000000..3a9c2918 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing + +val PGPSecretKeyRing.certificate: PGPPublicKeyRing + get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt new file mode 100644 index 00000000..bd0edf28 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -0,0 +1,484 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import _kotlin.hexKeyId +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.extensions.certificate +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.Strings +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.key.protection.fixes.S2KUsageFix +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import kotlin.jvm.Throws + +class KeyRingUtils { + + companion object { + + @JvmStatic + private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) + + /** + * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. + * If it has no primary secret key, throw a {@link NoSuchElementException}. + * + * @param secretKeys secret keys + * @return primary secret key + */ + @JvmStatic + fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { + return getPrimarySecretKeyFrom(secretKeys) + ?: throw NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key.") + } + + /** + * Return the primary secret key from the given secret key ring. + * If the key ring only contains subkeys (e.g. if the primary secret key is stripped), + * this method may return null. + * + * @param secretKeys secret key ring + * @return primary secret key + */ + @JvmStatic + fun getPrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey? { + return secretKeys.secretKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Return the primary {@link PGPPublicKey} from the provided key ring. + * Throws a {@link NoSuchElementException} if the key ring has no primary public key. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun requirePrimaryPublicKeyFrom(keyRing: PGPKeyRing): PGPPublicKey { + return getPrimaryPublicKey(keyRing) + ?: throw NoSuchElementException("Provided PGPKeyRing has no primary public key.") + } + + /** + * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun getPrimaryPublicKey(keyRing: PGPKeyRing): PGPPublicKey? { + return keyRing.publicKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Require the public key with the given subKeyId from the keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing key ring + * @param subKeyId subkey id + * @return subkey + */ + @JvmStatic + fun requirePublicKeyFrom(keyRing: PGPKeyRing, subKeyId: Long): PGPPublicKey { + return keyRing.getPublicKey(subKeyId) + ?: throw NoSuchElementException("KeyRing does not contain public key with keyId ${subKeyId.hexKeyId()}.") + } + + /** + * Require the secret key with the given secret subKeyId from the secret keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing secret key ring + * @param subKeyId subkey id + * @return secret subkey + */ + @JvmStatic + fun requireSecretKeyFrom(keyRing: PGPSecretKeyRing, subKeyId: Long): PGPSecretKey { + return keyRing.getSecretKey(subKeyId) + ?: throw NoSuchElementException("KeyRing does not contain secret key with keyID ${subKeyId.hexKeyId()}.") + } + + @JvmStatic + fun publicKeys(keys: PGPKeyRing): PGPPublicKeyRing { + return when (keys) { + is PGPPublicKeyRing -> keys + is PGPSecretKeyRing -> keys.certificate + else -> throw IllegalArgumentException("Unknown keys class: ${keys.javaClass.name}") + } + } + + /** + * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @return public key ring + */ + @JvmStatic + @Deprecated("Deprecated in favor of PGPSecretKeyRing extension method.", + ReplaceWith("secretKeys.certificate", "org.bouncycastle.extensions.certificate")) + fun publicKeyRingFrom(secretKeys: PGPSecretKeyRing): PGPPublicKeyRing { + return secretKeys.certificate + } + + /** + * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in + * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. + * + * @param secretKeyRings secret key ring collection + * @return public key ring collection + */ + @JvmStatic + fun publicKeyRingCollectionFrom(secretKeyRings: PGPSecretKeyRingCollection): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection( + secretKeyRings.keyRings.asSequence() + .map { it.certificate } + .toList()) + } + + /** + * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. + * + * @param secretKey secret key + * @param protector protector to unlock the secret key + * @return private key + * + * @throws PGPException if something goes wrong (e.g. wrong passphrase) + */ + @JvmStatic + fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { + return UnlockSecretKey.unlockSecretKey(secretKey, protector) + } + + /** + * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. + * + * @param certificates array of public key rings + * @return key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection(vararg certificates: PGPPublicKeyRing): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection(certificates.toList()) + } + + /** + * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. + * + * @param secretKeys array of secret key rings + * @return secret key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection(vararg secretKeys: PGPSecretKeyRing): PGPSecretKeyRingCollection { + return PGPSecretKeyRingCollection(secretKeys.toList()) + } + + /** + * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. + * + * @param certificate public key ring + * @param keyId id of the key in question + * @return true if ring contains said key, false otherwise + */ + @JvmStatic + fun keyRingContainsKeyWithId(certificate: PGPPublicKeyRing, keyId: Long): Boolean { + return certificate.getPublicKey(keyId) != null + } + + /** + * Inject a key certification for the primary key into the given key ring. + * + * @param keyRing key ring + * @param certification key signature + * @return key ring with injected signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + */ + @JvmStatic + fun injectCertification(keyRing: T, certification: PGPSignature): T { + return injectCertification(keyRing, keyRing.publicKey, certification) + } + + /** + * Inject a key certification for the given key into the given key ring. + * + * @param keyRing key ring + * @param certifiedKey signed public key + * @param certification key signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected signature + * + * @throws NoSuchElementException in case that the signed key is not part of the key ring + */ + @JvmStatic + fun injectCertification(keyRing: T, certifiedKey: PGPPublicKey, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + if (!keyRingContainsKeyWithId(certificate, certifiedKey.keyID)) { + throw NoSuchElementException("Cannot find public key with id ${certifiedKey.keyID.hexKeyId()} in the provided key ring.") + } + + certificate = PGPPublicKeyRing( + certificate.publicKeys.asSequence().map { + if (it.keyID == certifiedKey.keyID) { + PGPPublicKey.addCertification(it, certification) + } else { + it + } + }.toList()) + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-id certification into the given key ring. + * + * @param keyRing key ring + * @param userId signed user-id + * @param certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected certification + */ + @JvmStatic + fun injectCertification(keyRing: T, userId: CharSequence, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), + userId.toString(), certification) + ).plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-attribute vector certification into the given key ring. + * + * @param keyRing key ring + * @param userAttributes certified user attributes + * @param certification certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected user-attribute certification + */ + @JvmStatic + fun injectCertification(keyRing: T, userAttributes: PGPUserAttributeSubpacketVector, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), + userAttributes, certification) + ).plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a {@link PGPPublicKey} into the given key ring. + * + * @param keyRing key ring + * @param publicKey public key + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected public key + */ + @JvmStatic + fun keysPlusPublicKey(keyRing: T, publicKey: PGPPublicKey): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + return if (secretKeys == null) { + PGPPublicKeyRing.insertPublicKey(certificate, publicKey) as T + } else { + PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey) as T + } + } + + @JvmStatic + private fun secretAndPublicKeys(keyRing: PGPKeyRing): Pair { + var secretKeys: PGPSecretKeyRing? = null + val certificate: PGPPublicKeyRing + when (keyRing) { + is PGPSecretKeyRing -> { + secretKeys = keyRing + certificate = secretKeys.certificate + } + is PGPPublicKeyRing -> { + certificate = keyRing + } + else -> throw IllegalArgumentException("keyRing is an unknown PGPKeyRing subclass: ${keyRing.javaClass.name}") + } + return secretKeys to certificate + } + + /** + * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @param secretKey secret key + * @return secret key ring with injected secret key + */ + @JvmStatic + fun keysPlusSecretKey(secretKeys: PGPSecretKeyRing, secretKey: PGPSecretKey): PGPSecretKeyRing { + return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey) + } + + /** + * Inject the given signature into the public part of the given secret key. + * @param secretKey secret key + * @param signature signature + * @return secret key with the signature injected in its public key + */ + @JvmStatic + fun secretKeyPlusSignature(secretKey: PGPSecretKey, signature: PGPSignature): PGPSecretKey { + PGPPublicKey.addCertification(secretKey.publicKey, signature).let { + return PGPSecretKey.replacePublicKey(secretKey, it) + } + } + + /** + * Remove the secret key of the subkey identified by the given secret key id from the key ring. + * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. + * + * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. + * + * @param secretKeys secret key ring + * @param keyId id of the secret key to remove + * @return secret key ring with removed secret key + * + * @throws IOException in case of an error during serialization / deserialization of the key + * @throws PGPException in case of a broken key + */ + @JvmStatic + fun stripSecretKey(secretKeys: PGPSecretKeyRing, keyId: Long): PGPSecretKeyRing { + require(keyId != secretKeys.publicKey.keyID) { + "Bouncy Castle currently cannot deal with stripped primary secret keys." + } + if (secretKeys.getSecretKey(keyId) == null) { + throw NoSuchElementException("PGPSecretKeyRing does not contain secret key ${keyId.hexKeyId()}.") + } + + val out = ByteArrayOutputStream() + secretKeys.forEach { + if (it.keyID == keyId) { + // only encode the public key + it.publicKey.encode(out) + } else { + // else encode the whole secret + public key + it.encode(out) + } + } + secretKeys.extraPublicKeys.forEach { + it.encode(out) + } + return PGPSecretKeyRing(out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + /** + * Strip all user-ids, user-attributes and signatures from the given public key. + * + * @param bloatedKey public key + * @return stripped public key + * @throws PGPException if the packet is faulty or the required calculations fail + */ + @JvmStatic + fun getStrippedDownPublicKey(bloatedKey: PGPPublicKey): PGPPublicKey { + return PGPPublicKey(bloatedKey.publicKeyPacket, ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + @JvmStatic + fun getUserIdsIgnoringInvalidUTF8(key: PGPPublicKey): List { + return buildList { + key.rawUserIDs.forEach { + try { + add(Strings.fromUTF8ByteArray(it)) + } catch (e : IllegalArgumentException) { + LOGGER.warn("Invalid UTF-8 user-ID encountered: ${String(it)}") + } + } + } + } + + @JvmStatic + @Throws(MissingPassphraseException::class, PGPException::class) + fun changePassphrase(keyId: Long?, + secretKeys: PGPSecretKeyRing, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector): PGPSecretKeyRing { + return if (keyId == null) { + PGPSecretKeyRing( + secretKeys.secretKeys.asSequence().map { + reencryptPrivateKey(it, oldProtector, newProtector) + }.toList() + ) + } else { + PGPSecretKeyRing( + secretKeys.secretKeys.asSequence().map { + if (it.keyID == keyId) { + reencryptPrivateKey(it, oldProtector, newProtector) + } else { + it + } + }.toList() + ) + }.let { s2kUsageFixIfNecessary(it, newProtector) } + } + + @JvmStatic + fun reencryptPrivateKey(secretKey: PGPSecretKey, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector): PGPSecretKey { + if (secretKey.s2K != null && secretKey.s2K.type == S2K.GNU_DUMMY_S2K) { + // If the key uses GNU_DUMMY_S2K we leave it as is + return secretKey + } + + return PGPSecretKey.copyWithNewPassword(secretKey, + oldProtector.getDecryptor(secretKey.keyID), + newProtector.getEncryptor(secretKey.keyID)) + } + + @JvmStatic + fun s2kUsageFixIfNecessary(secretKeys: PGPSecretKeyRing, protector: SecretKeyRingProtector): PGPSecretKeyRing { + if (secretKeys.secretKeys.asSequence().any { it.s2KUsage == SecretKeyPacket.USAGE_CHECKSUM }) { + return S2KUsageFix.replaceUsageChecksumWithUsageSha1(secretKeys, protector, true) + } + return secretKeys + } + + } +} \ No newline at end of file From 39e170064ce1ca7fb7891b450cc1c44dc656de95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 16:44:41 +0200 Subject: [PATCH 115/351] Kotlin conversion: NotationRegistry --- .../org/pgpainless/util/NotationRegistry.kt} | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) rename pgpainless-core/src/main/{java/org/pgpainless/util/NotationRegistry.java => kotlin/org/pgpainless/util/NotationRegistry.kt} (57%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt similarity index 57% rename from pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt index 11aa7651..15dbe886 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/NotationRegistry.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt @@ -1,11 +1,8 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.util; - -import java.util.HashSet; -import java.util.Set; +package org.pgpainless.util /** * Registry for known notations. @@ -14,9 +11,12 @@ import java.util.Set; * * To add a notation name, call {@link #addKnownNotation(String)}. */ -public class NotationRegistry { +class NotationRegistry constructor(notations: Set = setOf()) { + private val knownNotations: MutableSet - private final Set knownNotations = new HashSet<>(); + init { + knownNotations = notations.toMutableSet() + } /** * Add a known notation name into the registry. @@ -24,11 +24,8 @@ public class NotationRegistry { * * @param notationName name of the notation */ - public void addKnownNotation(String notationName) { - if (notationName == null) { - throw new NullPointerException("Notation name MUST NOT be null."); - } - knownNotations.add(notationName); + fun addKnownNotation(notationName: String): NotationRegistry = apply { + knownNotations.add(notationName) } /** @@ -37,14 +34,12 @@ public class NotationRegistry { * @param notationName name of the notation * @return true if notation is known, false otherwise. */ - public boolean isKnownNotation(String notationName) { - return knownNotations.contains(notationName); - } + fun isKnownNotation(notationName: String): Boolean = knownNotations.contains(notationName) /** * Clear all known notations from the registry. */ - public void clear() { - knownNotations.clear(); + fun clear() { + knownNotations.clear() } -} +} \ No newline at end of file From d8df6c35d0cfe7bd7918aefca937f617b2ea8487 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 11:35:44 +0200 Subject: [PATCH 116/351] Rename heyKeyId -> openPgpKeyId --- .../{_kotlin => openpgp}/LongExtensions.kt | 10 +++++----- .../OpenPgpMessageInputStream.kt | 18 +++++++++--------- .../org/pgpainless/key/SubkeyIdentifier.kt | 4 ++-- .../CachingSecretKeyRingProtector.kt | 6 +++--- .../key/protection/UnlockSecretKey.kt | 4 ++-- .../org/pgpainless/key/util/KeyIdUtil.kt | 8 ++++---- .../org/pgpainless/key/util/KeyRingUtils.kt | 10 +++++----- .../subpackets/SignatureSubpacketsUtil.kt | 4 ++-- 8 files changed, 32 insertions(+), 32 deletions(-) rename pgpainless-core/src/main/kotlin/{_kotlin => openpgp}/LongExtensions.kt (69%) diff --git a/pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt similarity index 69% rename from pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt rename to pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt index b2c64e2e..13f94943 100644 --- a/pgpainless-core/src/main/kotlin/_kotlin/LongExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt @@ -2,19 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package _kotlin +package openpgp /** - * Format this Long as a 16 digit uppercase hex number. + * Format this Long as an OpenPGP key-ID (16 digit uppercase hex number). */ -fun Long.hexKeyId(): String { +fun Long.openPgpKeyId(): String { return String.format("%016X", this).uppercase() } /** - * Parse a 16 digit hex number into a Long. + * Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */ -fun Long.Companion.fromHexKeyId(hexKeyId: String): Long { +fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long { require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { "Provided long key-id does not match expected format. " + "A long key-id consists of 16 hexadecimal characters." diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 3c056cfa..4b7b1f1f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -4,7 +4,7 @@ package org.pgpainless.decryption_verification -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException import org.bouncycastle.openpgp.* @@ -180,7 +180,7 @@ class OpenPgpMessageInputStream( private fun processOnePassSignature() { syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) val ops = packetInputStream!!.readOnePassSignature() - LOGGER.debug("One-Pass-Signature Packet by key ${ops.keyID.hexKeyId()} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("One-Pass-Signature Packet by key ${ops.keyID.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addOnePassSignature(ops) } @@ -197,11 +197,11 @@ class OpenPgpMessageInputStream( val keyId = SignatureUtils.determineIssuerKeyId(signature) if (isSigForOps) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${keyId.hexKeyId()} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) } else { - LOGGER.debug("Prepended Signature Packet by key ${keyId.hexKeyId()} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug("Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addPrependedSignature(signature) } } @@ -282,10 +282,10 @@ class OpenPgpMessageInputStream( // try (known) secret keys for (pkesk in esks.pkesks) { val keyId = pkesk.keyID - LOGGER.debug("Encountered PKESK for recipient ${keyId.hexKeyId()}") + LOGGER.debug("Encountered PKESK for recipient ${keyId.openPgpKeyId()}") val decryptionKeys = getDecryptionKey(keyId) if (decryptionKeys == null) { - LOGGER.debug("Skipping PKESK because no matching key ${keyId.hexKeyId()} was provided.") + LOGGER.debug("Skipping PKESK because no matching key ${keyId.openPgpKeyId()} was provided.") continue } val secretKey = decryptionKeys.getSecretKey(keyId) @@ -618,7 +618,7 @@ class OpenPgpMessageInputStream( if (check != null) { detachedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key."))) @@ -631,7 +631,7 @@ class OpenPgpMessageInputStream( if (check != null) { prependedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key") @@ -693,7 +693,7 @@ class OpenPgpMessageInputStream( } if (!found) { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.hexKeyId()} found.") + LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( SignatureVerification(signature, null), SignatureValidationException("Missing verification key.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index c09c1ee5..ea36913c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -4,7 +4,7 @@ package org.pgpainless.key -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey @@ -22,7 +22,7 @@ class SubkeyIdentifier( constructor(keys: PGPKeyRing, keyId: Long): this( OpenPgpFingerprint.of(keys.publicKey), OpenPgpFingerprint.of(keys.getPublicKey(keyId) ?: - throw NoSuchElementException("OpenPGP key does not contain subkey ${keyId.hexKeyId()}"))) + throw NoSuchElementException("OpenPGP key does not contain subkey ${keyId.openPgpKeyId()}"))) constructor(keys: PGPKeyRing, subkeyFingerprint: OpenPgpFingerprint): this(OpenPgpFingerprint.of(keys), subkeyFingerprint) val keyId = subkeyFingerprint.keyId diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt index 7a2bcc26..32f69f70 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -4,7 +4,7 @@ package org.pgpainless.key.protection -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.key.OpenPgpFingerprint @@ -54,7 +54,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras */ fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { require(!cache.containsKey(keyId)) { - "The cache already holds a passphrase for ID ${keyId.hexKeyId()}.\n" + + "The cache already holds a passphrase for ID ${keyId.openPgpKeyId()}.\n" + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." } cache[keyId] = passphrase @@ -90,7 +90,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras // check for existing passphrases before doing anything keyRing.publicKeys.forEach { require(!cache.containsKey(it.keyID)) { - "The cache already holds a passphrase for the key with ID ${it.keyID.hexKeyId()}.\n" + + "The cache already holds a passphrase for the key with ID ${it.keyID.openPgpKeyId()}.\n" + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index 7385b265..1317ef05 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -5,7 +5,7 @@ package org.pgpainless.key.protection -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey @@ -43,7 +43,7 @@ class UnlockSecretKey { if (privateKey == null) { if (secretKey.s2K.type in 100..110) { - throw PGPException("Cannot decrypt secret key ${secretKey.keyID.hexKeyId()}: \n" + + throw PGPException("Cannot decrypt secret key ${secretKey.keyID.openPgpKeyId()}: \n" + "Unsupported private S2K type ${secretKey.s2K.type}") } throw PGPException("Cannot decrypt secret key.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt index b6ff1789..63b65672 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -4,8 +4,8 @@ package org.pgpainless.key.util -import _kotlin.fromHexKeyId -import _kotlin.hexKeyId +import openpgp.fromOpenPgpKeyId +import openpgp.openPgpKeyId class KeyIdUtil { @@ -21,7 +21,7 @@ class KeyIdUtil { @JvmStatic @Deprecated("Superseded by Long extension method.", ReplaceWith("Long.fromHexKeyId(longKeyId)")) - fun fromLongKeyId(longKeyId: String) = Long.fromHexKeyId(longKeyId) + fun fromLongKeyId(longKeyId: String) = Long.fromOpenPgpKeyId(longKeyId) /** * Format a long key-ID as upper-case hex string. @@ -31,6 +31,6 @@ class KeyIdUtil { @JvmStatic @Deprecated("Superseded by Long extension method.", ReplaceWith("keyId.hexKeyId()")) - fun formatKeyId(keyId: Long) = keyId.hexKeyId() + fun formatKeyId(keyId: Long) = keyId.openPgpKeyId() } } \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index bd0edf28..d3c77ab6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -4,7 +4,7 @@ package org.pgpainless.key.util -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.extensions.certificate @@ -100,7 +100,7 @@ class KeyRingUtils { @JvmStatic fun requirePublicKeyFrom(keyRing: PGPKeyRing, subKeyId: Long): PGPPublicKey { return keyRing.getPublicKey(subKeyId) - ?: throw NoSuchElementException("KeyRing does not contain public key with keyId ${subKeyId.hexKeyId()}.") + ?: throw NoSuchElementException("KeyRing does not contain public key with keyId ${subKeyId.openPgpKeyId()}.") } /** @@ -114,7 +114,7 @@ class KeyRingUtils { @JvmStatic fun requireSecretKeyFrom(keyRing: PGPSecretKeyRing, subKeyId: Long): PGPSecretKey { return keyRing.getSecretKey(subKeyId) - ?: throw NoSuchElementException("KeyRing does not contain secret key with keyID ${subKeyId.hexKeyId()}.") + ?: throw NoSuchElementException("KeyRing does not contain secret key with keyID ${subKeyId.openPgpKeyId()}.") } @JvmStatic @@ -233,7 +233,7 @@ class KeyRingUtils { var certificate: PGPPublicKeyRing = secretAndPublicKeys.second if (!keyRingContainsKeyWithId(certificate, certifiedKey.keyID)) { - throw NoSuchElementException("Cannot find public key with id ${certifiedKey.keyID.hexKeyId()} in the provided key ring.") + throw NoSuchElementException("Cannot find public key with id ${certifiedKey.keyID.openPgpKeyId()} in the provided key ring.") } certificate = PGPPublicKeyRing( @@ -389,7 +389,7 @@ class KeyRingUtils { "Bouncy Castle currently cannot deal with stripped primary secret keys." } if (secretKeys.getSecretKey(keyId) == null) { - throw NoSuchElementException("PGPSecretKeyRing does not contain secret key ${keyId.hexKeyId()}.") + throw NoSuchElementException("PGPSecretKeyRing does not contain secret key ${keyId.openPgpKeyId()}.") } val out = ByteArrayOutputStream() diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index 1b9b432a..172ef32f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -4,7 +4,7 @@ package org.pgpainless.signature.subpackets -import _kotlin.hexKeyId +import openpgp.openPgpKeyId import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature @@ -143,7 +143,7 @@ class SignatureSubpacketsUtil { @JvmStatic fun getKeyExpirationTimeAsDate(signature: PGPSignature, signingKey: PGPPublicKey): Date? = require(signature.keyID == signingKey.keyID) { - "Provided key (${signingKey.keyID.hexKeyId()}) did not create the signature (${signature.keyID.hexKeyId()})" + "Provided key (${signingKey.keyID.openPgpKeyId()}) did not create the signature (${signature.keyID.openPgpKeyId()})" }.run { getKeyExpirationTime(signature)?.let { SignatureUtils.datePlusSeconds(signingKey.creationTime, it.time) From 3115e13bc2a71cca3d72b5ea569417b3a4e76748 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 11:39:57 +0200 Subject: [PATCH 117/351] Kotlin conversion: CustomPublicKeyDataDecryptorFactory --- .../CustomPublicKeyDataDecryptorFactory.java | 27 ------------------- .../CustomPublicKeyDataDecryptorFactory.kt | 26 ++++++++++++++++++ 2 files changed, 26 insertions(+), 27 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java deleted file mode 100644 index 91902cc7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Custom {@link PublicKeyDataDecryptorFactory} which can enable customized implementations of message decryption - * using public keys. - * This class can for example be used to implement message encryption using hardware tokens like smartcards or - * TPMs. - * @see ConsumerOptions#addCustomDecryptorFactory(CustomPublicKeyDataDecryptorFactory) - */ -public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { - - /** - * Return the {@link SubkeyIdentifier} for which this particular {@link CustomPublicKeyDataDecryptorFactory} - * is intended. - * - * @return subkey identifier - */ - SubkeyIdentifier getSubkeyIdentifier(); - -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt new file mode 100644 index 00000000..b7f57da3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.key.SubkeyIdentifier + +/** + * Custom [PublicKeyDataDecryptorFactory] which can enable customized implementations of message decryption + * using public keys. + * This class can for example be used to implement message encryption using hardware tokens like smartcards or + * TPMs. + * @see [ConsumerOptions.addCustomDecryptorFactory] + */ +interface CustomPublicKeyDataDecryptorFactory : PublicKeyDataDecryptorFactory { + + /** + * Identifier for the subkey for which this particular [CustomPublicKeyDataDecryptorFactory] + * is intended. + * + * @return subkey identifier + */ + val subkeyIdentifier: SubkeyIdentifier +} \ No newline at end of file From b09979fa457e0221dc2e34ae911e7dbf4f0b208a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 11:49:39 +0200 Subject: [PATCH 118/351] Kotlin conversion: HardwareSecurity --- .../HardwareSecurity.java | 105 ------------------ .../HardwareSecurity.kt | 76 +++++++++++++ 2 files changed, 76 insertions(+), 105 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java deleted file mode 100644 index fc88ef33..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import org.bouncycastle.bcpg.AEADEncDataPacket; -import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.operator.PGPDataDecryptor; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.pgpainless.key.SubkeyIdentifier; - -/** - * Enable integration of hardware-backed OpenPGP keys. - */ -public class HardwareSecurity { - - public interface DecryptionCallback { - - /** - * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with - * hardware security modules such as smartcards or TPMs. - * - * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. - * - * @param keyId id of the key - * @param keyAlgorithm algorithm - * @param sessionKeyData encrypted session key - * - * @return decrypted session key - * @throws HardwareSecurityException exception - */ - byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) - throws HardwareSecurityException; - - } - - /** - * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys - * to a {@link DecryptionCallback}. - * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. - */ - public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { - - private final DecryptionCallback callback; - // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. - private final PublicKeyDataDecryptorFactory factory = - new BcPublicKeyDataDecryptorFactory(null); - private final SubkeyIdentifier subkey; - - /** - * Create a new {@link HardwareDataDecryptorFactory}. - * - * @param subkeyIdentifier identifier of the decryption subkey - * @param callback decryption callback - */ - public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { - this.callback = callback; - this.subkey = subkeyIdentifier; - } - - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) - throws PGPException { - try { - // delegate decryption to the callback - return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); - } catch (HardwareSecurityException e) { - throw new PGPException("Hardware-backed decryption failed.", e); - } - } - - @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) - throws PGPException { - return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); - } - - @Override - public PGPDataDecryptor createDataDecryptor(AEADEncDataPacket aeadEncDataPacket, PGPSessionKey sessionKey) - throws PGPException { - return factory.createDataDecryptor(aeadEncDataPacket, sessionKey); - } - - @Override - public PGPDataDecryptor createDataDecryptor(SymmetricEncIntegrityPacket seipd, PGPSessionKey sessionKey) - throws PGPException { - return factory.createDataDecryptor(seipd, sessionKey); - } - - @Override - public SubkeyIdentifier getSubkeyIdentifier() { - return subkey; - } - } - - public static class HardwareSecurityException - extends Exception { - - } - -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt new file mode 100644 index 00000000..7d0d243a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.openpgp.operator.PGPDataDecryptor +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.pgpainless.key.SubkeyIdentifier +import kotlin.jvm.Throws + +/** + * Enable integration of hardware-backed OpenPGP keys. + */ +class HardwareSecurity { + + interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with + * hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the [HardwareSecurityException] is thrown. + * + * @param keyId id of the key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + @Throws(HardwareSecurityException::class) + fun decryptSessionKey(keyId: Long, keyAlgorithm: Int, sessionKeyData: ByteArray): ByteArray + } + + /** + * Implementation of [PublicKeyDataDecryptorFactory] which delegates decryption of encrypted session keys + * to a [DecryptionCallback]. + * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + */ + class HardwareDataDecryptorFactory( + override val subkeyIdentifier: SubkeyIdentifier, + private val callback: DecryptionCallback, + ) : CustomPublicKeyDataDecryptorFactory { + + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private val factory: PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(null) + + override fun createDataDecryptor(withIntegrityPacket: Boolean, encAlgorithm: Int, key: ByteArray?): PGPDataDecryptor { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key) + } + + override fun createDataDecryptor(aeadEncDataPacket: AEADEncDataPacket?, sessionKey: PGPSessionKey?): PGPDataDecryptor { + return factory.createDataDecryptor(aeadEncDataPacket, sessionKey) + } + + override fun createDataDecryptor(seipd: SymmetricEncIntegrityPacket?, sessionKey: PGPSessionKey?): PGPDataDecryptor { + return factory.createDataDecryptor(seipd, sessionKey) + } + + override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray { + return try { + callback.decryptSessionKey(subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0]) + } catch (e : HardwareSecurityException) { + throw PGPException("Hardware-backed decryption failed.", e) + } + } + } + + class HardwareSecurityException : Exception() +} \ No newline at end of file From 4cfdcca2e015e53f2aea89b225324514b62e927e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 14:42:44 +0200 Subject: [PATCH 119/351] Kotlin conversion: MessageMetadata --- .../MessageMetadata.java | 869 ------------------ .../MessageMetadata.kt | 528 +++++++++++ .../OpenPgpMessageInputStream.kt | 21 +- .../SignatureVerification.kt | 13 +- .../MessageMetadataTest.java | 6 +- 5 files changed, 549 insertions(+), 888 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java deleted file mode 100644 index 1f7a5b03..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java +++ /dev/null @@ -1,869 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.util.ArrayList; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.SessionKey; - -/** - * View for extracting metadata about a {@link Message}. - */ -public class MessageMetadata { - - protected Message message; - - public MessageMetadata(@Nonnull Message message) { - this.message = message; - } - - public boolean isUsingCleartextSignatureFramework() { - return message.isCleartextSigned(); - } - - public boolean isEncrypted() { - SymmetricKeyAlgorithm algorithm = getEncryptionAlgorithm(); - return algorithm != null && algorithm != SymmetricKeyAlgorithm.NULL; - } - - public boolean isEncryptedFor(@Nonnull PGPKeyRing keys) { - Iterator encryptionLayers = getEncryptionLayers(); - while (encryptionLayers.hasNext()) { - EncryptedData encryptedData = encryptionLayers.next(); - for (long recipient : encryptedData.getRecipients()) { - PGPPublicKey key = keys.getPublicKey(recipient); - if (key != null) { - return true; - } - } - } - return false; - } - - /** - * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. - * - * @param userId userId - * @param email if true, treat the user-id as an email address and match all userIDs containing this address - * @param certificateAuthority certificate authority - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority) { - return isAuthenticatablySignedBy(userId, email, certificateAuthority, 120); - } - - /** - * Return true, if the message was verifiably signed by a certificate for which we can authenticate a binding to the given userId. - * - * @param userId userId - * @param email if true, treat the user-id as an email address and match all userIDs containing this address - * @param certificateAuthority certificate authority - * @param targetAmount target trust amount - * @return true, if we can authenticate a binding for a signing key with sufficient evidence - */ - public boolean isAuthenticatablySignedBy(String userId, boolean email, CertificateAuthority certificateAuthority, int targetAmount) { - for (SignatureVerification verification : getVerifiedSignatures()) { - CertificateAuthenticity authenticity = certificateAuthority.authenticateBinding( - verification.getSigningKey().getFingerprint(), userId, email, - verification.getSignature().getCreationTime(), targetAmount); - if (authenticity.isAuthenticated()) { - return true; - } - } - return false; - } - - /** - * Return a list containing all recipient keyIDs. - * - * @return list of recipients - */ - public List getRecipientKeyIds() { - List keyIds = new ArrayList<>(); - Iterator encLayers = getEncryptionLayers(); - while (encLayers.hasNext()) { - EncryptedData layer = encLayers.next(); - keyIds.addAll(layer.getRecipients()); - } - return keyIds; - } - - public @Nonnull Iterator getEncryptionLayers() { - return new LayerIterator(message) { - @Override - public boolean matches(Packet layer) { - return layer instanceof EncryptedData; - } - - @Override - public EncryptedData getProperty(Layer last) { - return (EncryptedData) last; - } - }; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} of the outermost encrypted data packet, or null if message is - * unencrypted. - * - * @return encryption algorithm - */ - public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return firstOrNull(getEncryptionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link SymmetricKeyAlgorithm SymmetricKeyAlgorithms} encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item - * that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. - * - * @return iterator of symmetric encryption algorithms - */ - public @Nonnull Iterator getEncryptionAlgorithms() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.algorithm); - } - - public @Nonnull Iterator getCompressionLayers() { - return new LayerIterator(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof CompressedData; - } - - @Override - CompressedData getProperty(Layer last) { - return (CompressedData) last; - } - }; - } - - /** - * Return the {@link CompressionAlgorithm} of the outermost compressed data packet, or null, if the message - * does not contain any compressed data packets. - * - * @return compression algorithm - */ - public @Nullable CompressionAlgorithm getCompressionAlgorithm() { - return firstOrNull(getCompressionAlgorithms()); - } - - /** - * Return an {@link Iterator} of {@link CompressionAlgorithm CompressionAlgorithms} encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next - * item that of the next nested compressed data packet and so on. - * The iterator might also be empty, in case of a message without any compressed data packets. - * - * @return iterator of compression algorithms - */ - public @Nonnull Iterator getCompressionAlgorithms() { - return map(getCompressionLayers(), compressionLayer -> compressionLayer.algorithm); - } - - /** - * Return the {@link SessionKey} of the outermost encrypted data packet. - * If the message was unencrypted, this method returns
null
. - * - * @return session key of the message - */ - public @Nullable SessionKey getSessionKey() { - return firstOrNull(getSessionKeys()); - } - - /** - * Return an {@link Iterator} of {@link SessionKey SessionKeys} for all encrypted data packets in the message. - * The first item returned by the iterator is the session key of the outermost encrypted data packet, - * the next item that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. - * - * @return iterator of session keys - */ - public @Nonnull Iterator getSessionKeys() { - return map(getEncryptionLayers(), encryptedData -> encryptedData.sessionKey); - } - - public boolean isVerifiedSignedBy(@Nonnull PGPKeyRing keys) { - return isVerifiedInlineSignedBy(keys) || isVerifiedDetachedSignedBy(keys); - } - - /** - * Return true, if the message was verifiable signed by a certificate that either has the given fingerprint - * as primary key, or as the signing subkey. - * - * @param fingerprint fingerprint - * @return true if message was signed by a cert identified by the given fingerprint - */ - public boolean isVerifiedSignedBy(@Nonnull OpenPgpFingerprint fingerprint) { - List verifications = getVerifiedSignatures(); - for (SignatureVerification verification : verifications) { - if (verification.getSigningKey() == null) { - continue; - } - - if (fingerprint.equals(verification.getSigningKey().getPrimaryKeyFingerprint()) || - fingerprint.equals(verification.getSigningKey().getSubkeyFingerprint())) { - return true; - } - } - return false; - } - - public List getVerifiedSignatures() { - List allVerifiedSignatures = getVerifiedInlineSignatures(); - allVerifiedSignatures.addAll(getVerifiedDetachedSignatures()); - return allVerifiedSignatures; - } - - public boolean isVerifiedDetachedSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedDetachedSignatures(), keys); - } - - /** - * Return a list of all verified detached signatures. - * This list contains all acceptable, correct detached signatures. - * - * @return verified detached signatures - */ - public @Nonnull List getVerifiedDetachedSignatures() { - return message.getVerifiedDetachedSignatures(); - } - - /** - * Return a list of all rejected detached signatures. - * - * @return rejected detached signatures - */ - public @Nonnull List getRejectedDetachedSignatures() { - return message.getRejectedDetachedSignatures(); - } - - /** - * Return a list of all rejected signatures. - * - * @return rejected signatures - */ - public @Nonnull List getRejectedSignatures() { - List rejected = new ArrayList<>(); - rejected.addAll(getRejectedInlineSignatures()); - rejected.addAll(getRejectedDetachedSignatures()); - return rejected; - } - - public boolean hasRejectedSignatures() { - return !getRejectedSignatures().isEmpty(); - } - - /** - * Return true, if the message contains any (verified or rejected) signature. - * @return true if message has signature - */ - public boolean hasSignature() { - return isVerifiedSigned() || hasRejectedSignatures(); - } - - public boolean isVerifiedInlineSignedBy(@Nonnull PGPKeyRing keys) { - return containsSignatureBy(getVerifiedInlineSignatures(), keys); - } - - /** - * Return a list of all verified inline-signatures. - * This list contains all acceptable, correct signatures that were part of the message itself. - * - * @return verified inline signatures - */ - public @Nonnull List getVerifiedInlineSignatures() { - List verifications = new ArrayList<>(); - Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); - while (verificationsByLayer.hasNext()) { - verifications.addAll(verificationsByLayer.next()); - } - return verifications; - } - - /** - * Return an {@link Iterator} of {@link List Lists} of verified inline-signatures of the message. - * Since signatures might occur in different layers within a message, this method can be used to gain more detailed - * insights into what signatures were encountered at what layers of the message structure. - * Each item of the {@link Iterator} represents a layer of the message and contains only signatures from - * this layer. - * An empty list means no (or no acceptable) signatures were encountered in that layer. - * - * @return iterator of lists of signatures by-layer. - */ - public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getVerifiedOnePassSignatures()); - list.addAll(last.getVerifiedPrependedSignatures()); - return list; - } - }; - } - - /** - * Return a list of all rejected inline-signatures of the message. - * - * @return list of rejected inline-signatures - */ - public @Nonnull List getRejectedInlineSignatures() { - List rejected = new ArrayList<>(); - Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); - while (rejectedByLayer.hasNext()) { - rejected.addAll(rejectedByLayer.next()); - } - return rejected; - } - - /** - * Similar to {@link #getVerifiedInlineSignaturesByLayer()}, this method returns all rejected inline-signatures - * of the message, but organized by layer. - * - * @return rejected inline-signatures by-layer - */ - public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { - return new LayerIterator>(message) { - @Override - boolean matches(Packet layer) { - return layer instanceof Layer; - } - - @Override - List getProperty(Layer last) { - List list = new ArrayList<>(); - list.addAll(last.getRejectedOnePassSignatures()); - list.addAll(last.getRejectedPrependedSignatures()); - return list; - } - }; - } - - private static boolean containsSignatureBy(@Nonnull List verifications, - @Nonnull PGPKeyRing keys) { - for (SignatureVerification verification : verifications) { - SubkeyIdentifier issuer = verification.getSigningKey(); - if (issuer == null) { - // No issuer, shouldn't happen, but better be safe and skip... - continue; - } - - if (keys.getPublicKey().getKeyID() != issuer.getPrimaryKeyId()) { - // Wrong cert - continue; - } - - if (keys.getPublicKey(issuer.getSubkeyId()) != null) { - // Matching cert and signing key - return true; - } - } - return false; - } - - /** - * Return the value of the literal data packet's filename field. - * This value can be used to store a decrypted file under its original filename, - * but since this field is not necessarily part of the signed data of a message, usage of this field is - * discouraged. - * - * @return filename - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable String getFilename() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFileName(); - } - - /** - * Returns true, if the filename of the literal data packet indicates that the data is intended for your eyes only. - * - * @return isForYourEyesOnly - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFilename()); - } - - /** - * Return the value of the literal data packets modification date field. - * This value can be used to restore the modification date of a decrypted file, - * but since this field is not necessarily part of the signed data, its use is discouraged. - * - * @return modification date - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable Date getModificationDate() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getModificationDate(); - } - - /** - * Return the value of the format field of the literal data packet. - * This value indicates what format (text, binary data, ...) the data has. - * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. - * - * @return format - * @see RFC4880 §5.9. Literal Data Packet - */ - public @Nullable StreamEncoding getLiteralDataEncoding() { - LiteralData literalData = findLiteralData(); - if (literalData == null) { - return null; - } - return literalData.getFormat(); - } - - /** - * Find the {@link LiteralData} layer of an OpenPGP message. - * Usually, every message has a literal data packet, but for malformed messages this method might still - * return
null
. - * - * @return literal data - */ - private @Nullable LiteralData findLiteralData() { - Nested nested = message.getChild(); - if (nested == null) { - return null; - } - - while (nested != null && nested.hasNestedChild()) { - Layer layer = (Layer) nested; - nested = layer.getChild(); - } - return (LiteralData) nested; - } - - /** - * Return the {@link SubkeyIdentifier} of the decryption key that was used to decrypt the outermost encryption - * layer. - * If the message was unencrypted, this might return
null
. - * - * @return decryption key - */ - public SubkeyIdentifier getDecryptionKey() { - return firstOrNull(map(getEncryptionLayers(), encryptedData -> encryptedData.decryptionKey)); - } - - public boolean isVerifiedSigned() { - return !getVerifiedSignatures().isEmpty(); - } - - public interface Packet { - - } - public abstract static class Layer implements Packet { - public static final int MAX_LAYER_DEPTH = 16; - protected final int depth; - protected final List verifiedDetachedSignatures = new ArrayList<>(); - protected final List rejectedDetachedSignatures = new ArrayList<>(); - protected final List verifiedOnePassSignatures = new ArrayList<>(); - protected final List rejectedOnePassSignatures = new ArrayList<>(); - protected final List verifiedPrependedSignatures = new ArrayList<>(); - protected final List rejectedPrependedSignatures = new ArrayList<>(); - protected Nested child; - - public Layer(int depth) { - this.depth = depth; - if (depth > MAX_LAYER_DEPTH) { - throw new MalformedOpenPgpMessageException("Maximum packet nesting depth (" + MAX_LAYER_DEPTH + ") exceeded."); - } - } - - /** - * Return the nested child element of this layer. - * Might return
null
, if this layer does not have a child element - * (e.g. if this is a {@link LiteralData} packet). - * - * @return child element - */ - public @Nullable Nested getChild() { - return child; - } - - /** - * Set the nested child element for this layer. - * - * @param child child element - */ - void setChild(Nested child) { - this.child = child; - } - - /** - * Return a list of all verified detached signatures of this layer. - * - * @return all verified detached signatures of this layer - */ - public List getVerifiedDetachedSignatures() { - return new ArrayList<>(verifiedDetachedSignatures); - } - - /** - * Return a list of all rejected detached signatures of this layer. - * - * @return all rejected detached signatures of this layer - */ - public List getRejectedDetachedSignatures() { - return new ArrayList<>(rejectedDetachedSignatures); - } - - /** - * Add a verified detached signature for this layer. - * - * @param signatureVerification verified detached signature - */ - void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { - verifiedDetachedSignatures.add(signatureVerification); - } - - /** - * Add a rejected detached signature for this layer. - * - * @param failure rejected detached signature - */ - void addRejectedDetachedSignature(SignatureVerification.Failure failure) { - rejectedDetachedSignatures.add(failure); - } - - /** - * Return a list of all verified one-pass-signatures of this layer. - * - * @return all verified one-pass-signatures of this layer - */ - public List getVerifiedOnePassSignatures() { - return new ArrayList<>(verifiedOnePassSignatures); - } - - /** - * Return a list of all rejected one-pass-signatures of this layer. - * - * @return all rejected one-pass-signatures of this layer - */ - public List getRejectedOnePassSignatures() { - return new ArrayList<>(rejectedOnePassSignatures); - } - - /** - * Add a verified one-pass-signature for this layer. - * - * @param verifiedOnePassSignature verified one-pass-signature for this layer - */ - void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { - this.verifiedOnePassSignatures.add(verifiedOnePassSignature); - } - - /** - * Add a rejected one-pass-signature for this layer. - * - * @param rejected rejected one-pass-signature for this layer - */ - void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { - this.rejectedOnePassSignatures.add(rejected); - } - - /** - * Return a list of all verified prepended signatures of this layer. - * - * @return all verified prepended signatures of this layer - */ - public List getVerifiedPrependedSignatures() { - return new ArrayList<>(verifiedPrependedSignatures); - } - - /** - * Return a list of all rejected prepended signatures of this layer. - * - * @return all rejected prepended signatures of this layer - */ - public List getRejectedPrependedSignatures() { - return new ArrayList<>(rejectedPrependedSignatures); - } - - /** - * Add a verified prepended signature for this layer. - * - * @param verified verified prepended signature - */ - void addVerifiedPrependedSignature(SignatureVerification verified) { - this.verifiedPrependedSignatures.add(verified); - } - - /** - * Add a rejected prepended signature for this layer. - * - * @param rejected rejected prepended signature - */ - void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { - this.rejectedPrependedSignatures.add(rejected); - } - - } - - public interface Nested extends Packet { - boolean hasNestedChild(); - } - - public static class Message extends Layer { - - protected boolean cleartextSigned; - - public Message() { - super(0); - } - - /** - * Returns true, is the message is a signed message using the cleartext signature framework. - * - * @return
true
if message is cleartext-signed,
false
otherwise - * @see RFC4880 §7. Cleartext Signature Framework - */ - public boolean isCleartextSigned() { - return cleartextSigned; - } - - } - - public static class LiteralData implements Nested { - protected String fileName; - protected Date modificationDate; - protected StreamEncoding format; - - public LiteralData() { - this("", new Date(0L), StreamEncoding.BINARY); - } - - public LiteralData(@Nonnull String fileName, - @Nonnull Date modificationDate, - @Nonnull StreamEncoding format) { - this.fileName = fileName; - this.modificationDate = modificationDate; - this.format = format; - } - - /** - * Return the value of the filename field. - * An empty String
""
indicates no filename. - * - * @return filename - */ - public @Nonnull String getFileName() { - return fileName; - } - - /** - * Return the value of the modification date field. - * A special date
{@code new Date(0L)}
indicates no modification date. - * - * @return modification date - */ - public @Nonnull Date getModificationDate() { - return modificationDate; - } - - /** - * Return the value of the format field. - * - * @return format - */ - public @Nonnull StreamEncoding getFormat() { - return format; - } - - @Override - public boolean hasNestedChild() { - // A literal data packet MUST NOT have a child element, as its content is the plaintext - return false; - } - } - - public static class CompressedData extends Layer implements Nested { - protected final CompressionAlgorithm algorithm; - - public CompressedData(@Nonnull CompressionAlgorithm zip, int depth) { - super(depth); - this.algorithm = zip; - } - - /** - * Return the {@link CompressionAlgorithm} used to compress the packet. - * @return compression algorithm - */ - public @Nonnull CompressionAlgorithm getAlgorithm() { - return algorithm; - } - - @Override - public boolean hasNestedChild() { - // A compressed data packet MUST have a child element - return true; - } - } - - public static class EncryptedData extends Layer implements Nested { - protected final SymmetricKeyAlgorithm algorithm; - protected SubkeyIdentifier decryptionKey; - protected SessionKey sessionKey; - protected List recipients; - - public EncryptedData(@Nonnull SymmetricKeyAlgorithm algorithm, int depth) { - super(depth); - this.algorithm = algorithm; - } - - /** - * Return the {@link SymmetricKeyAlgorithm} used to encrypt the packet. - * @return symmetric encryption algorithm - */ - public @Nonnull SymmetricKeyAlgorithm getAlgorithm() { - return algorithm; - } - - /** - * Return the {@link SessionKey} used to decrypt the packet. - * - * @return session key - */ - public @Nonnull SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Return a list of all recipient key ids to which the packet was encrypted for. - * - * @return recipients - */ - public @Nonnull List getRecipients() { - if (recipients == null) { - return new ArrayList<>(); - } - return new ArrayList<>(recipients); - } - - @Override - public boolean hasNestedChild() { - // An encrypted data packet MUST have a child element - return true; - } - } - - - private abstract static class LayerIterator implements Iterator { - private Nested current; - Layer last = null; - Message parent; - - LayerIterator(@Nonnull Message message) { - super(); - this.parent = message; - this.current = message.getChild(); - if (matches(current)) { - last = (Layer) current; - } - } - - @Override - public boolean hasNext() { - if (parent != null && matches(parent)) { - return true; - } - if (last == null) { - findNext(); - } - return last != null; - } - - @Override - public O next() { - if (parent != null && matches(parent)) { - O property = getProperty(parent); - parent = null; - return property; - } - if (last == null) { - findNext(); - } - if (last != null) { - O property = getProperty(last); - last = null; - return property; - } - throw new NoSuchElementException(); - } - - private void findNext() { - while (current != null && current instanceof Layer) { - current = ((Layer) current).getChild(); - if (matches(current)) { - last = (Layer) current; - break; - } - } - } - - abstract boolean matches(Packet layer); - - abstract O getProperty(Layer last); - } - - private static Iterator map(Iterator from, Function mapping) { - return new Iterator() { - @Override - public boolean hasNext() { - return from.hasNext(); - } - - @Override - public B next() { - return mapping.apply(from.next()); - } - }; - } - - public interface Function { - B apply(A item); - } - - private static @Nullable A firstOrNull(Iterator iterator) { - if (iterator.hasNext()) { - return iterator.next(); - } - return null; - } - - private static @Nonnull A firstOr(Iterator iterator, A item) { - if (iterator.hasNext()) { - return iterator.next(); - } - return item; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt new file mode 100644 index 00000000..c4f31f4c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -0,0 +1,528 @@ +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPLiteralData +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.util.SessionKey +import java.util.* +import javax.annotation.Nonnull + +/** + * View for extracting metadata about a [Message]. + */ +class MessageMetadata( + val message: Message +) { + + // ################################################################################################################ + // ### Encryption ### + // ################################################################################################################ + + /** + * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is unencrypted. + */ + val encryptionAlgorithm: SymmetricKeyAlgorithm? + get() = encryptionAlgorithms.let { + if (it.hasNext()) it.next() else null + } + + /** + * [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item + * that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + */ + val encryptionAlgorithms: Iterator + get() = encryptionLayers.asSequence().map { it.algorithm }.iterator() + + val isEncrypted: Boolean + get() = if (encryptionAlgorithm == null) false else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL + + fun isEncryptedFor(keys: PGPKeyRing): Boolean { + return encryptionLayers.asSequence().any { + it.recipients.any { keyId -> + keys.getPublicKey(keyId) != null + } + } + } + + /** + * [SessionKey] of the outermost encrypted data packet. + * If the message was unencrypted, this method returns `null`. + */ + val sessionKey: SessionKey? + get() = sessionKeys.asSequence().firstOrNull() + + /** + * [Iterator] of each [SessionKey] for all encrypted data packets in the message. + * The first item returned by the iterator is the session key of the outermost encrypted data packet, + * the next item that of the next nested encrypted data packet and so on. + * The iterator might also be empty, in case of an unencrypted message. + */ + val sessionKeys: Iterator + get() = encryptionLayers.asSequence().mapNotNull { it.sessionKey }.iterator() + + /** + * [SubkeyIdentifier] of the decryption key that was used to decrypt the outermost encryption + * layer. + * If the message was unencrypted or was decrypted using a passphrase, this field might be `null`. + */ + val decryptionKey: SubkeyIdentifier? + get() = encryptionLayers.asSequence() + .mapNotNull { it.decryptionKey } + .firstOrNull() + + /** + * List containing all recipient keyIDs. + */ + val recipientKeyIds: List + get() = encryptionLayers.asSequence() + .map { it.recipients.toMutableList() } + .reduce { all, keyIds -> all.addAll(keyIds); all } + .toList() + + val encryptionLayers: Iterator + get() = object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is EncryptedData + override fun getProperty(last: Layer) = last as EncryptedData + } + + // ################################################################################################################ + // ### Compression ### + // ################################################################################################################ + + /** + * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message + * does not contain any compressed data packets. + */ + val compressionAlgorithm: CompressionAlgorithm? = compressionAlgorithms.asSequence().firstOrNull() + + /** + * [Iterator] of each [CompressionAlgorithm] encountered in the message. + * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next + * item that of the next nested compressed data packet and so on. + * The iterator might also be empty, in case of a message without any compressed data packets. + */ + val compressionAlgorithms: Iterator + get() = compressionLayers.asSequence().map { it.algorithm }.iterator() + + val compressionLayers: Iterator + get() = object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is CompressedData + override fun getProperty(last: Layer) = last as CompressedData + } + + // ################################################################################################################ + // ### Signatures ### + // ################################################################################################################ + + val isUsingCleartextSignatureFramework: Boolean + get() = message.cleartextSigned + + val verifiedSignatures: List + get() = verifiedInlineSignatures.plus(verifiedDetachedSignatures) + + /** + * List of all rejected signatures. + */ + val rejectedSignatures: List + get() = mutableListOf() + .plus(rejectedInlineSignatures) + .plus(rejectedDetachedSignatures) + .toList() + + /** + * List of all verified inline-signatures. + * This list contains all acceptable, correct signatures that were part of the message itself. + */ + val verifiedInlineSignatures: List = verifiedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, signatureVerifications -> acc.addAll(signatureVerifications); acc } + .toList() + + /** + * [Iterator] of each [List] of verified inline-signatures of the message, separated by layer. + * Since signatures might occur in different layers within a message, this method can be used to gain more detailed + * insights into what signatures were encountered at what layers of the message structure. + * Each item of the [Iterator] represents a layer of the message and contains only signatures from + * this layer. + * An empty list means no (or no acceptable) signatures were encountered in that layer. + */ + val verifiedInlineSignaturesByLayer: Iterator> + get() = object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List { + return listOf() + .plus(last.verifiedOnePassSignatures) + .plus(last.verifiedPrependedSignatures) + } + + } + + /** + * List of all rejected inline-signatures of the message. + */ + val rejectedInlineSignatures: List = rejectedInlineSignaturesByLayer + .asSequence() + .map { it.toMutableList() } + .reduce { acc, failures -> acc.addAll(failures); acc} + .toList() + + /** + * Similar to [verifiedInlineSignaturesByLayer], this field contains all rejected inline-signatures + * of the message, but organized by layer. + */ + val rejectedInlineSignaturesByLayer: Iterator> + get() = object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer + + override fun getProperty(last: Layer): List = + mutableListOf() + .plus(last.rejectedOnePassSignatures) + .plus(last.rejectedPrependedSignatures) + } + + /** + * List of all verified detached signatures. + * This list contains all acceptable, correct detached signatures. + */ + val verifiedDetachedSignatures: List = message.verifiedDetachedSignatures + + /** + * List of all rejected detached signatures. + */ + val rejectedDetachedSignatures: List = message.rejectedDetachedSignatures + + /** + * True, if the message contains any (verified or rejected) signature, false if no signatures are present. + */ + val hasSignature: Boolean + get() = isVerifiedSigned() || hasRejectedSignatures() + + fun isVerifiedSigned(): Boolean = verifiedSignatures.isNotEmpty() + + fun hasRejectedSignatures(): Boolean = rejectedSignatures.isNotEmpty() + + /** + * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * + * @param userId userId + * @param email if true, treat the user-id as an email address and match all userIDs containing this address + * @param certificateAuthority certificate authority + * @param targetAmount targeted trust amount that needs to be reached by the binding to qualify as authenticated. + * defaults to 120. + * @return true, if we can authenticate a binding for a signing key with sufficient evidence + */ + @JvmOverloads + fun isAuthenticatablySignedBy(userId: String, email: Boolean, certificateAuthority: CertificateAuthority, targetAmount: Int = 120): Boolean { + return verifiedSignatures.any { + certificateAuthority.authenticateBinding( + it.signingKey.fingerprint, userId, email, it.signature.creationTime, targetAmount + ).authenticated + } + } + + /** + * Return rue, if the message was verifiable signed by a certificate that either has the given fingerprint + * as primary key, or as the signing subkey. + * + * @param fingerprint fingerprint + * @return true if message was signed by a cert identified by the given fingerprint + */ + fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedSignatures, keys) + + fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedDetachedSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedDetachedSignatures, keys) + + fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = verifiedInlineSignatures.any { + it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint + } + + fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedInlineSignatures, keys) + + private fun containsSignatureBy(signatures: List, keys: PGPKeyRing) = + signatures.any { + // Match certificate by primary key id + keys.publicKey.keyID == it.signingKey.primaryKeyId && + // match signing subkey + keys.getPublicKey(it.signingKey.subkeyId) != null + } + + // ################################################################################################################ + // ### Literal Data ### + // ################################################################################################################ + + /** + * Value of the literal data packet's filename field. + * This value can be used to store a decrypted file under its original filename, + * but since this field is not necessarily part of the signed data of a message, usage of this field is + * discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val filename: String? = findLiteralData()?.fileName + + /** + * True, if the sender signals an increased degree of confidentiality by setting the filename of the literal + * data packet to a special value that indicates that the data is intended for your eyes only. + */ + @Deprecated("Reliance on this signaling mechanism is discouraged.") + val isForYourEyesOnly: Boolean = PGPLiteralData.CONSOLE == filename + + /** + * Value of the literal data packets modification date field. + * This value can be used to restore the modification date of a decrypted file, + * but since this field is not necessarily part of the signed data, its use is discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val modificationDate: Date? = findLiteralData()?.modificationDate + + /** + * Value of the format field of the literal data packet. + * This value indicates what format (text, binary data, ...) the data has. + * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. + * + * @see RFC4880 §5.9. Literal Data Packet + */ + val literalDataEncoding: StreamEncoding? = findLiteralData()?.format + + /** + * Find the [LiteralData] layer of an OpenPGP message. + * This method might return null, for example for a cleartext signed message without OpenPGP packets. + * + * @return literal data + */ + private fun findLiteralData(): LiteralData? { + // If the message is a non-OpenPGP message with a detached signature, or a Cleartext Signed message, + // we might not have a Literal Data packet. + var nested = message.child ?: return null + + while (nested.hasNestedChild()) { + val layer = nested as Layer + nested = checkNotNull(layer.child) { + // Otherwise, we MUST find a Literal Data packet, or else the message is malformed + "Malformed OpenPGP message. Cannot find Literal Data Packet" + } + } + return nested as LiteralData + } + + // ################################################################################################################ + // ### Message Structure ### + // ################################################################################################################ + + interface Packet + + interface Nested : Packet { + fun hasNestedChild(): Boolean + } + + abstract class Layer( + val depth: Int + ) : Packet { + + init { + if (depth > MAX_LAYER_DEPTH) { + throw MalformedOpenPgpMessageException("Maximum packet nesting depth ($MAX_LAYER_DEPTH) exceeded.") + } + } + + val verifiedDetachedSignatures: List = mutableListOf() + val rejectedDetachedSignatures: List = mutableListOf() + val verifiedOnePassSignatures: List = mutableListOf() + val rejectedOnePassSignatures: List = mutableListOf() + val verifiedPrependedSignatures: List = mutableListOf() + val rejectedPrependedSignatures: List = mutableListOf() + + /** + * Nested child element of this layer. + * Might be `null`, if this layer does not have a child element + * (e.g. if this is a [LiteralData] packet). + */ + var child: Nested? = null + + fun addVerifiedDetachedSignature(signature: SignatureVerification) = apply { + (verifiedDetachedSignatures as MutableList).add(signature) + } + + fun addRejectedDetachedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedDetachedSignatures as MutableList).add(failure) + } + + fun addVerifiedOnePassSignature(signature: SignatureVerification) = apply { + (verifiedOnePassSignatures as MutableList).add(signature) + } + + fun addRejectedOnePassSignature(failure: SignatureVerification.Failure) = apply { + (rejectedOnePassSignatures as MutableList).add(failure) + } + + fun addVerifiedPrependedSignature(signature: SignatureVerification) = apply { + (verifiedPrependedSignatures as MutableList).add(signature) + } + + fun addRejectedPrependedSignature(failure: SignatureVerification.Failure) = apply { + (rejectedPrependedSignatures as MutableList).add(failure) + } + + companion object { + const val MAX_LAYER_DEPTH = 16 + } + } + + /** + * Outermost OpenPGP Message structure. + * + * @param cleartextSigned whether the message is using the Cleartext Signature Framework + * + * @see RFC4880 §7. Cleartext Signature Framework + */ + class Message(var cleartextSigned: Boolean = false) : Layer(0) { + fun setCleartextSigned() = apply { cleartextSigned = true } + } + + /** + * Literal Data Packet. + * + * @param fileName value of the filename field. An empty String represents no filename. + * @param modificationDate value of the modification date field. The special value `Date(0)` indicates no + * modification date. + * @param format value of the format field. + */ + class LiteralData( + val fileName: String = "", + val modificationDate: Date = Date(0L), + val format: StreamEncoding = StreamEncoding.BINARY + ) : Nested { + + // A literal data packet MUST NOT have a child element, as its content is the plaintext + override fun hasNestedChild() = false + } + + /** + * Compressed Data Packet. + * + * @param algorithm [CompressionAlgorithm] used to compress the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class CompressedData( + val algorithm: CompressionAlgorithm, + depth: Int) : Layer(depth), Nested { + + // A compressed data packet MUST have a child element + override fun hasNestedChild() = true + } + + /** + * Encrypted Data. + * + * @param algorithm symmetric key algorithm used to encrypt the packet. + * @param depth nesting depth at which this packet was encountered. + */ + class EncryptedData( + val algorithm: SymmetricKeyAlgorithm, + depth: Int + ) : Layer(depth), Nested { + + /** + * [SessionKey] used to decrypt the packet. + */ + var sessionKey: SessionKey? = null + + /** + * List of all recipient key ids to which the packet was encrypted for. + */ + val recipients: List = mutableListOf() + + fun addRecipients(keyIds: List) = apply { + (recipients as MutableList).addAll(keyIds) + } + + /** + * Identifier of the subkey that was used to decrypt the packet (in case of a public key encrypted packet). + */ + var decryptionKey: SubkeyIdentifier? = null + + // An encrypted data packet MUST have a child element + override fun hasNestedChild() = true + + } + + /** + * Iterator that iterates the packet structure from outermost to innermost packet, emitting the results of + * a transformation ([getProperty]) on those packets that match ([matches]) a given criterion. + * + * @param message outermost structure object + */ + private abstract class LayerIterator(@Nonnull message: Message) : Iterator { + private var current: Nested? + var last: Layer? = null + var parent: Message? + + init { + parent = message + current = message.child + current?.let { + if (matches(it)) { + last = current as Layer + } + } + } + + override fun hasNext(): Boolean { + parent?.let { + if (matches(it)) { + return true + } + } + if (last == null) { + findNext() + } + return last != null + } + + override fun next(): O { + parent?.let { + if (matches(it)) { + return getProperty(it).also { parent = null } + } + } + if (last == null) { + findNext() + } + last?.let { + return getProperty(it).also { last = null } + } + throw NoSuchElementException() + } + + private fun findNext() { + while (current != null && current is Layer) { + current = (current as Layer).child + if (current != null && matches(current!!)) { + last = current as Layer + break + } + } + } + + abstract fun matches(layer: Packet): Boolean + abstract fun getProperty(last: Layer): O + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 4b7b1f1f..87b0847a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -156,9 +156,9 @@ class OpenPgpMessageInputStream( val literalData = packetInputStream!!.readLiteralData() // Extract Metadata - layerMetadata.setChild(LiteralData( + layerMetadata.child = LiteralData( literalData.fileName, literalData.modificationTime, - StreamEncoding.requireFromCode(literalData.format))) + StreamEncoding.requireFromCode(literalData.format)) nestedInputStream = literalData.inputStream } @@ -394,7 +394,7 @@ class OpenPgpMessageInputStream( throwIfUnacceptable(sessionKey.algorithm) val encryptedData = EncryptedData(sessionKey.algorithm, layerMetadata.depth + 1) encryptedData.sessionKey = sessionKey - encryptedData.recipients = esks.pkesks.map { it.keyID } + encryptedData.addRecipients(esks.pkesks.map { it.keyID }) LOGGER.debug("Successfully decrypted data with passphrase") val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) @@ -421,7 +421,7 @@ class OpenPgpMessageInputStream( layerMetadata.depth + 1) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey - encryptedData.recipients = esks.pkesks.plus(esks.anonPkesks).map { it.keyID } + encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID }) LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) @@ -522,7 +522,7 @@ class OpenPgpMessageInputStream( private fun collectMetadata() { if (nestedInputStream is OpenPgpMessageInputStream) { val child = nestedInputStream as OpenPgpMessageInputStream - layerMetadata.setChild(child.layerMetadata as Nested) + layerMetadata.child = (child.layerMetadata as Nested) } } @@ -620,8 +620,7 @@ class OpenPgpMessageInputStream( } else { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key."))) + signature, null, SignatureValidationException("Missing verification key."))) } } @@ -633,8 +632,7 @@ class OpenPgpMessageInputStream( } else { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key") + signature, null, SignatureValidationException("Missing verification key") )) } } @@ -695,8 +693,7 @@ class OpenPgpMessageInputStream( if (!found) { LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( - SignatureVerification(signature, null), - SignatureValidationException("Missing verification key.") + signature, null, SignatureValidationException("Missing verification key.") )) } } @@ -890,7 +887,7 @@ class OpenPgpMessageInputStream( return if (openPgpIn.isAsciiArmored) { val armorIn = ArmoredInputStreamFactory.get(openPgpIn) if (armorIn.isClearText) { - (metadata as Message).cleartextSigned = true + (metadata as Message).setCleartextSigned() OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy) } else { // Simply consume dearmored OpenPGP message diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt index a188f6a5..8d229fb2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -22,12 +22,12 @@ import org.pgpainless.signature.SignatureUtils */ data class SignatureVerification( val signature: PGPSignature, - val signingKey: SubkeyIdentifier? + val signingKey: SubkeyIdentifier ) { override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + - " Key: ${signingKey?.toString() ?: "null"};" + " Key: $signingKey;" } /** @@ -38,11 +38,16 @@ data class SignatureVerification( * @param validationException exception that caused the verification to fail */ data class Failure( - val signatureVerification: SignatureVerification, + val signature: PGPSignature, + val signingKey: SubkeyIdentifier?, val validationException: SignatureValidationException ) { + + constructor(verification: SignatureVerification, validationException: SignatureValidationException): + this(verification.signature, verification.signingKey, validationException) + override fun toString(): String { - return "$signatureVerification Failure: ${validationException.message}" + return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" } } } \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index 771cb8f1..2c29d62a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -28,9 +28,9 @@ public class MessageMetadataTest { // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); - MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); - MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); - MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.getDepth() + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.getDepth() + 1); MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); message.setChild(compressedData); From a268cfd51acccf7ed3aa0d3844670a1d88bdc4f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 14:45:45 +0200 Subject: [PATCH 120/351] Add missing license header --- .../org/pgpainless/decryption_verification/MessageMetadata.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt index c4f31f4c..8719cef7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.decryption_verification import org.bouncycastle.openpgp.PGPKeyRing From 1234c8800ab3bad1cd9fc7e6c699dabacbf499f1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 14:48:15 +0200 Subject: [PATCH 121/351] Kotlin conversion: MissingPublicKeyCallback --- .../MissingPublicKeyCallback.kt} | 19 +++++++------------ ...erifyWithMissingPublicKeyCallbackTest.java | 5 ++--- 2 files changed, 9 insertions(+), 15 deletions(-) rename pgpainless-core/src/main/{java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java => kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt} (67%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt similarity index 67% rename from pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt index 9b6f4a1e..723530b7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt @@ -1,33 +1,28 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.decryption_verification; +package org.pgpainless.decryption_verification -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPPublicKeyRing; - -public interface MissingPublicKeyCallback { +fun interface MissingPublicKeyCallback { /** * This method gets called if we encounter a signature made by a key which was not provided for signature verification. * If you cannot provide the requested key, it is safe to return null here. * PGPainless will then continue verification with the next signature. * - * Note: The key-id might belong to a subkey, so be aware that when looking up the {@link PGPPublicKeyRing}, + * Note: The key-id might belong to a subkey, so be aware that when looking up the [PGPPublicKeyRing], * you may not only search for the key-id on the key rings primary key! * * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures * only contain the key id. * * @param keyId ID of the missing signing (sub)key - * * @return keyring containing the key or null * * @see RFC */ - @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); - -} + fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing? +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java index 52877626..0d58e7dd 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifyWithMissingPublicKeyCallbackTest.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; @@ -63,8 +62,8 @@ public class VerifyWithMissingPublicKeyCallbackTest { .addVerificationCert(unrelatedKeys) .setMissingCertificateCallback(new MissingPublicKeyCallback() { @Override - public PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId) { - assertEquals(signingKey.getKeyID(), (long) keyId, "Signing key-ID mismatch."); + public PGPPublicKeyRing onMissingPublicKeyEncountered(long keyId) { + assertEquals(signingKey.getKeyID(), keyId, "Signing key-ID mismatch."); return signingPubKeys; } })); From 6e653f3c920ec8853249f283e000ea2939c974bb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 14:50:31 +0200 Subject: [PATCH 122/351] Kotlin conversion: MissingKeyPassphraseStrategy --- .../MissingKeyPassphraseStrategy.java | 21 ------------------ .../MissingKeyPassphraseStrategy.kt | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java deleted file mode 100644 index ed6a9c63..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -/** - * Strategy defining how missing secret key passphrases are handled. - */ -public enum MissingKeyPassphraseStrategy { - /** - * Try to interactively obtain key passphrases one-by-one via callbacks, - * eg {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider}. - */ - INTERACTIVE, - /** - * Do not try to obtain passphrases interactively and instead throw a - * {@link org.pgpainless.exception.MissingPassphraseException} listing all keys with missing passphrases. - */ - THROW_EXCEPTION -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt new file mode 100644 index 00000000..f8bb448b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +/** + * Strategy defining how missing secret key passphrases are handled. + */ +enum class MissingKeyPassphraseStrategy { + /** + * Try to interactively obtain key passphrases one-by-one via callbacks, + * eg [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider]. + */ + INTERACTIVE, + + /** + * Do not try to obtain passphrases interactively and instead throw a + * [org.pgpainless.exception.MissingPassphraseException] listing all keys with missing passphrases. + */ + THROW_EXCEPTION +} \ No newline at end of file From 5441993baf885ee26b6c3e23ab332e15b87b0d58 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 15:53:45 +0200 Subject: [PATCH 123/351] Kotlin conversion: SigningOptions --- .../encryption_signing/SigningOptions.java | 634 ------------------ .../encryption_signing/SigningOptions.kt | 451 +++++++++++++ 2 files changed, 451 insertions(+), 634 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt 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 deleted file mode 100644 index 34d4bbcf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SigningOptions.java +++ /dev/null @@ -1,634 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -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.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.exception.KeyException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.subpackets.BaseSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; - -public final class SigningOptions { - - /** - * A method of signing. - */ - public static final class SigningMethod { - private final PGPSignatureGenerator signatureGenerator; - private final boolean detached; - private final HashAlgorithm hashAlgorithm; - - private SigningMethod(@Nonnull PGPSignatureGenerator signatureGenerator, - boolean detached, - @Nonnull HashAlgorithm hashAlgorithm) { - this.signatureGenerator = signatureGenerator; - this.detached = detached; - this.hashAlgorithm = hashAlgorithm; - } - - /** - * Inline-signature method. - * The resulting signature will be written into the message itself, together with a one-pass-signature packet. - * - * @param signatureGenerator signature generator - * @param hashAlgorithm hash algorithm used to generate the signature - * @return inline signing method - */ - public static SigningMethod inlineSignature(@Nonnull PGPSignatureGenerator signatureGenerator, - @Nonnull HashAlgorithm hashAlgorithm) { - return new SigningMethod(signatureGenerator, false, hashAlgorithm); - } - - /** - * 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 - * @param hashAlgorithm hash algorithm used to generate the signature - * @return detached signing method - */ - public static SigningMethod detachedSignature(@Nonnull PGPSignatureGenerator signatureGenerator, - @Nonnull HashAlgorithm hashAlgorithm) { - return new SigningMethod(signatureGenerator, true, hashAlgorithm); - } - - public boolean isDetached() { - return detached; - } - - public PGPSignatureGenerator getSignatureGenerator() { - return signatureGenerator; - } - - public HashAlgorithm getHashAlgorithm() { - return hashAlgorithm; - } - } - - private final Map signingMethods = new HashMap<>(); - private HashAlgorithm hashAlgorithmOverride; - private Date evaluationDate = new Date(); - - @Nonnull - public static SigningOptions get() { - return new SigningOptions(); - } - - /** - * Override the evaluation date for signing keys with the given date. - * - * @param evaluationDate new evaluation date - * @return this - */ - public SigningOptions setEvaluationDate(@Nonnull Date evaluationDate) { - this.evaluationDate = evaluationDate; - return this; - } - - /** - * Sign the message using an inline signature made by the provided signing key. - * - * @param signingKeyProtector protector to unlock the signing key - * @param signingKey key ring containing the signing key - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be unlocked or a signing method cannot be created - */ - @Nonnull - public SigningOptions addSignature(@Nonnull SecretKeyRingProtector signingKeyProtector, - @Nonnull PGPSecretKeyRing signingKey) - throws PGPException { - return addInlineSignature(signingKeyProtector, signingKey, DocumentSignatureType.BINARY_DOCUMENT); - } - - /** - * Add inline signatures with all secret key rings in the provided secret key ring collection. - * - * @param secrectKeyDecryptor decryptor to unlock the signing secret keys - * @param signingKeys collection of signing keys - * @param signatureType type of signature (binary, canonical text) - * @return this - * - * @throws KeyException if something is wrong with any of the keys - * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created - */ - @Nonnull - public SigningOptions addInlineSignatures(@Nonnull SecretKeyRingProtector secrectKeyDecryptor, - @Nonnull Iterable signingKeys, - @Nonnull DocumentSignatureType signatureType) - throws KeyException, PGPException { - for (PGPSecretKeyRing signingKey : signingKeys) { - addInlineSignature(secrectKeyDecryptor, signingKey, signatureType); - } - return this; - } - - /** - * 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) - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nonnull DocumentSignatureType signatureType) - throws KeyException, PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, null, 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 KeyException if something is wrong with the key - * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType) - throws KeyException, PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); - } - - /** - * 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) - * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the signature - * @return this - * - * @throws KeyException if the key is invalid - * @throws PGPException if the key cannot be unlocked or the signing method cannot be created - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) - throws KeyException, PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId.toString(), - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) - : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, false); - } - - return this; - } - - /** - * Create a binary inline signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId) throws PGPException { - return addInlineSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); - } - - - /** - * Create an inline signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @param signatureType signature type - * @param subpacketsCallback callback to modify the signatures subpackets - * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key - */ - @Nonnull - public SigningOptions addInlineSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - if (signingPubKey.getKeyID() == keyId) { - - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, false); - return this; - } - } - - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); - } - - /** - * Add detached signatures with all key rings from the provided secret key ring collection. - * - * @param secretKeyDecryptor decryptor to unlock the secret signing keys - * @param signingKeys collection of signing key rings - * @param signatureType type of the signature (binary, canonical text) - * @return this - * - * @throws KeyException if something is wrong with any of the keys - * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created - */ - @Nonnull - public SigningOptions addDetachedSignatures(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull Iterable signingKeys, - @Nonnull DocumentSignatureType signatureType) - throws PGPException { - for (PGPSecretKeyRing signingKey : signingKeys) { - addDetachedSignature(secretKeyDecryptor, signingKey, signatureType); - } - return this; - } - - /** - * Create a detached signature. - * The signature will be of type {@link DocumentSignatureType#BINARY_DOCUMENT}. - * - * @param secretKeyDecryptor decryptor to unlock the secret signing key - * @param signingKey signing key - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing signingKey) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, signingKey, DocumentSignatureType.BINARY_DOCUMENT); - } - - /** - * 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 (e.g. 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) - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nonnull DocumentSignatureType signatureType) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, null, 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 (e.g. 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) - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType) - throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, userId, signatureType, null); - } - - /** - * 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 (e.g. 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) - * @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature - * @return this - * - * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - @Nullable CharSequence userId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - if (userId != null && !keyRingInfo.isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(secretKey), - userId.toString(), - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ); - } - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = userId != null ? keyRingInfo.getPreferredHashAlgorithms(userId) - : keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketCallback, hashAlgorithm, signatureType, true); - } - - return this; - } - - /** - * Create a detached binary signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId) throws PGPException { - return addDetachedSignature(secretKeyDecryptor, secretKey, keyId, DocumentSignatureType.BINARY_DOCUMENT, null); - } - - /** - * Create a detached signature using the signing key with the given keyId. - * - * @param secretKeyDecryptor decryptor to unlock the secret key - * @param secretKey secret key ring - * @param keyId keyId of the signing (sub-)key - * @param signatureType signature type - * @param subpacketsCallback callback to modify the signatures subpackets - * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key - */ - @Nonnull - public SigningOptions addDetachedSignature(@Nonnull SecretKeyRingProtector secretKeyDecryptor, - @Nonnull PGPSecretKeyRing secretKey, - long keyId, - @Nonnull DocumentSignatureType signatureType, - @Nullable BaseSignatureSubpackets.Callback subpacketsCallback) throws PGPException { - KeyRingInfo keyRingInfo = PGPainless.inspectKeyRing(secretKey, evaluationDate); - - List signingPubKeys = keyRingInfo.getSigningSubkeys(); - if (signingPubKeys.isEmpty()) { - throw new KeyException.UnacceptableSigningKeyException(OpenPgpFingerprint.of(secretKey)); - } - - for (PGPPublicKey signingPubKey : signingPubKeys) { - if (signingPubKey.getKeyID() == keyId) { - - PGPSecretKey signingSecKey = secretKey.getSecretKey(signingPubKey.getKeyID()); - if (signingSecKey == null) { - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), signingPubKey.getKeyID()); - } - PGPPrivateKey signingSubkey = UnlockSecretKey.unlockSecretKey(signingSecKey, secretKeyDecryptor); - Set hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.getKeyID()); - HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, PGPainless.getPolicy()); - addSigningMethod(secretKey, signingSubkey, subpacketsCallback, hashAlgorithm, signatureType, true); - return this; - } - } - - throw new KeyException.MissingSecretKeyException(OpenPgpFingerprint.of(secretKey), keyId); - } - - private void addSigningMethod(@Nonnull PGPSecretKeyRing secretKey, - @Nonnull PGPPrivateKey signingSubkey, - @Nullable BaseSignatureSubpackets.Callback subpacketCallback, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull DocumentSignatureType signatureType, - boolean detached) - throws PGPException { - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(secretKey, signingSubkey.getKeyID()); - PGPSecretKey signingSecretKey = secretKey.getSecretKey(signingSubkey.getKeyID()); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(signingSecretKey.getPublicKey().getAlgorithm()); - int bitStrength = signingSecretKey.getPublicKey().getBitStrength(); - if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new KeyException.UnacceptableSigningKeyException( - new KeyException.PublicKeyAlgorithmPolicyException( - OpenPgpFingerprint.of(secretKey), signingSecretKey.getKeyID(), publicKeyAlgorithm, bitStrength)); - } - - PGPSignatureGenerator generator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType); - - // Subpackets - SignatureSubpackets hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingSecretKey.getPublicKey()); - SignatureSubpackets unhashedSubpackets = SignatureSubpackets.createEmptySubpackets(); - if (subpacketCallback != null) { - subpacketCallback.modifyHashedSubpackets(hashedSubpackets); - subpacketCallback.modifyUnhashedSubpackets(unhashedSubpackets); - } - generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)); - generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)); - - SigningMethod signingMethod = detached ? - SigningMethod.detachedSignature(generator, hashAlgorithm) : - SigningMethod.inlineSignature(generator, hashAlgorithm); - signingMethods.put(signingKeyIdentifier, signingMethod); - } - - /** - * Negotiate, which hash algorithm to use. - *

- * This method gives the 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 - */ - @Nonnull - private HashAlgorithm negotiateHashAlgorithm(@Nonnull Set preferences, - @Nonnull Policy policy) { - if (hashAlgorithmOverride != null) { - return hashAlgorithmOverride; - } - - return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(policy) - .negotiateHashAlgorithm(preferences); - } - - @Nonnull - private PGPSignatureGenerator createSignatureGenerator(@Nonnull PGPPrivateKey privateKey, - @Nonnull HashAlgorithm hashAlgorithm, - @Nonnull 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; - } - - /** - * Return a map of key-ids and signing methods. - * For internal use. - * - * @return signing methods - */ - @Nonnull - 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 - */ - @Nonnull - public SigningOptions overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithmOverride) { - this.hashAlgorithmOverride = hashAlgorithmOverride; - return this; - } - - /** - * Return the hash algorithm override (or null if no override is set). - * - * @return hash algorithm override - */ - @Nullable - public HashAlgorithm getHashAlgorithmOverride() { - return hashAlgorithmOverride; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt new file mode 100644 index 00000000..b9208ed5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -0,0 +1,451 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless.Companion.getPolicy +import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint.Companion.of +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper +import java.util.* + +class SigningOptions { + + val signingMethods: Map = mutableMapOf() + private var _hashAlgorithmOverride: HashAlgorithm? = null + private var _evaluationDate: Date = Date() + + val hashAlgorithmOverride: HashAlgorithm? + get() = _hashAlgorithmOverride + + /** + * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. + * If no override has been set, an acceptable algorithm will be negotiated instead. + * Note: To override the hash algorithm for signing, call this method *before* calling + * [addInlineSignature] or [addDetachedSignature]. + * + * @param hashAlgorithmOverride override hash algorithm + * @return this + */ + fun overrideHashAlgorithm(hashAlgorithmOverride: HashAlgorithm) = apply { + _hashAlgorithmOverride = hashAlgorithmOverride + } + + val evaluationDate: Date + get() = _evaluationDate + + /** + * Override the evaluation date for signing keys with the given date. + * + * @param evaluationDate new evaluation date + * @return this + */ + fun setEvaluationDate(evaluationDate: Date) = apply { + _evaluationDate = evaluationDate + } + + /** + * Sign the message using an inline signature made by the provided signing key. + * + * @param signingKeyProtector protector to unlock the signing key + * @param signingKey key ring containing the signing key + * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or a signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = apply { + addInlineSignature(signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) + } + + /** + * Add inline signatures with all secret key rings in the provided secret key ring collection. + * + * @param signingKeyProtector decryptor to unlock the signing secret keys + * @param signingKeys collection of signing keys + * @param signatureType type of signature (binary, canonical text) + * @return this + * + * @throws KeyException if something is wrong with any of the keys + * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addInlineSignatures(signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType) = apply { + signingKeys.forEach { + addInlineSignature(signingKeyProtector, it, null, 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 signingKeyProtector decryptor to unlock the signing secret key + * @param signingKey signing key + * @param signatureType type of signature (binary, canonical text) + * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType) = apply { + addInlineSignature(signingKeyProtector, signingKey, null, 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 signingKeyProtector decryptor to unlock the signing secret key + * @param signingKey signing key + * @param userId user-id of the signer + * @param signatureType signature type (binary, canonical text) + * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the signature + * @return this + * + * @throws KeyException if the key is invalid + * @throws PGPException if the key cannot be unlocked or the signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: CharSequence? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + if (userId != null && !keyRingInfo.isUserIdValid(userId)) { + throw UnboundUserIdException( + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId) + ) + } + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val hashAlgorithms = + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + } + } + + /** + * Create an inline signature using the signing key with the given keyId. + * + * @param signingKeyProtector decryptor to unlock the secret key + * @param signingKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + if (signingPubKey.keyID != keyId) { + continue + } + + val signingSecKey = signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey = unlockSecretKey(signingSecKey, signingKeyProtector) + val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + return this + } + throw MissingSecretKeyException(of(signingKey), keyId) + } + + /** + * Add detached signatures with all key rings from the provided secret key ring collection. + * + * @param signingKeyProtector decryptor to unlock the secret signing keys + * @param signingKeys collection of signing key rings + * @param signatureType type of the signature (binary, canonical text) + * @return this + * + * @throws KeyException if something is wrong with any of the keys + * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created + */ + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignatures(signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType) = apply { + signingKeys.forEach { + addDetachedSignature(signingKeyProtector, it, null, 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 (e.g. when signing a file). + * + * @param signingKeyProtector decryptor to unlock the secret signing key + * @param signingKey signing key + * @param signatureType type of data that is signed (binary, canonical text) + * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + */ + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType) = apply { + addDetachedSignature(signingKeyProtector, signingKey, null, 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 (e.g. when signing a file). + *

+ * This method uses the passed in user-id to select user-specific hash algorithms. + * + * @param signingKeyProtector decryptor to unlock the secret signing key + * @param signingKey signing key + * @param userId user-id + * @param signatureType type of data that is signed (binary, canonical text) + * @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature + * @return this + * + * @throws KeyException if something is wrong with the key + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + */ + @JvmOverloads + @Throws(KeyException::class, PGPException::class) + fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: String? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketCallback: Callback? = null) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + if (userId != null && !keyRingInfo.isUserIdValid(userId)) { + throw UnboundUserIdException( + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId) + ) + } + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val hashAlgorithms = + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback) + } + } + + /** + * Create a detached signature using the signing key with the given keyId. + * + * @param signingKeyProtector decryptor to unlock the secret key + * @param signingKey secret key ring + * @param keyId keyId of the signing (sub-)key + * @param signatureType signature type + * @param subpacketsCallback callback to modify the signatures subpackets + * @return builder + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + */ + @Throws(KeyException::class, PGPException::class) + @JvmOverloads + fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null) = apply { + val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) + + val signingPubKeys = keyRingInfo.signingSubkeys + if (signingPubKeys.isEmpty()) { + throw UnacceptableSigningKeyException(of(signingKey)) + } + + for (signingPubKey in signingPubKeys) { + if (signingPubKey.keyID == keyId) { + val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) + val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketsCallback) + return this + } + } + + throw MissingSecretKeyException(of(signingKey), keyId) + } + + private fun addSigningMethod(signingKey: PGPSecretKeyRing, + signingSubkey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType, + detached: Boolean, + subpacketCallback: Callback? = null) { + val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID) + val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID) + val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm) + val bitStrength = signingSecretKey.publicKey.bitStrength + if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) { + throw UnacceptableSigningKeyException( + PublicKeyAlgorithmPolicyException( + of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) + } + + val generator: PGPSignatureGenerator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) + + // Subpackets + val hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingSecretKey.publicKey) + val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets() + if (subpacketCallback != null) { + subpacketCallback.modifyHashedSubpackets(hashedSubpackets) + subpacketCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)) + generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)) + + val signingMethod = + if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) + else SigningMethod.inlineSignature(generator, hashAlgorithm) + (signingMethods as MutableMap)[signingKeyIdentifier] = signingMethod + } + + /** + * Negotiate, which hash algorithm to use. + * + * + * This method gives the highest priority to the algorithm override, which can be set via [.overrideHashAlgorithm]. + * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. + * Lastly, should no acceptable algorithm be found, the [Policies][Policy] default signature hash algorithm is + * used as a fallback. + * + * @param preferences preferences + * @param policy policy + * @return selected hash algorithm + */ + private fun negotiateHashAlgorithm(preferences: Set, + policy: Policy): HashAlgorithm { + return _hashAlgorithmOverride ?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences) + } + + @Throws(PGPException::class) + private fun createSignatureGenerator(privateKey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType): PGPSignatureGenerator { + return ImplementationFactory.getInstance() + .getPGPContentSignerBuilder(privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId) + .let { csb -> + PGPSignatureGenerator(csb).also { it.init(signatureType.signatureType.code, privateKey) } + } + } + + + companion object { + @JvmStatic + fun get() = SigningOptions() + } + + /** + * A method of signing. + */ + class SigningMethod private constructor( + val signatureGenerator: PGPSignatureGenerator, + val isDetached: Boolean, + val hashAlgorithm: HashAlgorithm + ) { + companion object { + + /** + * Inline-signature method. + * The resulting signature will be written into the message itself, together with a one-pass-signature packet. + * + * @param signatureGenerator signature generator + * @param hashAlgorithm hash algorithm used to generate the signature + * @return inline signing method + */ + @JvmStatic + fun inlineSignature(signatureGenerator: PGPSignatureGenerator, hashAlgorithm: HashAlgorithm) = + SigningMethod(signatureGenerator, false, hashAlgorithm) + + /** + * 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 + * @param hashAlgorithm hash algorithm used to generate the signature + * @return detached signing method + */ + @JvmStatic + fun detachedSignature(signatureGenerator: PGPSignatureGenerator, hashAlgorithm: HashAlgorithm) = + SigningMethod(signatureGenerator, true, hashAlgorithm) + } + } +} \ No newline at end of file From a4cd96596744db616066a70cdcd94e1052fb3598 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 16:22:51 +0200 Subject: [PATCH 124/351] Kotlin conversion: ProducerOptions --- .../encryption_signing/ProducerOptions.java | 355 ------------------ .../encryption_signing/ProducerOptions.kt | 291 ++++++++++++++ 2 files changed, 291 insertions(+), 355 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt 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 deleted file mode 100644 index 00fbb10a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; - -public final class ProducerOptions { - - private final EncryptionOptions encryptionOptions; - private final SigningOptions signingOptions; - private String fileName = ""; - private Date modificationDate = PGPLiteralData.NOW; - private StreamEncoding encodingField = StreamEncoding.BINARY; - private boolean applyCRLFEncoding = false; - private boolean cleartextSigned = false; - private boolean hideArmorHeaders = false; - - private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() - .defaultCompressionAlgorithm(); - private boolean asciiArmor = true; - private String comment = null; - private String version = null; - - 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); - } - - /** - * Only wrap the data in an OpenPGP packet. - * No encryption or signing will be applied. - * - * @return builder - */ - 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."); - } - } - - /** - * Specify, whether 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) { - if (cleartextSigned && !asciiArmor) { - throw new IllegalArgumentException("Cleartext signing is enabled. Cannot disable ASCII armoring."); - } - 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; - } - - /** - * Set the comment header in ASCII armored output. - * The default value is null, which means no comment header is added. - * Multiline comments are possible using '\\n'. - *
- * Note: If a default header comment is set using {@link org.pgpainless.util.ArmoredOutputStreamFactory#setComment(String)}, - * then both comments will be written to the produced ASCII armor. - * - * @param comment comment header text - * @return builder - */ - public ProducerOptions setComment(String comment) { - this.comment = comment; - return this; - } - - /** - * Set the version header in ASCII armored output. - * The default value is null, which means no version header is added. - *
- * Note: If the value is non-null, then this method overrides the default version header set using - * {@link org.pgpainless.util.ArmoredOutputStreamFactory#setVersionInfo(String)}. - * - * @param version version header, or null for no version info. - * @return builder - */ - public ProducerOptions setVersion(String version) { - this.version = version; - return this; - } - - /** - * Return comment set for header in ascii armored output. - * - * @return comment - */ - public String getComment() { - return comment; - } - - /** - * Return the version info header in ascii armored output. - * - * @return version info - */ - public String getVersion() { - return version; - } - - /** - * Return whether a comment was set (!= null). - * - * @return true if commend is set - */ - public boolean hasComment() { - return comment != null; - } - - /** - * Return whether a version header was set (!= null). - * - * @return true if version header is set - */ - public boolean hasVersion() { - return version != null; - } - - public ProducerOptions setCleartextSigned() { - if (signingOptions == null) { - throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled."); - } - if (encryptionOptions != null) { - throw new IllegalArgumentException("Cannot encode encrypted message as Cleartext Signed."); - } - for (SigningOptions.SigningMethod method : signingOptions.getSigningMethods().values()) { - if (!method.isDetached()) { - throw new IllegalArgumentException("For cleartext signed message, all signatures must be added as detached signatures."); - } - } - cleartextSigned = true; - asciiArmor = true; - compressionAlgorithmOverride = CompressionAlgorithm.UNCOMPRESSED; - return this; - } - - public boolean isCleartextSigned() { - return cleartextSigned; - } - - /** - * Set the name of the encrypted file. - * Note: This option cannot be used simultaneously with {@link #setForYourEyesOnly()}. - * - * @param fileName name of the encrypted file - * @return this - */ - public ProducerOptions setFileName(@Nonnull String fileName) { - this.fileName = fileName; - return this; - } - - /** - * Return the encrypted files name. - * - * @return file name - */ - public String getFileName() { - return fileName; - } - - /** - * Mark the encrypted message as for-your-eyes-only by setting a special file name. - * Note: Therefore this method cannot be used simultaneously with {@link #setFileName(String)}. - * - * @return this - * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this special filename in - * newly generated literal data packets - */ - @Deprecated - public ProducerOptions setForYourEyesOnly() { - this.fileName = PGPLiteralData.CONSOLE; - return this; - } - - /** - * Set the modification date of the encrypted file. - * - * @param modificationDate Modification date of the encrypted file. - * @return this - */ - public ProducerOptions setModificationDate(@Nonnull Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - /** - * Return the modification date of the encrypted file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Set format metadata field of the literal data packet. - * Defaults to {@link StreamEncoding#BINARY}. - *
- * This does not change the encoding of the wrapped data itself. - * To apply CR/LF encoding to your input data before processing, use {@link #applyCRLFEncoding()} instead. - * - * @see RFC4880 §5.9. Literal Data Packet - * - * @param encoding encoding - * @return this - * - * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are discouraged. - */ - @Deprecated - public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { - this.encodingField = encoding; - return this; - } - - public StreamEncoding getEncoding() { - return encodingField; - } - - /** - * Apply special encoding of line endings to the input data. - * By default, this is disabled, which means that the data is not altered. - *
- * Enabling it will change the line endings to CR/LF. - * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in - * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". - * - * @return this - */ - public ProducerOptions applyCRLFEncoding() { - this.applyCRLFEncoding = true; - return this; - } - - /** - * Return the input encoding that will be applied before signing / encryption. - * - * @return input encoding - */ - public boolean isApplyCRLFEncoding() { - return applyCRLFEncoding; - } - - /** - * 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; - } - - public CompressionAlgorithm getCompressionAlgorithmOverride() { - return compressionAlgorithmOverride; - } - - public @Nullable EncryptionOptions getEncryptionOptions() { - return encryptionOptions; - } - - public @Nullable SigningOptions getSigningOptions() { - return signingOptions; - } - - public boolean isHideArmorHeaders() { - return hideArmorHeaders; - } - - /** - * If set to

true
, armor headers like version or comments will be omitted from armored output. - * By default, armor headers are not hidden. - * Note: If comments are added via {@link #setComment(String)}, those are not omitted, even if - * {@link #hideArmorHeaders} is set to
true
. - * - * @param hideArmorHeaders true or false - * @return this - */ - public ProducerOptions setHideArmorHeaders(boolean hideArmorHeaders) { - this.hideArmorHeaders = hideArmorHeaders; - return this; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt new file mode 100644 index 00000000..bdc153e9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -0,0 +1,291 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.PGPLiteralData +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import java.util.* + +class ProducerOptions private constructor( + val encryptionOptions: EncryptionOptions?, + val signingOptions: SigningOptions?) { + + private var _fileName: String = "" + private var _modificationDate: Date = PGPLiteralData.NOW + private var encodingField: StreamEncoding = StreamEncoding.BINARY + private var applyCRLFEncoding = false + private var cleartextSigned = false + private var _hideArmorHeaders = false + + private var _compressionAlgorithmOverride: CompressionAlgorithm = PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm + private var asciiArmor = true + private var _comment: String? = null + private var _version: String? = null + + /** + * Specify, whether the result of the encryption/signing operation shall be ascii armored. + * The default value is true. + * + * @param asciiArmor ascii armor + * @return builder + */ + fun setAsciiArmor(asciiArmor: Boolean) = apply { + require(!(cleartextSigned && !asciiArmor)) { + "Cleartext signing is enabled. Cannot disable ASCII armoring." + } + this.asciiArmor = asciiArmor + } + + /** + * Return true if the output of the encryption/signing operation shall be ascii armored. + * + * @return ascii armored + */ + val isAsciiArmor: Boolean + get() = asciiArmor + + /** + * Set the comment header in ASCII armored output. + * The default value is null, which means no comment header is added. + * Multiline comments are possible using '\\n'. + *
+ * Note: If a default header comment is set using [org.pgpainless.util.ArmoredOutputStreamFactory.setComment], + * then both comments will be written to the produced ASCII armor. + * + * @param comment comment header text + * @return builder + */ + fun setComment(comment: String?) = apply { + _comment = comment + } + + /** + * Return comment set for header in ascii armored output. + * + * @return comment + */ + val comment: String? + get() = _comment + + /** + * Return whether a comment was set (!= null). + * + * @return true if commend is set + */ + fun hasComment() = _comment != null + + /** + * Set the version header in ASCII armored output. + * The default value is null, which means no version header is added. + *
+ * Note: If the value is non-null, then this method overrides the default version header set using + * [org.pgpainless.util.ArmoredOutputStreamFactory.setVersionInfo]. + * + * @param version version header, or null for no version info. + * @return builder + */ + fun setVersion(version: String?) = apply { + _version = version + } + + /** + * Return the version info header in ascii armored output. + * + * @return version info + */ + val version: String? + get() = _version + + /** + * Return whether a version header was set (!= null). + * + * @return true if version header is set + */ + fun hasVersion() = version != null + + fun setCleartextSigned() = apply { + require(signingOptions != null) { + "Signing Options cannot be null if cleartext signing is enabled." + } + require(encryptionOptions == null) { + "Cannot encode encrypted message as Cleartext Signed." + } + require(signingOptions.signingMethods.values.all { it.isDetached }) { + "For cleartext signed messages, all signatures must be added as detached signatures." + } + + cleartextSigned = true + asciiArmor = true + _compressionAlgorithmOverride = CompressionAlgorithm.UNCOMPRESSED + } + + val isCleartextSigned: Boolean + get() = cleartextSigned + + /** + * Set the name of the encrypted file. + * Note: This option cannot be used simultaneously with [setForYourEyesOnly]. + * + * @param fileName name of the encrypted file + * @return this + */ + fun setFileName(fileName: String) = apply { + _fileName = fileName + } + + /** + * Return the encrypted files name. + * + * @return file name + */ + val fileName: String + get() = _fileName + + /** + * Mark the encrypted message as for-your-eyes-only by setting a special file name. + * Note: Therefore this method cannot be used simultaneously with [setFileName]. + * + * @return this + * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this special filename in + * newly generated literal data packets + */ + @Deprecated("Signaling using special file name is discouraged.") + fun setForYourEyesOnly() = apply { + _fileName = PGPLiteralData.CONSOLE + } + + /** + * Set the modification date of the encrypted file. + * + * @param modificationDate Modification date of the encrypted file. + * @return this + */ + fun setModificationDate(modificationDate: Date) = apply { + _modificationDate = modificationDate + } + + /** + * Return the modification date of the encrypted file. + * + * @return modification date + */ + val modificationDate: Date + get() = _modificationDate + + /** + * Set format metadata field of the literal data packet. + * Defaults to [StreamEncoding.BINARY]. + *
+ * This does not change the encoding of the wrapped data itself. + * To apply CR/LF encoding to your input data before processing, use [applyCRLFEncoding] instead. + * + * @see RFC4880 §5.9. Literal Data Packet + * + * @param encoding encoding + * @return this + * + * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are discouraged. + */ + @Deprecated("Options other than BINARY are discouraged.") + fun setEncoding(encoding: StreamEncoding) = apply { + encodingField = encoding + } + + val encoding: StreamEncoding + get() = encodingField + + /** + * Apply special encoding of line endings to the input data. + * By default, this is disabled, which means that the data is not altered. + *
+ * Enabling it will change the line endings to CR/LF. + * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in + * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". + * + * @return this + */ + fun applyCRLFEncoding() = apply { + applyCRLFEncoding = true + } + + /** + * Return the input encoding that will be applied before signing / encryption. + * + * @return input encoding + */ + val isApplyCRLFEncoding: Boolean + get() = applyCRLFEncoding + + /** + * Override which compression algorithm shall be used. + * + * @param compressionAlgorithm compression algorithm override + * @return builder + */ + fun overrideCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { + _compressionAlgorithmOverride = compressionAlgorithm + } + + val compressionAlgorithmOverride: CompressionAlgorithm + get() = _compressionAlgorithmOverride + + val isHideArmorHeaders: Boolean + get() = _hideArmorHeaders + + /** + * If set to `true`, armor headers like version or comments will be omitted from armored output. + * By default, armor headers are not hidden. + * Note: If comments are added via [setComment], those are not omitted, even if + * [hideArmorHeaders] is set to `true`. + * + * @param hideArmorHeaders true or false + * @return this + */ + fun setHideArmorHeaders(hideArmorHeaders: Boolean) = apply { + _hideArmorHeaders = hideArmorHeaders + } + + companion object { + /** + * Sign and encrypt some data. + * + * @param encryptionOptions encryption options + * @param signingOptions signing options + * @return builder + */ + @JvmStatic + fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) = + ProducerOptions(encryptionOptions, signingOptions) + + /** + * Sign some data without encryption. + * + * @param signingOptions signing options + * @return builder + */ + @JvmStatic + fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) + + /** + * Encrypt some data without signing. + * + * @param encryptionOptions encryption options + * @return builder + */ + @JvmStatic + fun encrypt(encryptionOptions: EncryptionOptions) = ProducerOptions(encryptionOptions, null) + + /** + * Only wrap the data in an OpenPGP packet. + * No encryption or signing will be applied. + * + * @return builder + */ + @JvmStatic + fun noEncryptionNoSigning() = ProducerOptions(null, null) + } +} \ No newline at end of file From 6a23016104d402241d3701f4a340a41312d2b954 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 16:39:03 +0200 Subject: [PATCH 125/351] Kotlin conversion: EncryptionResult --- .../encryption_signing/EncryptionResult.java | 209 ------------------ .../encryption_signing/EncryptionResult.kt | 103 +++++++++ 2 files changed, 103 insertions(+), 209 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt 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 deleted file mode 100644 index 4112092c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionResult.java +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -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 String fileName; - private final Date modificationDate; - private final StreamEncoding fileEncoding; - - private EncryptionResult(SymmetricKeyAlgorithm encryptionAlgorithm, - CompressionAlgorithm compressionAlgorithm, - MultiMap detachedSignatures, - Set recipients, - String fileName, - Date modificationDate, - StreamEncoding encoding) { - this.encryptionAlgorithm = encryptionAlgorithm; - this.compressionAlgorithm = compressionAlgorithm; - this.detachedSignatures = detachedSignatures; - this.recipients = Collections.unmodifiableSet(recipients); - this.fileName = fileName; - this.modificationDate = modificationDate; - this.fileEncoding = encoding; - } - - /** - * Return the symmetric encryption algorithm used to encrypt the message. - * - * @return symmetric encryption algorithm - * */ - public SymmetricKeyAlgorithm getEncryptionAlgorithm() { - return encryptionAlgorithm; - } - - /** - * Return the compression algorithm that was used to compress the message before encryption/signing. - * - * @return compression algorithm - */ - public CompressionAlgorithm getCompressionAlgorithm() { - return compressionAlgorithm; - } - - /** - * Return a {@link MultiMap} of key identifiers and detached signatures that were generated for the message. - * Each key of the map represents a signing key, which has one or more detached signatures associated with it. - * - * @return detached signatures - */ - public MultiMap getDetachedSignatures() { - return detachedSignatures; - } - - /** - * Return the set of recipient encryption keys. - * - * @return recipients - */ - public Set getRecipients() { - return recipients; - } - - /** - * Return the file name of the encrypted/signed data. - * - * @return filename - */ - public String getFileName() { - return fileName; - } - - /** - * Return the modification date of the encrypted/signed file. - * - * @return modification date - */ - public Date getModificationDate() { - return modificationDate; - } - - /** - * Return the encoding format of the encrypted/signed data. - * - * @return encoding format - */ - public StreamEncoding getFileEncoding() { - return fileEncoding; - } - - /** - * Return true, if the message is marked as for-your-eyes-only. - * This is typically done by setting the filename "_CONSOLE". - * - * @return is message for your eyes only? - */ - public boolean isForYourEyesOnly() { - return PGPLiteralData.CONSOLE.equals(getFileName()); - } - - /** - * Returns true, if the message was encrypted for at least one subkey of the given certificate. - * - * @param certificate certificate - * @return true if encrypted for 1+ subkeys, false otherwise. - */ - public boolean isEncryptedFor(PGPPublicKeyRing certificate) { - for (SubkeyIdentifier recipient : recipients) { - if (certificate.getPublicKey().getKeyID() != recipient.getPrimaryKeyId()) { - continue; - } - - if (certificate.getPublicKey(recipient.getSubkeyId()) != null) { - return true; - } - } - return false; - } - - /** - * Create a builder for the encryption result class. - * - * @return builder - */ - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private SymmetricKeyAlgorithm encryptionAlgorithm; - private CompressionAlgorithm compressionAlgorithm; - - private final MultiMap detachedSignatures = new MultiMap<>(); - private final Set recipients = new HashSet<>(); - private String fileName = ""; - private Date modificationDate = new Date(0L); // NOW - private StreamEncoding encoding = StreamEncoding.BINARY; - - 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 setFileName(@Nonnull String fileName) { - this.fileName = fileName; - return this; - } - - public Builder setModificationDate(@Nonnull Date modificationDate) { - this.modificationDate = modificationDate; - return this; - } - - public Builder setFileEncoding(StreamEncoding fileEncoding) { - this.encoding = fileEncoding; - 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."); - } - - return new EncryptionResult(encryptionAlgorithm, compressionAlgorithm, detachedSignatures, recipients, - fileName, modificationDate, encoding); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt new file mode 100644 index 00000000..fd9febc7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.util.MultiMap +import java.util.* + +data class EncryptionResult( + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val compressionAlgorithm: CompressionAlgorithm, + val detachedSignatures: MultiMap, + val recipients: Set, + val fileName: String, + val modificationDate: Date, + val fileEncoding: StreamEncoding +) { + + /** + * Return true, if the message is marked as for-your-eyes-only. + * This is typically done by setting the filename "_CONSOLE". + * + * @return is message for your eyes only? + */ + val isForYourEyesOnly: Boolean + get() = PGPLiteralData.CONSOLE == fileName + + /** + * Returns true, if the message was encrypted for at least one subkey of the given certificate. + * + * @param certificate certificate + * @return true if encrypted for 1+ subkeys, false otherwise. + */ + fun isEncryptedFor(certificate: PGPPublicKeyRing) = recipients.any { + certificate.publicKey.keyID == it.primaryKeyId && + certificate.getPublicKey(it.subkeyId) != null + } + + companion object { + /** + * Create a builder for the encryption result class. + * + * @return builder + */ + @JvmStatic + fun builder() = Builder() + } + + class Builder { + var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null + var _compressionAlgorithm: CompressionAlgorithm? = null + + val detachedSignatures: MultiMap = MultiMap() + val recipients: Set = mutableSetOf() + private var _fileName = "" + private var _modificationDate = Date(0) + private var _encoding = StreamEncoding.BINARY + + fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { + _encryptionAlgorithm = encryptionAlgorithm + } + + fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { + _compressionAlgorithm = compressionAlgorithm + } + + fun setFileName(fileName: String) = apply { + _fileName = fileName + } + + fun setModificationDate(modificationDate: Date) = apply { + _modificationDate = modificationDate + } + + fun setFileEncoding(encoding: StreamEncoding) = apply { + _encoding = encoding + } + + fun addRecipient(recipient: SubkeyIdentifier) = apply { + (recipients as MutableSet).add(recipient) + } + + fun addDetachedSignature(signingSubkeyIdentifier: SubkeyIdentifier, detachedSignature: PGPSignature) = apply { + detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) + } + + fun build(): EncryptionResult { + checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } + checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } + + return EncryptionResult(_encryptionAlgorithm!!, _compressionAlgorithm!!, detachedSignatures, recipients, + _fileName, _modificationDate, _encoding) + } + } +} \ No newline at end of file From e8fef1f1f3c79ba1466b2c50b728abd16a15d5b5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 6 Sep 2023 16:49:28 +0200 Subject: [PATCH 126/351] Add PGPKeyRingExtensions class and make use of it --- .../extensions/PGPKeyRingExtensions.kt | 15 +++++++ .../MessageMetadata.kt | 40 ++++++++----------- .../encryption_signing/EncryptionResult.kt | 6 +-- .../org/pgpainless/key/SubkeyIdentifier.kt | 3 ++ 4 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt new file mode 100644 index 00000000..43669bd1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.openpgp.PGPKeyRing +import org.pgpainless.key.SubkeyIdentifier + +/** + * Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. + */ +fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = + this.publicKey.keyID == subkeyIdentifier.primaryKeyId && + this.getPublicKey(subkeyIdentifier.subkeyId) != null \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt index 8719cef7..3eb49e5d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification +import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPLiteralData import org.pgpainless.algorithm.CompressionAlgorithm @@ -227,10 +228,9 @@ class MessageMetadata( */ @JvmOverloads fun isAuthenticatablySignedBy(userId: String, email: Boolean, certificateAuthority: CertificateAuthority, targetAmount: Int = 120): Boolean { - return verifiedSignatures.any { - certificateAuthority.authenticateBinding( - it.signingKey.fingerprint, userId, email, it.signature.creationTime, targetAmount - ).authenticated + return verifiedSignatures.any { certificateAuthority + .authenticateBinding(it.signingKey.fingerprint, userId, email, it.signature.creationTime, targetAmount) + .authenticated } } @@ -241,31 +241,23 @@ class MessageMetadata( * @param fingerprint fingerprint * @return true if message was signed by a cert identified by the given fingerprint */ - fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedSignatures.any { - it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint - } + fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedSignatures.any { it.signingKey.matches(fingerprint) } - fun isVerifiedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedSignatures, keys) + fun isVerifiedSignedBy(keys: PGPKeyRing) = + verifiedSignatures.any { keys.matches(it.signingKey) } - fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = verifiedDetachedSignatures.any { - it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint - } + fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } - fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedDetachedSignatures, keys) + fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = + verifiedDetachedSignatures.any { keys.matches(it.signingKey) } - fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = verifiedInlineSignatures.any { - it.signingKey.primaryKeyFingerprint == fingerprint || it.signingKey.subkeyFingerprint == fingerprint - } + fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = + verifiedInlineSignatures.any { it.signingKey.matches(fingerprint) } - fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = containsSignatureBy(verifiedInlineSignatures, keys) - - private fun containsSignatureBy(signatures: List, keys: PGPKeyRing) = - signatures.any { - // Match certificate by primary key id - keys.publicKey.keyID == it.signingKey.primaryKeyId && - // match signing subkey - keys.getPublicKey(it.signingKey.subkeyId) != null - } + fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = + verifiedInlineSignatures.any { keys.matches(it.signingKey) } // ################################################################################################################ // ### Literal Data ### diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index fd9febc7..0e9a40c9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature @@ -39,10 +40,7 @@ data class EncryptionResult( * @param certificate certificate * @return true if encrypted for 1+ subkeys, false otherwise. */ - fun isEncryptedFor(certificate: PGPPublicKeyRing) = recipients.any { - certificate.publicKey.keyID == it.primaryKeyId && - certificate.getPublicKey(it.subkeyId) != null - } + fun isEncryptedFor(certificate: PGPPublicKeyRing) = recipients.any { certificate.matches(it) } companion object { /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index ea36913c..58e7719c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -31,6 +31,9 @@ class SubkeyIdentifier( val subkeyId = subkeyFingerprint.keyId val primaryKeyId = primaryKeyFingerprint.keyId + fun matches(fingerprint: OpenPgpFingerprint) = + primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint + override fun equals(other: Any?): Boolean { if (other == null) { return false From 0b071ff8e1f70be1a0f0f532c72d2b3fb7aff242 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 14:29:17 +0200 Subject: [PATCH 127/351] Kotlin conversion: CachingBcPublicKeyDataDecryptorFactory --- ...achingBcPublicKeyDataDecryptorFactory.java | 95 ------------------- .../java/org/bouncycastle/package-info.java | 8 -- .../CachingBcPublicKeyDataDecryptorFactory.kt | 50 ++++++++++ 3 files changed, 50 insertions(+), 103 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java delete mode 100644 pgpainless-core/src/main/java/org/bouncycastle/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java deleted file mode 100644 index 510b0938..00000000 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.bouncycastle; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.bouncycastle.util.encoders.Base64; -import org.bouncycastle.util.encoders.Hex; -import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -/** - * Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys. - * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. - * - * This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any - * cache hits. - * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. - * The result of that is then placed in the cache and returned. - */ -public class CachingBcPublicKeyDataDecryptorFactory - extends BcPublicKeyDataDecryptorFactory - implements CustomPublicKeyDataDecryptorFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); - - private final Map cachedSessionKeys = new HashMap<>(); - private final SubkeyIdentifier decryptionKey; - - public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { - super(privateKey); - this.decryptionKey = decryptionKey; - } - - @Override - public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookupSessionKeyData(secKeyData); - if (sessionKey == null) { - LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); - sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); - cacheSessionKeyData(secKeyData, sessionKey); - } else { - LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); - } - return sessionKey; - } - - public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - return super.recoverSessionData(keyAlgorithm, secKeyData); - } - - private byte[] lookupSessionKeyData(byte[][] secKeyData) { - String key = toKey(secKeyData); - byte[] sessionKey = cachedSessionKeys.get(key); - return copy(sessionKey); - } - - private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { - String key = toKey(secKeyData); - cachedSessionKeys.put(key, copy(sessionKey)); - } - - private static String toKey(byte[][] secKeyData) { - byte[] sk = secKeyData[0]; - String key = Base64.toBase64String(sk); - return key; - } - - private static byte[] copy(byte[] bytes) { - if (bytes == null) { - return null; - } - byte[] copy = new byte[bytes.length]; - System.arraycopy(bytes, 0, copy, 0, copy.length); - return copy; - } - - public void clear() { - cachedSessionKeys.clear(); - } - - @Override - public SubkeyIdentifier getSubkeyIdentifier() { - return decryptionKey; - } -} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java deleted file mode 100644 index 565bb5f4..00000000 --- a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes which could be upstreamed to BC at some point. - */ -package org.bouncycastle; diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt new file mode 100644 index 00000000..60b860f2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle + +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.bouncycastle.util.encoders.Base64 +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory +import org.pgpainless.key.SubkeyIdentifier + +/** + * Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or [recoverSessionData] to first return any + * cache hits. + * If no hit is found, the method call is delegated to the underlying [PublicKeyDataDecryptorFactory]. + * The result of that is then placed in the cache and returned. + */ +class CachingBcPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey, + override val subkeyIdentifier: SubkeyIdentifier +) : BcPublicKeyDataDecryptorFactory(privateKey), CustomPublicKeyDataDecryptorFactory { + + private val cachedSessions: MutableMap = mutableMapOf() + + override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray = + lookupSessionKeyData(secKeyData) ?: + costlyRecoverSessionData(keyAlgorithm, secKeyData) + .also { cacheSessionKeyData(secKeyData, it) } + + private fun lookupSessionKeyData(secKeyData: Array): ByteArray? = + cachedSessions[toKey(secKeyData)]?.clone() + + private fun costlyRecoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray = + super.recoverSessionData(keyAlgorithm, secKeyData) + + private fun cacheSessionKeyData(secKeyData: Array, sessionKey: ByteArray) { + cachedSessions[toKey(secKeyData)] = sessionKey.clone() + } + + private fun toKey(secKeyData: Array): String = + Base64.toBase64String(secKeyData[0]) + + fun clear() { + cachedSessions.clear() + } +} \ No newline at end of file From 1ebf8e1e6fa9c9f9664ef0f742b8a52d1b188253 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 14:32:49 +0200 Subject: [PATCH 128/351] Kotlin conversion: KeyLength --- .../org/pgpainless/key/generation/type/KeyLength.java | 10 ---------- .../org/pgpainless/key/generation/type/KeyLength.kt | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java deleted file mode 100644 index 1cadfef8..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyLength.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type; - -public interface KeyLength { - - int getLength(); -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt new file mode 100644 index 00000000..3f806987 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type + +interface KeyLength { + + fun getLength(): Int +} \ No newline at end of file From 472d5c4bebd9020267f514a2f8bfc748d625de65 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 14:46:56 +0200 Subject: [PATCH 129/351] Kotlin conversion: KeyType --- .../key/generation/type/KeyType.java | 118 ------------------ .../pgpainless/key/generation/type/KeyType.kt | 110 ++++++++++++++++ 2 files changed, 110 insertions(+), 118 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java deleted file mode 100644 index 191a22f7..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/KeyType.java +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type; - -import java.security.spec.AlgorithmParameterSpec; - -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.ecc.ecdh.ECDH; -import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA; -import org.pgpainless.key.generation.type.eddsa.EdDSA; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.rsa.RSA; -import org.pgpainless.key.generation.type.xdh.XDH; -import org.pgpainless.key.generation.type.xdh.XDHSpec; - -public interface KeyType { - - /** - * Return the encryption algorithm name. - * - * @return algorithm name. - */ - String getName(); - - /** - * Return the public key algorithm. - * - * @return public key algorithm - */ - PublicKeyAlgorithm getAlgorithm(); - - /** - * Return the strength of the key in bits. - * @return strength of the key in bits - */ - int getBitStrength(); - - /** - * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the key. - * - * @return algorithm parameter spec - */ - AlgorithmParameterSpec getAlgorithmSpec(); - - /** - * Return true if the key that is generated from this type is able to carry the SIGN_DATA key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. - * - * @return true if the key can sign. - */ - default boolean canSign() { - return getAlgorithm().isSigningCapable(); - } - - /** - * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. - * - * @return true if the key is able to certify other keys - */ - default boolean canCertify() { - return canSign(); - } - - /** - * Return true if the key that is generated from this type is able to carry the AUTHENTICATION key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. - * - * @return true if the key can be used for authentication purposes. - */ - default boolean canAuthenticate() { - return canSign(); - } - - /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. - * - * @return true if the key can encrypt communication - */ - default boolean canEncryptCommunication() { - return getAlgorithm().isEncryptionCapable(); - } - - /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * @return true if the key can encrypt for storage - */ - default boolean canEncryptStorage() { - return getAlgorithm().isEncryptionCapable(); - } - - static KeyType RSA(RsaLength length) { - return RSA.withLength(length); - } - - static KeyType ECDH(EllipticCurve curve) { - return ECDH.fromCurve(curve); - } - - static KeyType ECDSA(EllipticCurve curve) { - return ECDSA.fromCurve(curve); - } - - static KeyType EDDSA(EdDSACurve curve) { - return EdDSA.fromCurve(curve); - } - - static KeyType XDH(XDHSpec curve) { - return XDH.fromSpec(curve); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt new file mode 100644 index 00000000..105962b9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type + +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.ecc.EllipticCurve +import org.pgpainless.key.generation.type.ecc.ecdh.ECDH +import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA +import org.pgpainless.key.generation.type.eddsa.EdDSA +import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.rsa.RSA +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh.XDH +import org.pgpainless.key.generation.type.xdh.XDHSpec +import java.security.spec.AlgorithmParameterSpec + +@Suppress("INAPPLICABLE_JVM_NAME") // https://youtrack.jetbrains.com/issue/KT-31420 +interface KeyType { + + /** + * Return the encryption algorithm name. + * + * @return algorithm name. + */ + val name: String + + /** + * Return the public key algorithm. + * + * @return public key algorithm + */ + val algorithm: PublicKeyAlgorithm + + /** + * Return the strength of the key in bits. + * @return strength of the key in bits + */ + val bitStrength: Int + + /** + * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the key. + * + * @return algorithm parameter spec + */ + val algorithmSpec: AlgorithmParameterSpec + + /** + * Return true if the key that is generated from this type is able to carry the SIGN_DATA key flag. + * See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. + * + * @return true if the key can sign. + */ + val canSign: Boolean + @JvmName("canSign") get() = algorithm.signingCapable + + /** + * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER key flag. + * See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. + * + * @return true if the key is able to certify other keys + */ + val canCertify: Boolean + @JvmName("canCertify") get() = canSign + + /** + * Return true if the key that is generated from this type is able to carry the AUTHENTICATION key flag. + * See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. + * + * @return true if the key can be used for authentication purposes. + */ + val canAuthenticate: Boolean + @JvmName("canAuthenticate") get() = canSign + + /** + * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS key flag. + * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * + * @return true if the key can encrypt communication + */ + val canEncryptCommunication: Boolean + @JvmName("canEncryptCommunication") get() = algorithm.encryptionCapable + + /** + * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE key flag. + * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. + * + * @return true if the key can encrypt for storage + */ + val canEncryptStorage: Boolean + @JvmName("canEncryptStorage") get() = algorithm.encryptionCapable + + companion object { + @JvmStatic + fun RSA(length: RsaLength): RSA = RSA.withLength(length) + + @JvmStatic + fun ECDH(curve: EllipticCurve): ECDH = ECDH.fromCurve(curve) + + @JvmStatic + fun ECDSA(curve: EllipticCurve): ECDSA = ECDSA.fromCurve(curve) + + @JvmStatic + fun EDDSA(curve: EdDSACurve): EdDSA = EdDSA.fromCurve(curve) + + @JvmStatic + fun XDH(curve: XDHSpec): XDH = XDH.fromSpec(curve) + } +} \ No newline at end of file From b3f4ba052a06b3ffcf6ff96bc9e1de9dfc1e8c7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 14:47:13 +0200 Subject: [PATCH 130/351] Remove whitespace --- .../main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt index 3f806987..12e39b08 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -5,6 +5,6 @@ package org.pgpainless.key.generation.type interface KeyLength { - + fun getLength(): Int } \ No newline at end of file From 13082215d6593fffc3e040bd32c09c31544685b6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:45:37 +0200 Subject: [PATCH 131/351] Fix property access --- .../main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index c101e2c0..2e4a6642 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -82,7 +82,7 @@ class KeyRingBuilder : KeyRingBuilderInterface { } } - private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify() + private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify override fun build(): PGPSecretKeyRing { val keyFingerprintCalculator = ImplementationFactory.getInstance() From 7f96272152de73d587c999dfe5996437f1659b32 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 14:56:42 +0200 Subject: [PATCH 132/351] Kotlin conversion: EllipticCurve --- .../generation/type/ecc/EllipticCurve.java | 42 ------------------- .../key/generation/type/ecc/EllipticCurve.kt | 27 ++++++++++++ 2 files changed, 27 insertions(+), 42 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java deleted file mode 100644 index 2372896e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/EllipticCurve.java +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc; - -import javax.annotation.Nonnull; - -import org.pgpainless.key.generation.type.xdh.XDHSpec; - -/** - * Elliptic curves for use with - * {@link org.pgpainless.key.generation.type.ecc.ecdh.ECDH}/{@link org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA}. - * For curve25519 related curve definitions see - * {@link XDHSpec} and {@link org.pgpainless.key.generation.type.eddsa.EdDSACurve}. - */ -public enum EllipticCurve { - _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see https://tools.ietf.org/search/rfc4492#page-32 - _P384("secp384r1", 384), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 - _P521("secp521r1", 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 - _SECP256K1("secp256k1", 256), - _BRAINPOOLP256R1("brainpoolP256r1", 256), - _BRAINPOOLP384R1("brainpoolP384r1", 384), - _BRAINPOOLP512R1("brainpoolP512r1", 512) - ; - - private final String name; - private final int bitStrength; - - EllipticCurve(@Nonnull String name, int bitStrength) { - this.name = name; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public int getBitStrength() { - return bitStrength; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt new file mode 100644 index 00000000..287df67f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc + + +/** + * Elliptic curves for use with [org.pgpainless.key.generation.type.ecc.ecdh.ECDH] and + * [org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA]. + * For curve25519 related curve definitions see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa.EdDSACurve]. + */ +enum class EllipticCurve( + val curveName: String, + val bitStrength: Int +) { + _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see https://tools.ietf.org/search/rfc4492#page-32 + _P384("secp384r1", 384), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 + _P521("secp521r1", 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 + _SECP256K1("secp256k1", 256), + _BRAINPOOLP256R1("brainpoolP256r1", 256), + _BRAINPOOLP384R1("brainpoolP384r1", 384), + _BRAINPOOLP512R1("brainpoolP512r1", 512), + ; + + fun getName(): String = curveName +} \ No newline at end of file From 9e7a25ffe19a1c62de8287377f288386a74b6eeb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:00:54 +0200 Subject: [PATCH 133/351] Kotlin conversion: ECDH --- .../key/generation/type/ecc/ecdh/ECDH.java | 46 ------------------- .../type/ecc/ecdh/package-info.java | 8 ---- .../key/generation/type/ecc/ecdh/ECDH.kt | 22 +++++++++ 3 files changed, 22 insertions(+), 54 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java deleted file mode 100644 index bb7e3f3c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc.ecdh; - -import javax.annotation.Nonnull; -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; - -public final class ECDH implements KeyType { - - private final EllipticCurve curve; - - private ECDH(EllipticCurve curve) { - this.curve = curve; - } - - public static ECDH fromCurve(@Nonnull EllipticCurve curve) { - return new ECDH(curve); - } - - @Override - public String getName() { - return "ECDH"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDH; - } - - @Override - public int getBitStrength() { - return curve.getBitStrength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java deleted file mode 100644 index b1f2c882..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdh/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ECDH. - */ -package org.pgpainless.key.generation.type.ecc.ecdh; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt new file mode 100644 index 00000000..6bab2fcc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc.ecdh + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve + +class ECDH private constructor(val curve: EllipticCurve) : KeyType { + override val name = "ECDH" + override val algorithm = PublicKeyAlgorithm.ECDH + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic + fun fromCurve(curve: EllipticCurve) = ECDH(curve) + } +} \ No newline at end of file From 89b73895f5ceb450a86cfa47a279127ba7806848 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:03:06 +0200 Subject: [PATCH 134/351] Kotlin conversion: ECDSA --- .../key/generation/type/ecc/ecdsa/ECDSA.java | 48 ------------------- .../type/ecc/ecdsa/package-info.java | 8 ---- .../key/generation/type/ecc/package-info.java | 8 ---- .../key/generation/type/ecc/ecdsa/ECDSA.kt | 22 +++++++++ 4 files changed, 22 insertions(+), 64 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java deleted file mode 100644 index 87301655..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.ecc.ecdsa; - - -import java.security.spec.AlgorithmParameterSpec; -import javax.annotation.Nonnull; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.KeyType; - -public final class ECDSA implements KeyType { - - private final EllipticCurve curve; - - private ECDSA(@Nonnull EllipticCurve curve) { - this.curve = curve; - } - - public static ECDSA fromCurve(@Nonnull EllipticCurve curve) { - return new ECDSA(curve); - } - - @Override - public String getName() { - return "ECDSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDSA; - } - - @Override - public int getBitStrength() { - return curve.getBitStrength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java deleted file mode 100644 index 9b8ca577..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/ecdsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ECDSA. - */ -package org.pgpainless.key.generation.type.ecc.ecdsa; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java deleted file mode 100644 index d55a487a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/ecc/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes describing different OpenPGP key types based on elliptic curves. - */ -package org.pgpainless.key.generation.type.ecc; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt new file mode 100644 index 00000000..b7a0b94f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.ecc.ecdsa + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve + +class ECDSA private constructor(val curve: EllipticCurve) : KeyType { + override val name = "ECDSA" + override val algorithm = PublicKeyAlgorithm.ECDSA + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic + fun fromCurve(curve: EllipticCurve) = ECDSA(curve) + } +} \ No newline at end of file From 8f49b01d510608e4f50fe501e6d1a6871cd5d7cc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:08:10 +0200 Subject: [PATCH 135/351] Kotlin conversion: EdDSACurve --- .../key/generation/type/eddsa/EdDSACurve.java | 28 ------------------- .../key/generation/type/eddsa/EdDSACurve.kt | 14 ++++++++++ 2 files changed, 14 insertions(+), 28 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java deleted file mode 100644 index 4d5aed1c..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSACurve.java +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.eddsa; - -import javax.annotation.Nonnull; - -public enum EdDSACurve { - _Ed25519("ed25519", 256), - ; - - final String name; - final int bitStrength; - - EdDSACurve(@Nonnull String curveName, int bitStrength) { - this.name = curveName; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public int getBitStrength() { - return bitStrength; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt new file mode 100644 index 00000000..52b6949b --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.eddsa + +enum class EdDSACurve( + val curveName: String, + val bitStrength: Int) { + _Ed25519("ed25519", 256), + ; + + fun getName() = curveName +} \ No newline at end of file From 4382c1f20e342b6c304dc559e0f2e4fd35116e14 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:08:51 +0200 Subject: [PATCH 136/351] Kotlin conversion: EdDSA --- .../key/generation/type/eddsa/EdDSA.java | 50 ------------------- .../generation/type/eddsa/package-info.java | 8 --- .../key/generation/type/eddsa/EdDSA.kt | 21 ++++++++ 3 files changed, 21 insertions(+), 58 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java deleted file mode 100644 index 2db57f57..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/EdDSA.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.eddsa; - -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * Edwards-curve Digital Signature Algorithm (EdDSA). - * - * @see EdDSA for OpenPGP - */ -public final class EdDSA implements KeyType { - - private final EdDSACurve curve; - - private EdDSA(EdDSACurve curve) { - this.curve = curve; - } - - public static EdDSA fromCurve(EdDSACurve curve) { - return new EdDSA(curve); - } - - @Override - public String getName() { - return "EdDSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.EDDSA; - } - - @Override - public int getBitStrength() { - return curve.getBitStrength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(curve.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java deleted file mode 100644 index cba16f54..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/eddsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to EdDSA. - */ -package org.pgpainless.key.generation.type.eddsa; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt new file mode 100644 index 00000000..d1e51a8e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.eddsa + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class EdDSA private constructor(val curve: EdDSACurve) : KeyType { + override val name = "EdDSA" + override val algorithm = PublicKeyAlgorithm.EDDSA + override val bitStrength = curve.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) + + companion object { + @JvmStatic + fun fromCurve(curve: EdDSACurve) = EdDSA(curve) + } +} \ No newline at end of file From f8abb28a81e7163ff819a0418b5622e5d1e47fce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:14:00 +0200 Subject: [PATCH 137/351] Turn KeyLength method into val --- .../main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt index 12e39b08..377fbb94 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -6,5 +6,5 @@ package org.pgpainless.key.generation.type interface KeyLength { - fun getLength(): Int + val length: Int } \ No newline at end of file From 72147b685e10e1ec6d6835996cf35902d19ade5b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:14:30 +0200 Subject: [PATCH 138/351] Kotlin conversion: ElGamalLength --- .../generation/type/elgamal/ElGamalLength.kt} | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) rename pgpainless-core/src/main/{java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java => kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt} (91%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt similarity index 91% rename from pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt index 17e79131..9eae195c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamalLength.java +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt @@ -1,12 +1,11 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub +// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type.elgamal; +package org.pgpainless.key.generation.type.elgamal -import java.math.BigInteger; - -import org.pgpainless.key.generation.type.KeyLength; +import org.pgpainless.key.generation.type.KeyLength +import java.math.BigInteger /** * The following primes are taken from RFC-3526. @@ -16,8 +15,13 @@ import org.pgpainless.key.generation.type.KeyLength; * * @deprecated the use of ElGamal keys is no longer recommended. */ -@Deprecated -public enum ElGamalLength implements KeyLength { + +@Deprecated("The use of ElGamal keys is no longer recommended.") +enum class ElGamalLength( + override val length: Int, + p: String, + g: String +) : KeyLength { /** * prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. @@ -56,26 +60,13 @@ public enum ElGamalLength implements KeyLength { _8192(8192, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", "2") ; - private final int length; - private final BigInteger p; - private final BigInteger g; + val p: BigInteger + val g: BigInteger - ElGamalLength(int length, String p, String g) { - this.length = length; - this.p = new BigInteger(p, 16); - this.g = new BigInteger(g, 16); + init { + this.p = BigInteger(p, 16) + this.g = BigInteger(g, 16) } - @Override - public int getLength() { - return length; - } - public BigInteger getP() { - return p; - } - - public BigInteger getG() { - return g; - } -} +} \ No newline at end of file From 2d755be10e4dc180ddddbc1999a82f65fa674cb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:18:06 +0200 Subject: [PATCH 139/351] Kotlin conversion: ElGamal --- .../key/generation/type/elgamal/ElGamal.java | 52 ------------------- .../generation/type/elgamal/package-info.java | 8 --- .../key/generation/type/elgamal/ElGamal.kt | 28 ++++++++++ 3 files changed, 28 insertions(+), 60 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java deleted file mode 100644 index 23c33d3d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/ElGamal.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.elgamal; - -import java.security.spec.AlgorithmParameterSpec; -import javax.annotation.Nonnull; - -import org.bouncycastle.jce.spec.ElGamalParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * ElGamal encryption only key type. - * - * @deprecated the use of ElGamal is not recommended anymore. - */ -@Deprecated -public final class ElGamal implements KeyType { - - private final ElGamalLength length; - - private ElGamal(@Nonnull ElGamalLength length) { - this.length = length; - } - - public static ElGamal withLength(ElGamalLength length) { - return new ElGamal(length); - } - - @Override - public String getName() { - return "ElGamal"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ELGAMAL_ENCRYPT; - } - - @Override - public int getBitStrength() { - return length.getLength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ElGamalParameterSpec(length.getP(), length.getG()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java deleted file mode 100644 index 19bc0214..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/elgamal/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to ElGamal. - */ -package org.pgpainless.key.generation.type.elgamal; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt new file mode 100644 index 00000000..6cfbc8a7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.elgamal + +import org.bouncycastle.jce.spec.ElGamalParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +/** + * ElGamal encryption only key type. + * + * @deprecated the use of ElGamal is not recommended anymore. + */ +@Deprecated("The use of ElGamal is not recommended anymore.") +class ElGamal private constructor(length: ElGamalLength) : KeyType { + + override val name = "ElGamal" + override val algorithm = PublicKeyAlgorithm.ELGAMAL_ENCRYPT + override val bitStrength = length.length + override val algorithmSpec = ElGamalParameterSpec(length.p, length.g) + + companion object { + @JvmStatic + fun withLength(length: ElGamalLength) = ElGamal(length) + } +} \ No newline at end of file From ca3ff6acce332ddb79ca855d35333fdfe2771f13 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:21:32 +0200 Subject: [PATCH 140/351] Kotlin conversion: RsaLength --- .../key/generation/type/rsa/RsaLength.java | 29 ------------------- .../key/generation/type/rsa/RsaLength.kt | 17 +++++++++++ 2 files changed, 17 insertions(+), 29 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java deleted file mode 100644 index 74951f0d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RsaLength.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.rsa; - -import org.pgpainless.key.generation.type.KeyLength; - -public enum RsaLength implements KeyLength { - @Deprecated - _1024(1024), - @Deprecated - _2048(2048), - _3072(3072), - _4096(4096), - _8192(8192), - ; - - private final int length; - - RsaLength(int length) { - this.length = length; - } - - @Override - public int getLength() { - return length; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt new file mode 100644 index 00000000..ae8bb804 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.rsa + +import org.pgpainless.key.generation.type.KeyLength + +enum class RsaLength(override val length: Int) : KeyLength { + @Deprecated("1024 bits are considered too weak for RSA nowadays.", ReplaceWith("_3072")) + _1024(1024), + @Deprecated("2048 bits are considered too weak for RSA nowadays.", ReplaceWith("_3072")) + _2048(2048), + _3072(3072), + _4096(4096), + _8192(8192) +} \ No newline at end of file From ac245fb56bba1c3ea7aedda8b6c2425d5eadc659 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:24:02 +0200 Subject: [PATCH 141/351] Kotlin conversion: RSA --- .../key/generation/type/rsa/RSA.java | 48 ------------------- .../key/generation/type/rsa/package-info.java | 8 ---- .../pgpainless/key/generation/type/rsa/RSA.kt | 25 ++++++++++ 3 files changed, 25 insertions(+), 56 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java deleted file mode 100644 index 3cf717b2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/RSA.java +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.rsa; - -import javax.annotation.Nonnull; -import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.RSAKeyGenParameterSpec; - -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -/** - * Key type that specifies the RSA_GENERAL algorithm. - */ -public class RSA implements KeyType { - - private final RsaLength length; - - RSA(@Nonnull RsaLength length) { - this.length = length; - } - - public static RSA withLength(@Nonnull RsaLength length) { - return new RSA(length); - } - - @Override - public String getName() { - return "RSA"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.RSA_GENERAL; - } - - @Override - public int getBitStrength() { - return length.getLength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new RSAKeyGenParameterSpec(length.getLength(), RSAKeyGenParameterSpec.F4); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java deleted file mode 100644 index 2a2a0120..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/rsa/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to RSA. - */ -package org.pgpainless.key.generation.type.rsa; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt new file mode 100644 index 00000000..1f8c0509 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.rsa + +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType +import java.security.spec.RSAKeyGenParameterSpec + +/** + * Key type that specifies the RSA_GENERAL algorithm. + */ +class RSA private constructor(length: RsaLength): KeyType { + + override val name = "RSA" + override val algorithm = PublicKeyAlgorithm.RSA_GENERAL + override val bitStrength = length.length + override val algorithmSpec = RSAKeyGenParameterSpec(length.length, RSAKeyGenParameterSpec.F4) + + companion object { + @JvmStatic + fun withLength(length: RsaLength) = RSA(length) + } +} \ No newline at end of file From 521424c23aca24a3c1966b73ff00a526097c0695 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:26:24 +0200 Subject: [PATCH 142/351] Kotlin conversion: XDHSpec --- .../key/generation/type/xdh/XDHSpec.java | 34 ------------------- .../key/generation/type/xdh/XDHSpec.kt | 15 ++++++++ 2 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java deleted file mode 100644 index ccbd2038..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDHSpec.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.xdh; - -import javax.annotation.Nonnull; - -public enum XDHSpec { - _X25519("X25519", "curve25519", 256), - ; - - final String name; - final String curveName; - final int bitStrength; - - XDHSpec(@Nonnull String name, @Nonnull String curveName, int bitStrength) { - this.name = name; - this.curveName = curveName; - this.bitStrength = bitStrength; - } - - public String getName() { - return name; - } - - public String getCurveName() { - return curveName; - } - - public int getBitStrength() { - return bitStrength; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt new file mode 100644 index 00000000..9486365f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.xdh + +enum class XDHSpec( + val algorithmName: String, + val curveName: String, + val bitStrength: Int) { + _X25519("X25519", "curve25519", 256), + ; + + fun getName() = algorithmName +} \ No newline at end of file From ad734ca1b4bef8ffad1cbafab98314a5ca3d2da1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:29:08 +0200 Subject: [PATCH 143/351] Kotlin conversion: XDH --- .../key/generation/package-info.java | 8 ---- .../key/generation/type/package-info.java | 8 ---- .../key/generation/type/xdh/XDH.java | 45 ------------------- .../key/generation/type/xdh/package-info.java | 8 ---- .../pgpainless/key/generation/type/xdh/XDH.kt | 21 +++++++++ 5 files changed, 21 insertions(+), 69 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java deleted file mode 100644 index 96728227..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP key generation. - */ -package org.pgpainless.key.generation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java deleted file mode 100644 index bf048484..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes describing different OpenPGP key types. - */ -package org.pgpainless.key.generation.type; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java deleted file mode 100644 index 4e589677..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/XDH.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.generation.type.xdh; - -import java.security.spec.AlgorithmParameterSpec; - -import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.KeyType; - -public final class XDH implements KeyType { - - private final XDHSpec spec; - - private XDH(XDHSpec spec) { - this.spec = spec; - } - - public static XDH fromSpec(XDHSpec spec) { - return new XDH(spec); - } - - @Override - public String getName() { - return "XDH"; - } - - @Override - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.ECDH; - } - - @Override - public int getBitStrength() { - return spec.getBitStrength(); - } - - @Override - public AlgorithmParameterSpec getAlgorithmSpec() { - return new ECNamedCurveGenParameterSpec(spec.getName()); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java deleted file mode 100644 index 96af405a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/type/xdh/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to Diffie-Hellman on the X25519 curve. - */ -package org.pgpainless.key.generation.type.xdh; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt new file mode 100644 index 00000000..06888237 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation.type.xdh + +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.KeyType + +class XDH private constructor(spec: XDHSpec): KeyType { + override val name = "XDH" + override val algorithm = PublicKeyAlgorithm.ECDH + override val bitStrength = spec.bitStrength + override val algorithmSpec = ECNamedCurveGenParameterSpec(spec.algorithmName) + + companion object { + @JvmStatic + fun fromSpec(spec: XDHSpec) = XDH(spec) + } +} \ No newline at end of file From b6e47d773991ac7863f5e5e132b3555af4426786 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 7 Sep 2023 15:56:58 +0200 Subject: [PATCH 144/351] Kotlin conversion: KeyAccessor --- .../org/pgpainless/key/info/KeyAccessor.java | 167 ------------------ .../org/pgpainless/key/SubkeyIdentifier.kt | 2 + .../org/pgpainless/key/info/KeyAccessor.kt | 88 +++++++++ 3 files changed, 90 insertions(+), 167 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt 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 deleted file mode 100644 index 8ab8a9c4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyAccessor.java +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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; - - KeyAccessor(@Nonnull KeyRingInfo info, @Nonnull 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 - */ - @Nonnull - public abstract PGPSignature getSignatureWithPreferences(); - - /** - * Return preferred symmetric key encryption algorithms. - * - * @return preferred symmetric algorithms - */ - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred hash algorithms. - * - * @return preferred hash algorithms - */ - @Nonnull - public Set getPreferredHashAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredHashAlgorithms(getSignatureWithPreferences()); - } - - /** - * Return preferred compression algorithms. - * - * @return preferred compression algorithms - */ - @Nonnull - public Set getPreferredCompressionAlgorithms() { - return SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(getSignatureWithPreferences()); - } - - /** - * Address the key via a user-id (e.g. "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(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key, - @Nonnull String userId) { - super(info, key); - this.userId = userId; - } - - @Override - @Nonnull - public 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(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key) { - super(info, key); - } - - @Override - @Nonnull - public PGPSignature getSignatureWithPreferences() { - String primaryUserId = info.getPrimaryUserId(); - // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the - // preferred symmetric algorithm. - PGPSignature signature = null; - if (primaryUserId != null) { - signature = info.getLatestUserIdCertification(primaryUserId); - } - - if (signature == null) { - signature = info.getLatestDirectKeySelfSignature(); - } - - if (signature == null) { - throw new IllegalStateException("No valid signature found."); - } - return signature; - } - } - - public static class SubKey extends KeyAccessor { - - public SubKey(@Nonnull KeyRingInfo info, - @Nonnull SubkeyIdentifier key) { - super(info, key); - } - - @Override - @Nonnull - public PGPSignature getSignatureWithPreferences() { - PGPSignature signature; - if (key.getPrimaryKeyId() == key.getSubkeyId()) { - signature = info.getLatestDirectKeySelfSignature(); - if (signature == null && info.getPrimaryUserId() != null) { - signature = info.getLatestUserIdCertification(info.getPrimaryUserId()); - } - } else { - signature = info.getCurrentSubkeyBindingSignature(key.getSubkeyId()); - } - - if (signature == null) { - throw new IllegalStateException("No valid signature found."); - } - return signature; - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index 58e7719c..cda7eccd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -31,6 +31,8 @@ class SubkeyIdentifier( val subkeyId = subkeyFingerprint.keyId val primaryKeyId = primaryKeyFingerprint.keyId + val isPrimaryKey = keyId == subkeyId + fun matches(fingerprint: OpenPgpFingerprint) = primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt new file mode 100644 index 00000000..f49a6ca4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +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 + +abstract class KeyAccessor( + protected val info: KeyRingInfo, + protected val key: SubkeyIdentifier +) { + + /** + * Depending on the way we address the key (key-id or user-id), return the respective [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 + */ + abstract val signatureWithPreferences: PGPSignature + + /** + * Preferred symmetric key encryption algorithms. + */ + val preferredSymmetricKeyAlgorithms: Set + get() = SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signatureWithPreferences) + + /** + * Preferred hash algorithms. + */ + val preferredHashAlgorithms: Set + get() = SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signatureWithPreferences) + + /** + * Preferred compression algorithms. + */ + val preferredCompressionAlgorithms: Set + get() = SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signatureWithPreferences) + + /** + * Address the key via a user-id (e.g. `Alice `). + * In this case we are sourcing preferred algorithms from the user-id certification first. + */ + class ViaUserId(info: KeyRingInfo, key: SubkeyIdentifier, private val userId: CharSequence): KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() = checkNotNull(info.getLatestUserIdCertification(userId.toString())) { + "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. + */ + class ViaKeyId(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() { + // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the + // preferred symmetric algorithm. + info.primaryUserId?.let { + userId -> info.getLatestUserIdCertification(userId).let { if (it != null) return it } + } + + return checkNotNull(info.latestDirectKeySelfSignature) { "No valid signature found." } + } + } + + class SubKey(info: KeyRingInfo, key: SubkeyIdentifier): KeyAccessor(info, key) { + override val signatureWithPreferences: PGPSignature + get() = checkNotNull( + if (key.isPrimaryKey) { + info.latestDirectKeySelfSignature ?: + info.primaryUserId?.let { info.getLatestUserIdCertification(it) } + } else { + info.getCurrentSubkeyBindingSignature(key.subkeyId) + } + ) { "No valid signature found." } + + } +} \ No newline at end of file From 8fe9d250a8f3839a8d7b8b0f2e7dba16ecd3134f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Sep 2023 14:57:58 +0200 Subject: [PATCH 145/351] Kotlin conversion: KeyInfo --- .../java/org/pgpainless/key/info/KeyInfo.java | 139 ------------------ .../extensions/PGPPublicKeyExtensions.kt | 37 +++++ .../extensions/PGPSecretKeyExtensions.kt | 29 ++++ .../kotlin/org/pgpainless/key/info/KeyInfo.kt | 75 ++++++++++ .../key/protection/UnlockSecretKey.kt | 4 +- 5 files changed, 143 insertions(+), 141 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java deleted file mode 100644 index f455608e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyInfo.java +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info; - -import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers; -import org.bouncycastle.bcpg.ECDHPublicBCPGKey; -import org.bouncycastle.bcpg.ECDSAPublicBCPGKey; -import org.bouncycastle.bcpg.ECPublicBCPGKey; -import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; - -public class KeyInfo { - - private final PGPSecretKey secretKey; - private final PGPPublicKey publicKey; - - public KeyInfo(PGPSecretKey secretKey) { - this.secretKey = secretKey; - this.publicKey = secretKey.getPublicKey(); - } - - public KeyInfo(PGPPublicKey publicKey) { - this.publicKey = publicKey; - this.secretKey = null; - } - - public String getCurveName() { - return getCurveName(publicKey); - } - - /** - * Returns indication that a contained secret key is encrypted. - * - * @return true if secret key is encrypted, false if secret key is not encrypted or there is public key only. - */ - public boolean isEncrypted() { - return secretKey != null && isEncrypted(secretKey); - } - - /** - * Returns indication that a contained secret key is not encrypted. - * - * @return true if secret key is not encrypted or there is public key only, false if secret key is encrypted. - */ - public boolean isDecrypted() { - return secretKey == null || isDecrypted(secretKey); - } - - /** - * Returns indication that a contained secret key has S2K of a type GNU_DUMMY_S2K. - * - * @return true if secret key has S2K of a type GNU_DUMMY_S2K, false if there is public key only, - * or S2K on the secret key is absent or not of a type GNU_DUMMY_S2K. - */ - public boolean hasDummyS2K() { - return secretKey != null && hasDummyS2K(secretKey); - } - - public static String getCurveName(PGPPublicKey publicKey) { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); - ECPublicBCPGKey key; - switch (algorithm) { - case ECDSA: { - key = (ECDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - case ECDH: { - key = (ECDHPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - case EDDSA: { - key = (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey(); - break; - } - default: - throw new IllegalArgumentException("Not an elliptic curve public key (" + algorithm + ")"); - } - return getCurveName(key); - } - - public static String getCurveName(ECPublicBCPGKey key) { - ASN1ObjectIdentifier identifier = key.getCurveOID(); - - String curveName = ECUtil.getCurveName(identifier); - if (curveName != null) { - return curveName; - } - - // Workaround for ECUtil not recognizing ed25519 - // see https://github.com/bcgit/bc-java/issues/1087 - // UPDATE: Apparently 1087 is not fixed properly with BC 1.71 - // See https://github.com/bcgit/bc-java/issues/1142 - // TODO: Remove once BC comes out with a fix. - if (identifier.equals(GNUObjectIdentifiers.Ed25519)) { - return EdDSACurve._Ed25519.getName(); - } - - return null; - } - - /** - * Returns indication that a secret key is encrypted. - * - * @param secretKey A secret key to examine. - * @return true if secret key is encrypted, false otherwise. - */ - public static boolean isEncrypted(PGPSecretKey secretKey) { - return secretKey.getS2KUsage() != 0; - } - - /** - * Returns indication that a secret key is not encrypted. - * - * @param secretKey A secret key to examine. - * @return true if secret key is encrypted, false otherwise. - */ - public static boolean isDecrypted(PGPSecretKey secretKey) { - return secretKey.getS2KUsage() == 0; - } - - /** - * Returns indication that a secret key has S2K of a type GNU_DUMMY_S2K. - * - * @param secretKey A secret key to examine. - * @return true if secret key has S2K of a type GNU_DUMMY_S2K, false otherwise. - */ - public static boolean hasDummyS2K(PGPSecretKey secretKey) { - final S2K s2k = secretKey.getS2K(); - return s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt new file mode 100644 index 00000000..a1ed72fe --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers +import org.bouncycastle.bcpg.ECDHPublicBCPGKey +import org.bouncycastle.bcpg.ECDSAPublicBCPGKey +import org.bouncycastle.bcpg.EdDSAPublicBCPGKey +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.generation.type.eddsa.EdDSACurve + +/** + * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and [PublicKeyAlgorithm.EDDSA], + * this method returns the name of the underlying elliptic curve. + * + * For other key types or unknown curves, this method throws an [IllegalArgumentException]. + * + * @return curve name + */ +fun PGPPublicKey.getCurveName(): String { + PublicKeyAlgorithm.requireFromId(algorithm) + .let { + when (it) { + PublicKeyAlgorithm.ECDSA -> publicKeyPacket.key as ECDSAPublicBCPGKey + PublicKeyAlgorithm.ECDH -> publicKeyPacket.key as ECDHPublicBCPGKey + PublicKeyAlgorithm.EDDSA -> publicKeyPacket.key as EdDSAPublicBCPGKey + else -> throw IllegalArgumentException("No an elliptic curve public key ($it).") + } + } + .let { if (it.curveOID == GNUObjectIdentifiers.Ed25519) return EdDSACurve._Ed25519.curveName else it.curveOID} + .let { it to ECUtil.getCurveName(it) } + .let { if (it.second != null) return it.second else throw IllegalArgumentException("Unknown curve: ${it.first}") } +} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt new file mode 100644 index 00000000..cba7bdba --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.openpgp.PGPSecretKey + +/** + * Returns indication that the secret key is encrypted. + * + * @return true if secret key is encrypted, false otherwise. + */ +fun PGPSecretKey?.isEncrypted(): Boolean = (this != null) && (s2KUsage != 0) + +/** + * Returns indication that the secret key is not encrypted. + * + * @return true if secret key is encrypted, false otherwise. + */ +fun PGPSecretKey?.isDecrypted(): Boolean = (this == null) || (s2KUsage == 0) + +/** + * Returns indication that the secret key has S2K of a type GNU_DUMMY_S2K. + * + * @return true if secret key has S2K of type GNU_DUMMY_S2K, false otherwise. + */ +fun PGPSecretKey?.hasDummyS2K(): Boolean = (this != null) && (s2K?.type == S2K.GNU_DUMMY_S2K) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt new file mode 100644 index 00000000..652cf22d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub , 2021 Flowcrypt a.s. +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +import org.bouncycastle.extensions.getCurveName +import org.bouncycastle.extensions.hasDummyS2K +import org.bouncycastle.extensions.isDecrypted +import org.bouncycastle.extensions.isEncrypted +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey + +@Deprecated("Deprecated in favor of extension functions to PGPSecretKey and PGPPublicKey.") +class KeyInfo private constructor( + val secretKey: PGPSecretKey?, + val publicKey: PGPPublicKey) { + + constructor(secretKey: PGPSecretKey): this(secretKey, secretKey.publicKey) + constructor(publicKey: PGPPublicKey): this(null, publicKey) + + /** + * Return the name of the elliptic curve used by this key, or throw an [IllegalArgumentException] if the key + * is not based on elliptic curves, or on an unknown curve. + */ + @Deprecated("Deprecated in favor of calling getCurveName() on the PGPPublicKey itself.", + ReplaceWith("publicKey.getCurveName()")) + val curveName: String + get() = publicKey.getCurveName() + + /** + * Return true, if the secret key is encrypted. + * This method returns false, if the secret key is null. + */ + @Deprecated("Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) + val isEncrypted: Boolean + get() = secretKey?.isEncrypted() ?: false + + /** + * Return true, if the secret key is decrypted. + * This method returns true, if the secret key is null. + */ + @Deprecated("Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) + val isDecrypted: Boolean + get() = secretKey?.isDecrypted() ?: true + + /** + * Return true, if the secret key is using the GNU_DUMMY_S2K s2k type. + * This method returns false, if the secret key is null. + */ + @Deprecated("Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) + val hasDummyS2K: Boolean + @JvmName("hasDummyS2K") + get() = secretKey?.hasDummyS2K() ?: false + + companion object { + @JvmStatic + @Deprecated("Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) + fun isEncrypted(secretKey: PGPSecretKey?) = secretKey.isEncrypted() + + @JvmStatic + @Deprecated("Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) + fun isDecrypted(secretKey: PGPSecretKey?) = secretKey.isDecrypted() + + @JvmStatic + @Deprecated("Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) + fun hasDummyS2K(secretKey: PGPSecretKey?) = secretKey.hasDummyS2K() + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index 1317ef05..84d7d0d7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -6,6 +6,7 @@ package org.pgpainless.key.protection import openpgp.openPgpKeyId +import org.bouncycastle.extensions.isEncrypted import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey @@ -13,7 +14,6 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.pgpainless.PGPainless import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException -import org.pgpainless.key.info.KeyInfo import org.pgpainless.key.util.PublicKeyParameterValidationUtil import org.pgpainless.util.Passphrase import kotlin.jvm.Throws @@ -25,7 +25,7 @@ class UnlockSecretKey { @JvmStatic @Throws(PGPException::class, KeyIntegrityException::class) fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { - return if (KeyInfo.isEncrypted(secretKey)) { + return if (secretKey.isEncrypted()) { unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyID)) } else { unlockSecretKey(secretKey, null as PBESecretKeyDecryptor?) From 9ee0f09b8d01b149b3ce0fa12bec5484ddc40cf3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Sep 2023 15:01:34 +0200 Subject: [PATCH 146/351] Fix bug caused by false field comparison in SubkeyIdentifier --- .../src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index cda7eccd..a793fc99 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -31,7 +31,7 @@ class SubkeyIdentifier( val subkeyId = subkeyFingerprint.keyId val primaryKeyId = primaryKeyFingerprint.keyId - val isPrimaryKey = keyId == subkeyId + val isPrimaryKey = primaryKeyId == subkeyId fun matches(fingerprint: OpenPgpFingerprint) = primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint From 85e2fe956a3cea752a97d9ca3e6128350ec60856 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 8 Sep 2023 15:03:49 +0200 Subject: [PATCH 147/351] Add test for SubkeyIdentifier.isPrimaryKey() --- .../java/org/pgpainless/key/SubkeyIdentifierTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java index fe792e4d..add18fd5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/SubkeyIdentifierTest.java @@ -5,8 +5,10 @@ package org.pgpainless.key; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.NoSuchElementException; @@ -102,6 +104,15 @@ public class SubkeyIdentifierTest { assertNotEquals(id1, null); } + @Test + public void testIsPrimaryKey() { + SubkeyIdentifier primaryKey = new SubkeyIdentifier(PRIMARY_FP); + assertTrue(primaryKey.isPrimaryKey()); + + SubkeyIdentifier subKey = new SubkeyIdentifier(PRIMARY_FP, SUBKEY_FP); + assertFalse(subKey.isPrimaryKey()); + } + @Test public void nonExistentSubkeyThrowsNoSuchElementException() { assertThrows(NoSuchElementException.class, () -> new SubkeyIdentifier(CERT, 123)); From 6f9e692474b1e2d77fcdb885a67bd984a33c2651 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 9 Oct 2023 12:49:04 +0200 Subject: [PATCH 148/351] Kotlin conversion: KeyRingInfo --- .../org/pgpainless/key/info/KeyRingInfo.java | 1318 ----------------- .../org/pgpainless/key/info/package-info.java | 8 - .../extensions/PGPSignatureExtensions.kt | 7 + .../org/pgpainless/key/info/KeyRingInfo.kt | 765 ++++++++++ 4 files changed, 772 insertions(+), 1326 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt 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 deleted file mode 100644 index 31a6dd86..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ /dev/null @@ -1,1318 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.info; - -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.NoSuchElementException; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -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.CompressionAlgorithm; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.RevocationState; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.util.KeyIdUtil; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.consumer.SignaturePicker; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.DateUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Utility class to quickly extract certain information from a {@link PGPPublicKeyRing}/{@link PGPSecretKeyRing}. - */ -public class KeyRingInfo { - - private static final Pattern PATTERN_EMAIL_FROM_USERID = Pattern.compile("<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>"); - private static final Pattern PATTERN_EMAIL_EXPLICIT = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$"); - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingInfo.class); - - private final PGPKeyRing keys; - private final Signatures signatures; - private final Date referenceDate; - private final String primaryUserId; - private final RevocationState revocationState; - - /** - * Evaluate the key ring at creation time of the given signature. - * - * @param keyRing key ring - * @param signature signature - * @return info of key ring at signature creation time - */ - @Nonnull - public static KeyRingInfo evaluateForSignature(@Nonnull PGPKeyRing keyRing, - @Nonnull PGPSignature signature) { - return new KeyRingInfo(keyRing, signature.getCreationTime()); - } - - /** - * Evaluate the key ring right now. - * - * @param keys key ring - */ - public KeyRingInfo(@Nonnull PGPKeyRing keys) { - this(keys, new Date()); - } - - /** - * Evaluate the key ring at the provided validation date. - * - * @param keys key ring - * @param referenceDate date of validation - */ - public KeyRingInfo(@Nonnull PGPKeyRing keys, - @Nonnull Date referenceDate) { - this(keys, PGPainless.getPolicy(), referenceDate); - } - - /** - * Evaluate the key ring at the provided validation date. - * - * @param keys key ring - * @param policy policy - * @param referenceDate validation date - */ - public KeyRingInfo(@Nonnull PGPKeyRing keys, - @Nonnull Policy policy, - @Nonnull Date referenceDate) { - this.referenceDate = referenceDate; - this.keys = keys; - this.signatures = new Signatures(keys, this.referenceDate, policy); - this.primaryUserId = findPrimaryUserId(); - this.revocationState = findRevocationState(); - } - - /** - * Return the underlying {@link PGPKeyRing}. - * @return keys - */ - public PGPKeyRing getKeys() { - return keys; - } - - public List getValidSubkeys() { - List subkeys = new ArrayList<>(); - Iterator iterator = getKeys().getPublicKeys(); - while (iterator.hasNext()) { - PGPPublicKey key = iterator.next(); - if (isKeyValidlyBound(key.getKeyID())) { - subkeys.add(key); - } - } - return subkeys; - } - - @Nonnull - private RevocationState findRevocationState() { - PGPSignature revocation = signatures.primaryKeyRevocation; - if (revocation != null) { - return SignatureUtils.isHardRevocation(revocation) ? - RevocationState.hardRevoked() : RevocationState.softRevoked(revocation.getCreationTime()); - } - return RevocationState.notRevoked(); - } - - /** - * Return the first {@link PGPPublicKey} of this key ring. - * - * @return public key - */ - @Nonnull - public PGPPublicKey getPublicKey() { - return keys.getPublicKey(); - } - - /** - * Return the public key with the given fingerprint. - * - * @param fingerprint fingerprint - * @return public key or null - */ - @Nullable - public PGPPublicKey getPublicKey(@Nonnull OpenPgpFingerprint fingerprint) { - return getPublicKey(fingerprint.getKeyId()); - } - - /** - * Return the public key with the given key id. - * - * @param keyId key id - * @return public key or null - */ - @Nullable - public PGPPublicKey getPublicKey(long keyId) { - return getPublicKey(keys, keyId); - } - - /** - * Return the public key with the given key id from the provided key ring. - * - * @param keyRing key ring - * @param keyId key id - * @return public key or null - */ - @Nullable - public static PGPPublicKey getPublicKey(@Nonnull PGPKeyRing keyRing, long keyId) { - return keyRing.getPublicKey(keyId); - } - - /** - * Return true if the public key with the given key id is bound to the key ring properly. - * - * @param keyId key id - * @return true if key is bound validly - */ - public boolean isKeyValidlyBound(long keyId) { - PGPPublicKey publicKey = keys.getPublicKey(keyId); - if (publicKey == null) { - return false; - } - - if (publicKey == getPublicKey()) { - if (signatures.primaryKeyRevocation != null && SignatureUtils.isHardRevocation(signatures.primaryKeyRevocation)) { - return false; - } - return signatures.primaryKeyRevocation == null; - } - - PGPSignature binding = signatures.subkeyBindings.get(keyId); - PGPSignature revocation = signatures.subkeyRevocations.get(keyId); - - // No valid binding - if (binding == null || SignatureUtils.isSignatureExpired(binding)) { - return false; - } - - // Revocation - if (revocation != null) { - if (SignatureUtils.isHardRevocation(revocation)) { - // Subkey is hard revoked - return false; - } else { - // Key is soft-revoked, not yet re-bound - return SignatureUtils.isSignatureExpired(revocation) - || !revocation.getCreationTime().after(binding.getCreationTime()); - } - } - - return true; - } - - /** - * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. - * The first key in the list being the primary key. - * Note that the list is unmodifiable. - * - * @return list of public keys - */ - @Nonnull - public List getPublicKeys() { - Iterator iterator = keys.getPublicKeys(); - List list = iteratorToList(iterator); - return Collections.unmodifiableList(list); - } - - /** - * Return the primary {@link PGPSecretKey} of this key ring or null if the key ring is not a {@link PGPSecretKeyRing}. - * - * @return primary secret key or null if the key ring is public - */ - @Nullable - public PGPSecretKey getSecretKey() { - if (keys instanceof PGPSecretKeyRing) { - PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; - return secretKeys.getSecretKey(); - } - return null; - } - - /** - * Return the secret key with the given fingerprint. - * - * @param fingerprint fingerprint - * @return secret key or null - */ - @Nullable - public PGPSecretKey getSecretKey(@Nonnull OpenPgpFingerprint fingerprint) { - return getSecretKey(fingerprint.getKeyId()); - } - - /** - * Return the secret key with the given key id. - * - * @param keyId key id - * @return secret key or null - */ - @Nullable - public PGPSecretKey getSecretKey(long keyId) { - if (keys instanceof PGPSecretKeyRing) { - return ((PGPSecretKeyRing) keys).getSecretKey(keyId); - } - return null; - } - - /** - * Return all secret keys of the key ring. - * If the key ring is a {@link PGPPublicKeyRing}, then return an empty list. - * Note that the list is unmodifiable. - * - * @return list of secret keys - */ - @Nonnull - public List getSecretKeys() { - if (keys instanceof PGPSecretKeyRing) { - PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) keys; - Iterator iterator = secretKeys.getSecretKeys(); - return Collections.unmodifiableList(iteratorToList(iterator)); - } - return Collections.emptyList(); - } - - /** - * Return the key id of the primary key of this key ring. - * - * @return key id - */ - public long getKeyId() { - return getPublicKey().getKeyID(); - } - - /** - * Return the {@link OpenPgpFingerprint} of this key ring. - * - * @return fingerprint - */ - @Nonnull - public OpenPgpFingerprint getFingerprint() { - return OpenPgpFingerprint.of(getPublicKey()); - } - - @Nullable - public String getPrimaryUserId() { - return primaryUserId; - } - - /** - * Return the current primary user-id of the key ring. - *

- * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, - * this method returns the first user-id on the key, otherwise null. - * - * @return primary user-id or null - */ - @Nullable - private String findPrimaryUserId() { - String primaryUserId = null; - Date currentModificationDate = null; - - List userIds = getUserIds(); - if (userIds.isEmpty()) { - return null; - } - - String firstUserId = null; - for (String userId : userIds) { - PGPSignature certification = signatures.userIdCertifications.get(userId); - if (certification == null) { - continue; - } - - if (firstUserId == null) { - firstUserId = userId; - } - Date creationTime = certification.getCreationTime(); - - if (certification.getHashedSubPackets().isPrimaryUserID()) { - if (currentModificationDate == null || creationTime.after(currentModificationDate)) { - primaryUserId = userId; - currentModificationDate = creationTime; - } - - } - } - - if (primaryUserId != null) { - return primaryUserId; - } - - return firstUserId; - } - - /** - * Return a list of all user-ids of the primary key. - * Note: This list might also contain expired / revoked user-ids. - * Consider using {@link #getValidUserIds()} instead. - * - * @return list of user-ids - */ - @Nonnull - public List getUserIds() { - return KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.getPublicKey()); - } - - /** - * Return a list of valid user-ids. - * - * @return valid user-ids - */ - @Nonnull - public List getValidUserIds() { - List valid = new ArrayList<>(); - List userIds = getUserIds(); - for (String userId : userIds) { - if (isUserIdBound(userId)) { - valid.add(userId); - } - } - return valid; - } - - /** - * Return a list of all user-ids that were valid at some point, but might be expired by now. - * - * @return bound user-ids - */ - @Nonnull - public List getValidAndExpiredUserIds() { - List probablyExpired = new ArrayList<>(); - List userIds = getUserIds(); - - for (String userId : userIds) { - PGPSignature certification = signatures.userIdCertifications.get(userId); - PGPSignature revocation = signatures.userIdRevocations.get(userId); - - // Unbound user-id - if (certification == null) { - continue; - } - - // Not revoked -> valid - if (revocation == null) { - probablyExpired.add(userId); - continue; - } - - // Hard revocation -> invalid - if (SignatureUtils.isHardRevocation(revocation)) { - continue; - } - - // Soft revocation -> valid if certification is newer than revocation (revalidation) - if (certification.getCreationTime().after(revocation.getCreationTime())) { - probablyExpired.add(userId); - } - } - return probablyExpired; - } - - /** - * Return true if the provided user-id is valid. - * - * @param userId user-id - * @return true if user-id is valid - */ - public boolean isUserIdValid(@Nonnull CharSequence userId) { - if (primaryUserId == null) { - // No primary userID? No userID at all! - return false; - } - - if (!userId.equals(primaryUserId)) { - if (!isUserIdBound(primaryUserId)) { - // primary user-id not valid? UserID not valid! - return false; - } - } - return isUserIdBound(userId); - } - - private boolean isUserIdBound(@Nonnull CharSequence userId) { - String userIdString = userId.toString(); - PGPSignature certification = signatures.userIdCertifications.get(userIdString); - PGPSignature revocation = signatures.userIdRevocations.get(userIdString); - - if (certification == null) { - return false; - } - if (SignatureUtils.isSignatureExpired(certification)) { - return false; - } - if (certification.getHashedSubPackets().isPrimaryUserID()) { - Date keyExpiration = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(certification, keys.getPublicKey()); - if (keyExpiration != null && referenceDate.after(keyExpiration)) { - return false; - } - } - // Not revoked -> valid - if (revocation == null) { - return true; - } - // Hard revocation -> invalid - if (SignatureUtils.isHardRevocation(revocation)) { - return false; - } - // Soft revocation -> valid if certification is newer than revocation (revalidation) - return certification.getCreationTime().after(revocation.getCreationTime()); - } - - /** - * Return a list of all user-ids of the primary key that appear to be email-addresses. - * Note: This list might contain expired / revoked user-ids. - * - * @return email addresses - */ - @Nonnull - public List getEmailAddresses() { - List userIds = getUserIds(); - List emails = new ArrayList<>(); - for (String userId : userIds) { - Matcher matcher = PATTERN_EMAIL_FROM_USERID.matcher(userId); - if (matcher.find()) { - emails.add(matcher.group(1)); - } else { - matcher = PATTERN_EMAIL_EXPLICIT.matcher(userId); - if (matcher.find()) { - emails.add(matcher.group(1)); - } - } - } - return emails; - } - - /** - * Return the latest direct-key self signature. - *

- * Note: This signature might be expired (check with {@link SignatureUtils#isSignatureExpired(PGPSignature)}). - * - * @return latest direct key self-signature or null - */ - @Nullable - public PGPSignature getLatestDirectKeySelfSignature() { - return signatures.primaryKeySelfSignature; - } - - /** - * Return the latest revocation self-signature on the primary key. - * - * @return revocation or null - */ - @Nullable - public PGPSignature getRevocationSelfSignature() { - return signatures.primaryKeyRevocation; - } - - /** - * Return the latest certification self-signature on the provided user-id. - * - * @param userId user-id - * @return certification signature or null - */ - @Nullable - public PGPSignature getLatestUserIdCertification(@Nonnull CharSequence userId) { - return signatures.userIdCertifications.get(userId.toString()); - } - - /** - * Return the latest user-id revocation signature for the provided user-id. - * - * @param userId user-id - * @return revocation or null - */ - @Nullable - public PGPSignature getUserIdRevocation(@Nonnull CharSequence userId) { - return signatures.userIdRevocations.get(userId.toString()); - } - - /** - * Return the currently active subkey binding signature for the subkey with the provided key-id. - * - * @param keyId subkey id - * @return subkey binding signature or null - */ - @Nullable - public PGPSignature getCurrentSubkeyBindingSignature(long keyId) { - return signatures.subkeyBindings.get(keyId); - } - - /** - * Return the latest subkey binding revocation signature for the subkey with the given key-id. - * - * @param keyId subkey id - * @return subkey binding revocation or null - */ - @Nullable - public PGPSignature getSubkeyRevocationSignature(long keyId) { - return signatures.subkeyRevocations.get(keyId); - } - - /** - * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. - * @param keyId key-id - * @return list of key flags - */ - @Nonnull - public List getKeyFlagsOf(long keyId) { - // key is primary key - if (getPublicKey().getKeyID() == keyId) { - - PGPSignature directKeySignature = getLatestDirectKeySelfSignature(); - if (directKeySignature != null) { - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(directKeySignature); - if (keyFlags != null) { - return keyFlags; - } - } - - String primaryUserId = getPrimaryUserId(); - if (primaryUserId != null) { - PGPSignature userIdSignature = getLatestUserIdCertification(primaryUserId); - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdSignature); - if (keyFlags != null) { - return keyFlags; - } - } - } - // Key is subkey - else { - PGPSignature bindingSignature = getCurrentSubkeyBindingSignature(keyId); - if (bindingSignature != null) { - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(bindingSignature); - if (keyFlags != null) { - return keyFlags; - } - } - } - return Collections.emptyList(); - } - - /** - * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. - * - * @param userId user-id - * @return key flags - */ - @Nonnull - public List getKeyFlagsOf(String userId) { - if (!isUserIdValid(userId)) { - return Collections.emptyList(); - } - - PGPSignature userIdCertification = getLatestUserIdCertification(userId); - if (userIdCertification == null) { - throw new AssertionError("While user-id '" + userId + "' was reported as valid, there appears to be no certification for it."); - } - - List keyFlags = SignatureSubpacketsUtil.parseKeyFlags(userIdCertification); - if (keyFlags != null) { - return keyFlags; - } - return Collections.emptyList(); - } - - /** - * Return the algorithm of the primary key. - * - * @return public key algorithm - */ - @Nonnull - public PublicKeyAlgorithm getAlgorithm() { - return PublicKeyAlgorithm.requireFromId(getPublicKey().getAlgorithm()); - } - - /** - * Return the creation date of the primary key. - * - * @return creation date - */ - @Nonnull - public Date getCreationDate() { - return getPublicKey().getCreationTime(); - } - - /** - * Return the date on which the key ring was last modified. - * This date corresponds to the date of the last signature that was made on this key ring by the primary key. - * - * @return last modification date. - */ - @Nonnull - public Date getLastModified() { - PGPSignature mostRecent = getMostRecentSignature(); - if (mostRecent == null) { - // No sigs found. Return public key creation date instead. - return getLatestKeyCreationDate(); - } - return mostRecent.getCreationTime(); - } - - /** - * Return the creation time of the latest added subkey. - * - * @return latest key creation time - */ - @Nonnull - public Date getLatestKeyCreationDate() { - Date latestCreation = null; - for (PGPPublicKey key : getPublicKeys()) { - if (!isKeyValidlyBound(key.getKeyID())) { - continue; - } - Date keyCreation = key.getCreationTime(); - if (latestCreation == null || latestCreation.before(keyCreation)) { - latestCreation = keyCreation; - } - } - if (latestCreation == null) { - throw new AssertionError("Apparently there is no validly bound key in this key ring."); - } - return latestCreation; - } - - @Nullable - private PGPSignature getMostRecentSignature() { - Set allSignatures = new HashSet<>(); - PGPSignature mostRecentSelfSignature = getLatestDirectKeySelfSignature(); - PGPSignature revocationSelfSignature = getRevocationSelfSignature(); - if (mostRecentSelfSignature != null) allSignatures.add(mostRecentSelfSignature); - if (revocationSelfSignature != null) allSignatures.add(revocationSelfSignature); - 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) { - if (mostRecent == null || signature.getCreationTime().after(mostRecent.getCreationTime())) { - mostRecent = signature; - } - } - return mostRecent; - } - - @Nonnull - public RevocationState getRevocationState() { - return revocationState; - } - - /** - * Return the date on which the primary key was revoked, or null if it has not yet been revoked. - * - * @return revocation date or null - */ - @Nullable - public Date getRevocationDate() { - return getRevocationState().isSoftRevocation() ? getRevocationState().getDate() : null; - } - - /** - * Return the date of expiration of the primary key or null if the key has no expiration date. - * - * @return expiration date - */ - @Nullable - public Date getPrimaryKeyExpirationDate() { - PGPSignature directKeySig = getLatestDirectKeySelfSignature(); - Date directKeyExpirationDate = null; - if (directKeySig != null) { - directKeyExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(directKeySig, getPublicKey()); - } - - PGPSignature primaryUserIdCertification = null; - Date userIdExpirationDate = null; - String possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId(); - if (possiblyExpiredPrimaryUserId != null) { - primaryUserIdCertification = getLatestUserIdCertification(possiblyExpiredPrimaryUserId); - if (primaryUserIdCertification != null) { - userIdExpirationDate = SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(primaryUserIdCertification, getPublicKey()); - } - } - - if (directKeySig == null && primaryUserIdCertification == null) { - throw new NoSuchElementException("No direct-key signature and no user-id signature found."); - } - - if (directKeyExpirationDate != null && userIdExpirationDate == null) { - return directKeyExpirationDate; - } - - if (directKeyExpirationDate == null) { - return userIdExpirationDate; - } - - if (directKeyExpirationDate.before(userIdExpirationDate)) { - return directKeyExpirationDate; - } - - return userIdExpirationDate; - } - - @Nullable - public String getPossiblyExpiredPrimaryUserId() { - String validPrimaryUserId = getPrimaryUserId(); - if (validPrimaryUserId != null) { - return validPrimaryUserId; - } - - Date latestCreationTime = null; - String primaryUserId = null; - boolean foundPrimary = false; - for (String userId : getUserIds()) { - PGPSignature signature = getLatestUserIdCertification(userId); - if (signature == null) { - continue; - } - - boolean isPrimary = signature.getHashedSubPackets().isPrimaryUserID(); - if (foundPrimary && !isPrimary) { - continue; - } - - Date creationTime = signature.getCreationTime(); - if (latestCreationTime == null || creationTime.after(latestCreationTime) || isPrimary && !foundPrimary) { - latestCreationTime = creationTime; - primaryUserId = userId; - } - - foundPrimary |= isPrimary; - } - - return primaryUserId; - } - - /** - * Return the expiration date of the subkey with the provided fingerprint. - * - * @param fingerprint subkey fingerprint - * @return expiration date or null - */ - @Nullable - public Date getSubkeyExpirationDate(OpenPgpFingerprint fingerprint) { - if (getPublicKey().getKeyID() == fingerprint.getKeyId()) { - return getPrimaryKeyExpirationDate(); - } - - PGPPublicKey subkey = getPublicKey(fingerprint.getKeyId()); - if (subkey == null) { - throw new NoSuchElementException("No subkey with fingerprint " + fingerprint + " found."); - } - - PGPSignature bindingSig = getCurrentSubkeyBindingSignature(fingerprint.getKeyId()); - if (bindingSig == null) { - throw new AssertionError("Subkey has no valid binding signature."); - } - - return SignatureUtils.getKeyExpirationDate(subkey.getCreationTime(), bindingSig); - } - - /** - * Return the latest date on which the key ring is still usable for the given key flag. - * If only a subkey is carrying the required flag and the primary key expires earlier than the subkey, - * the expiry date of the primary key is returned. - *

- * This method might return null, if the primary key and a subkey with the required flag does not expire. - * @param use key flag representing the use case, e.g. {@link KeyFlag#SIGN_DATA} or - * {@link KeyFlag#ENCRYPT_COMMS}/{@link KeyFlag#ENCRYPT_STORAGE}. - * @return latest date on which the key ring can be used for the given use case, or null if it can be used indefinitely. - */ - @Nullable - public Date getExpirationDateForUse(KeyFlag use) { - if (use == KeyFlag.SPLIT || use == KeyFlag.SHARED) { - throw new IllegalArgumentException("SPLIT and SHARED are not uses, but properties."); - } - - Date primaryExpiration = getPrimaryKeyExpirationDate(); - List nonExpiringSubkeys = new ArrayList<>(); - Date latestSubkeyExpirationDate = null; - - List keysWithFlag = getKeysWithKeyFlag(use); - if (keysWithFlag.isEmpty()) { - throw new NoSuchElementException("No key with the required key flag found."); - } - - for (PGPPublicKey key : keysWithFlag) { - Date subkeyExpirationDate = getSubkeyExpirationDate(OpenPgpFingerprint.of(key)); - if (subkeyExpirationDate == null) { - nonExpiringSubkeys.add(key); - } else { - if (latestSubkeyExpirationDate == null || subkeyExpirationDate.after(latestSubkeyExpirationDate)) { - latestSubkeyExpirationDate = subkeyExpirationDate; - } - } - } - - if (nonExpiringSubkeys.isEmpty()) { - if (primaryExpiration == null) { - return latestSubkeyExpirationDate; - } - if (latestSubkeyExpirationDate.before(primaryExpiration)) { - return latestSubkeyExpirationDate; - } - } - return primaryExpiration; - } - - public boolean isHardRevoked(@Nonnull CharSequence userId) { - PGPSignature revocation = signatures.userIdRevocations.get(userId.toString()); - if (revocation == null) { - return false; - } - RevocationReason revocationReason = revocation.getHashedSubPackets().getRevocationReason(); - return revocationReason == null || RevocationAttributes.Reason.isHardRevocation(revocationReason.getRevocationReason()); - } - - /** - * Return true if the key ring is a {@link PGPSecretKeyRing}. - * If it is a {@link PGPPublicKeyRing} return false and if it is neither, throw an {@link AssertionError}. - * - * @return true if the key ring is a secret key ring. - */ - public boolean isSecretKey() { - if (keys instanceof PGPSecretKeyRing) { - return true; - } else if (keys instanceof PGPPublicKeyRing) { - return false; - } else { - throw new AssertionError("Expected PGPKeyRing to be either PGPPublicKeyRing or PGPSecretKeyRing, but got " + keys.getClass().getName() + " instead."); - } - } - - /** - * Returns true when every secret key on the key ring is not encrypted. - * If there is at least one encrypted secret key on the key ring, returns false. - * If the key ring is a {@link PGPPublicKeyRing}, returns true. - * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect the result. - * - * @return true if all secret keys are unencrypted. - */ - public boolean isFullyDecrypted() { - if (!isSecretKey()) { - return true; - } - for (PGPSecretKey secretKey : getSecretKeys()) { - if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isEncrypted(secretKey)) { - return false; - } - } - return true; - } - - /** - * Returns true when every secret key on the key ring is encrypted. - * If there is at least one not encrypted secret key on the key ring, returns false. - * If the key ring is a {@link PGPPublicKeyRing}, returns false. - * Sub-keys with S2K of a type GNU_DUMMY_S2K do not affect a result. - * - * @return true if all secret keys are encrypted. - */ - public boolean isFullyEncrypted() { - if (!isSecretKey()) { - return false; - } - for (PGPSecretKey secretKey : getSecretKeys()) { - if (!KeyInfo.hasDummyS2K(secretKey) && KeyInfo.isDecrypted(secretKey)) { - return false; - } - } - return true; - } - - /** - * Return the version number of the public keys format. - * - * @return version - */ - public int getVersion() { - return keys.getPublicKey().getVersion(); - } - - /** - * Return a list of all subkeys which can be used for encryption of the given purpose. - * This list does not include expired or revoked keys. - * - * @param purpose purpose (encrypt data at rest / communications) - * @return encryption subkeys - */ - @Nonnull - public List getEncryptionSubkeys(@Nonnull EncryptionPurpose purpose) { - Date primaryExpiration = getPrimaryKeyExpirationDate(); - if (primaryExpiration != null && primaryExpiration.before(referenceDate)) { - LOGGER.debug("Certificate is expired: Primary key is expired on " + DateUtil.formatUTCDate(primaryExpiration)); - return Collections.emptyList(); - } - - Iterator subkeys = keys.getPublicKeys(); - List encryptionKeys = new ArrayList<>(); - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - if (!isKeyValidlyBound(subKey.getKeyID())) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not validly bound."); - continue; - } - - Date subkeyExpiration = getSubkeyExpirationDate(OpenPgpFingerprint.of(subKey)); - if (subkeyExpiration != null && subkeyExpiration.before(referenceDate)) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is expired on " + DateUtil.formatUTCDate(subkeyExpiration)); - continue; - } - - if (!subKey.isEncryptionKey()) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " algorithm is not capable of encryption."); - continue; - } - - List keyFlags = getKeyFlagsOf(subKey.getKeyID()); - switch (purpose) { - case COMMUNICATIONS: - if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS)) { - encryptionKeys.add(subKey); - } - break; - case STORAGE: - if (keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { - encryptionKeys.add(subKey); - } - break; - case ANY: - if (keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE)) { - encryptionKeys.add(subKey); - } - break; - } - } - return encryptionKeys; - } - - /** - * Return a list of all subkeys that could potentially be used to decrypt a message. - * Contrary to {@link #getEncryptionSubkeys(EncryptionPurpose)}, this method also includes revoked, expired keys, - * as well as keys which do not carry any encryption keyflags. - * Merely keys which use algorithms that cannot be used for encryption at all are excluded. - * That way, decryption of messages produced by faulty implementations can still be decrypted. - * - * @return decryption keys - */ - @Nonnull - public List getDecryptionSubkeys() { - Iterator subkeys = keys.getPublicKeys(); - List decryptionKeys = new ArrayList<>(); - - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - // subkeys have been valid at some point - if (subKey.getKeyID() != getKeyId()) { - PGPSignature binding = signatures.subkeyBindings.get(subKey.getKeyID()); - if (binding == null) { - LOGGER.debug("Subkey " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " was never validly bound."); - continue; - } - } - - // Public-Key algorithm can encrypt - if (!subKey.isEncryptionKey()) { - LOGGER.debug("(Sub?)-Key " + KeyIdUtil.formatKeyId(subKey.getKeyID()) + " is not encryption-capable."); - continue; - } - - decryptionKeys.add(subKey); - } - return decryptionKeys; - } - - /** - * Return a list of all keys which carry the provided key flag in their signature. - * - * @param flag flag - * @return keys with flag - */ - @Nonnull - public List getKeysWithKeyFlag(@Nonnull KeyFlag flag) { - List keysWithFlag = new ArrayList<>(); - for (PGPPublicKey key : getPublicKeys()) { - List keyFlags = getKeyFlagsOf(key.getKeyID()); - if (keyFlags.contains(flag)) { - keysWithFlag.add(key); - } - } - - return keysWithFlag; - } - - /** - * Return a list of all subkeys that can be used for encryption with the given user-id. - * This list does not include expired or revoked keys. - * TODO: Does it make sense to pass in a user-id? - * Aren't the encryption subkeys the same, regardless of which user-id is used? - * - * @param userId user-id - * @param purpose encryption purpose - * @return encryption subkeys - */ - @Nonnull - public List getEncryptionSubkeys(@Nullable CharSequence userId, - @Nonnull EncryptionPurpose purpose) { - if (userId != null && !isUserIdValid(userId)) { - throw new KeyException.UnboundUserIdException( - OpenPgpFingerprint.of(keys), - userId.toString(), - getLatestUserIdCertification(userId), - getUserIdRevocation(userId) - ); - } - - return getEncryptionSubkeys(purpose); - } - - /** - * Return a list of all subkeys which can be used to sign data. - * - * @return signing keys - */ - @Nonnull - public List getSigningSubkeys() { - Iterator subkeys = keys.getPublicKeys(); - List signingKeys = new ArrayList<>(); - while (subkeys.hasNext()) { - PGPPublicKey subKey = subkeys.next(); - - if (!isKeyValidlyBound(subKey.getKeyID())) { - continue; - } - - List keyFlags = getKeyFlagsOf(subKey.getKeyID()); - if (keyFlags.contains(KeyFlag.SIGN_DATA)) { - signingKeys.add(subKey); - } - } - return signingKeys; - } - - @Nonnull - public Set getPreferredHashAlgorithms() { - return getPreferredHashAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredHashAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredHashAlgorithms(); - } - - @Nonnull - public Set getPreferredHashAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)) - .getPreferredHashAlgorithms(); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms() { - return getPreferredSymmetricKeyAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredSymmetricKeyAlgorithms(); - } - - @Nonnull - public Set getPreferredSymmetricKeyAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredSymmetricKeyAlgorithms(); - } - - @Nonnull - public Set getPreferredCompressionAlgorithms() { - return getPreferredCompressionAlgorithms(getPrimaryUserId()); - } - - @Nonnull - public Set getPreferredCompressionAlgorithms(@Nullable CharSequence userId) { - return getKeyAccessor(userId, getKeyId()).getPreferredCompressionAlgorithms(); - } - - @Nonnull - public Set getPreferredCompressionAlgorithms(long keyId) { - return new KeyAccessor.SubKey(this, new SubkeyIdentifier(keys, keyId)).getPreferredCompressionAlgorithms(); - } - - public boolean isUsableForThirdPartyCertification() { - return isKeyValidlyBound(getKeyId()) && getKeyFlagsOf(getKeyId()).contains(KeyFlag.CERTIFY_OTHER); - } - - /** - * Returns true, if the certificate has at least one usable encryption subkey. - * - * @return true if usable for encryption - */ - public boolean isUsableForEncryption() { - return isUsableForEncryption(EncryptionPurpose.ANY); - } - - /** - * Returns true, if the certificate has at least one usable encryption subkey for the given purpose. - * - * @param purpose purpose of encryption - * @return true if usable for encryption - */ - public boolean isUsableForEncryption(@Nonnull EncryptionPurpose purpose) { - return isKeyValidlyBound(getKeyId()) && !getEncryptionSubkeys(purpose).isEmpty(); - } - - /** - * Returns true, if the key ring is capable of signing. - * Contrary to {@link #isUsableForSigning()}, this method also returns true, if this {@link KeyRingInfo} is based - * on a key ring which has at least one valid public key marked for signing. - * The secret key is not required for the key ring to qualify as signing capable. - * - * @return true if key corresponding to the cert is capable of signing - */ - public boolean isSigningCapable() { - // check if primary-key is revoked / expired - if (!isKeyValidlyBound(getKeyId())) { - return false; - } - // check if it has signing-capable key - return !getSigningSubkeys().isEmpty(); - } - - /** - * Returns true, if this {@link KeyRingInfo} is based on a {@link PGPSecretKeyRing}, which has a valid signing key - * which is ready to be used (i.e. secret key is present and is not on a smart-card). - *

- * If you just want to check, whether a key / certificate has signing capable subkeys, - * use {@link #isSigningCapable()} instead. - * - * @return true if key is ready to be used for signing - */ - public boolean isUsableForSigning() { - if (!isSigningCapable()) { - return false; - } - - List signingKeys = getSigningSubkeys(); - for (PGPPublicKey pk : signingKeys) { - return isSecretKeyAvailable(pk.getKeyID()); - } - // No usable secret key found - return false; - } - - public boolean isSecretKeyAvailable(long keyId) { - PGPSecretKey sk = getSecretKey(keyId); - if (sk == null) { - // Missing secret key - return false; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - return false; - } - // protected secret key - return true; - } - - private KeyAccessor getKeyAccessor(@Nullable CharSequence userId, long keyID) { - if (getPublicKey(keyID) == null) { - throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); - } - - if (userId != null && !getUserIds().contains(userId.toString())) { - throw new NoSuchElementException("No user-id '" + userId + "' found on this key."); - } - - if (userId != null) { - return new KeyAccessor.ViaUserId(this, new SubkeyIdentifier(keys, keyID), userId.toString()); - } else { - return new KeyAccessor.ViaKeyId(this, new SubkeyIdentifier(keys, keyID)); - } - } - - 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(@Nonnull PGPKeyRing keyRing, - @Nonnull Date referenceDate, - @Nonnull Policy policy) { - primaryKeyRevocation = SignaturePicker.pickCurrentRevocationSelfSignature(keyRing, policy, referenceDate); - primaryKeySelfSignature = SignaturePicker.pickLatestDirectKeySignature(keyRing, policy, referenceDate); - userIdRevocations = new HashMap<>(); - userIdCertifications = new HashMap<>(); - subkeyRevocations = new HashMap<>(); - subkeyBindings = new HashMap<>(); - - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keyRing.getPublicKey()); - for (String userId : userIds) { - PGPSignature revocation = SignaturePicker.pickCurrentUserIdRevocationSignature(keyRing, userId, policy, referenceDate); - if (revocation != null) { - userIdRevocations.put(userId, revocation); - } - PGPSignature certification = SignaturePicker.pickLatestUserIdCertificationSignature(keyRing, userId, policy, referenceDate); - 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, policy, referenceDate); - if (subkeyRevocation != null) { - subkeyRevocations.put(subkey.getKeyID(), subkeyRevocation); - } - PGPSignature subkeyBinding = SignaturePicker.pickLatestSubkeyBindingSignature(keyRing, subkey, policy, referenceDate); - if (subkeyBinding != null) { - subkeyBindings.put(subkey.getKeyID(), subkeyBinding); - } - } - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java deleted file mode 100644 index 9f33dd40..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Extract information from PGPKeyRings. - */ -package org.pgpainless.key.info; diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index ccbb8f45..16f61304 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -5,6 +5,7 @@ package org.bouncycastle.extensions import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.RevocationState import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.signature.SignatureUtils import java.util.* @@ -44,3 +45,9 @@ fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint) = SignatureUtils.w * Return true, if this signature is a hard revocation. */ fun PGPSignature.isHardRevocation() = SignatureUtils.isHardRevocation(this) + +fun PGPSignature?.toRevocationState() = + if (this == null) RevocationState.notRevoked() + else + if (isHardRevocation()) RevocationState.hardRevoked() + else RevocationState.softRevoked(creationTime) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt new file mode 100644 index 00000000..79cc8683 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -0,0 +1,765 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.info + +import openpgp.openPgpKeyId +import org.bouncycastle.extensions.* +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.* +import org.pgpainless.exception.KeyException.UnboundUserIdException +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.signature.SignatureUtils.Companion.isHardRevocation +import org.pgpainless.signature.SignatureUtils.Companion.isSignatureExpired +import org.pgpainless.signature.consumer.SignaturePicker +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate +import org.pgpainless.util.DateUtil +import org.slf4j.LoggerFactory +import java.security.Key +import java.util.* +import kotlin.NoSuchElementException + +class KeyRingInfo( + val keys: PGPKeyRing, + val policy: Policy = PGPainless.getPolicy(), + val referenceDate: Date = Date()) { + + @JvmOverloads + constructor(keys: PGPKeyRing, referenceDate: Date = Date()): this(keys, PGPainless.getPolicy(), referenceDate) + + private val signatures: Signatures = Signatures(keys, referenceDate, policy) + + /** + * Primary {@link PGPPublicKey}.´ + */ + val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) + + /** + * Primary key ID. + */ + val keyId: Long = publicKey.keyID + + /** + * Primary key fingerprint. + */ + val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys) + + /** + * All User-IDs (valid, expired, revoked). + */ + val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + + /** + * Primary User-ID. + */ + val primaryUserId = findPrimaryUserId() + + /** + * Revocation State. + */ + val revocationState = signatures.primaryKeyRevocation.toRevocationState() + /** + * Return the date on which the primary key was revoked, or null if it has not yet been revoked. + * + * @return revocation date or null + */ + val revocationDate: Date? = if (revocationState.isSoftRevocation()) revocationState.date else null + + /** + * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing]. + */ + val secretKey: PGPSecretKey? = when(keys) { + is PGPSecretKeyRing -> keys.secretKey!! + else -> null + } + + /** + * OpenPGP key version. + */ + val version: Int = publicKey.version + + /** + * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. + * The first key in the list being the primary key. + * Note that the list is unmodifiable. + * + * @return list of public keys + */ + val publicKeys: List = keys.publicKeys.asSequence().toList() + + /** + * All secret keys. + * If the key ring is a [PGPPublicKeyRing], then return an empty list. + */ + val secretKeys: List = when(keys) { + is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() + else -> listOf() + } + + /** + * List of valid public subkeys. + */ + val validSubkeys: List = keys.publicKeys.asSequence() + .filter { isKeyValidlyBound(it.keyID) } + .toList() + + /** + * List of valid user-IDs. + */ + val validUserIds: List = userIds.filter { isUserIdBound(it) } + + /** + * List of valid and expired user-IDs. + */ + val validAndExpiredUserIds: List = userIds.filter { + val certification = signatures.userIdCertifications[it] ?: return@filter false + val revocation = signatures.userIdRevocations[it] ?: return@filter true + return@filter !revocation.isHardRevocation() && certification.creationTime > revocation.creationTime + } + + /** + * List of email addresses that can be extracted from the user-IDs. + */ + val emailAddresses: List = userIds.mapNotNull { + PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 -> + if (m1.find()) m1.group(1) + else PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 -> + if(m2.find()) m2.group(1) else null + } + } + } + + /** + * Newest direct-key self-signature on the primary key. + */ + val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature + + /** + * Newest primary-key revocation self-signature. + */ + val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation + + /** + * Public-key encryption-algorithm of the primary key. + */ + val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm) + + /** + * Creation date of the primary key. + */ + val creationDate: Date = publicKey.creationTime!! + + /** + * Latest date at which the key was modified (either by adding a subkey or self-signature). + */ + val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate() + + /** + * True, if the underlying keyring is a [PGPSecretKeyRing]. + */ + val isSecretKey: Boolean = keys is PGPSecretKeyRing + + /** + * True, if there are no encrypted secret keys. + */ + val isFullyDecrypted: Boolean = !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } + + /** + * True, if there are only encrypted secret keys. + */ + val isFullyEncrypted: Boolean = isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } + + /** + * List of public keys, whose secret key counterparts can be used to decrypt messages. + */ + val decryptionSubkeys: List = keys.publicKeys.asSequence().filter { + if (it.keyID != keyId) { + if (signatures.subkeyBindings[it.keyID] == null) { + LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") + return@filter false + } + } + if (!it.isEncryptionKey) { + LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") + return@filter false + } + return@filter true + }.toList() + + /** + * Expiration date of the primary key. + */ + val primaryKeyExpirationDate: Date? + get() { + val directKeyExpirationDate: Date? = latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } + val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() + val primaryUserIdCertification = possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } + val userIdExpirationDate: Date? = primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } + + if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { + throw NoSuchElementException("No direct-key signature and no user-id signature found.") + } + if (directKeyExpirationDate != null && userIdExpirationDate == null) { + return directKeyExpirationDate + } + if (directKeyExpirationDate == null) { + return userIdExpirationDate + } + return if (directKeyExpirationDate < userIdExpirationDate) + directKeyExpirationDate + else userIdExpirationDate + } + + /** + * Return the expiration date of the subkey with the provided fingerprint. + * + * @param fingerprint subkey fingerprint + * @return expiration date or null + */ + fun getSubkeyExpirationDate(fingerprint: OpenPgpFingerprint): Date? { + return getSubkeyExpirationDate(fingerprint.keyId) + } + + /** + * Return the expiration date of the subkey with the provided keyId. + * + * @param keyId subkey keyId + * @return expiration date + */ + fun getSubkeyExpirationDate(keyId: Long): Date? { + if (publicKey.keyID == keyId) return primaryKeyExpirationDate + val subkey = getPublicKey(keyId) ?: throw NoSuchElementException("No subkey with key-ID ${keyId.openPgpKeyId()} found.") + val bindingSig = getCurrentSubkeyBindingSignature(keyId) ?: throw AssertionError("Subkey has no valid binding signature.") + return SignatureUtils.getKeyExpirationDate(subkey.creationTime, bindingSig) + } + + /** + * Return the date after which the key can no longer be used to perform the given use-case, caused by expiration. + * + * @return expiration date for the given use-case + */ + fun getExpirationDateForUse(use: KeyFlag): Date? { + require(use != KeyFlag.SPLIT && use != KeyFlag.SHARED) { + "SPLIT and SHARED are not uses, but properties." + } + + val primaryKeyExpiration = primaryKeyExpirationDate + val keysWithFlag: List = getKeysWithKeyFlag(use) + if (keysWithFlag.isEmpty()) throw NoSuchElementException("No key with the required key flag found.") + + var nonExpiring = false + val latestSubkeyExpiration = keysWithFlag.map { key -> + getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } + }.filterNotNull().maxByOrNull { it } + + if (nonExpiring) return primaryKeyExpiration + return if (primaryKeyExpiration == null) latestSubkeyExpiration + else if (latestSubkeyExpiration == null) + primaryKeyExpiration + else minOf(primaryKeyExpiration, latestSubkeyExpiration) + } + + /** + * Return true, if the given user-ID is hard-revoked. + * + * @return true, if the given user-ID is hard-revoked. + */ + fun isHardRevoked(userId: CharSequence): Boolean { + return signatures.userIdRevocations[userId]?.isHardRevocation() ?: false + } + + /** + * Return a list of all keys which carry the provided key flag in their signature. + * + * @param flag flag + * @return keys with flag + */ + fun getKeysWithKeyFlag(flag: KeyFlag): List = publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } + + /** + * Return a list of all subkeys which can be used to encrypt a message for the given user-ID. + * + * @return encryption subkeys + */ + fun getEncryptionSubkeys(userId: CharSequence?, purpose: EncryptionPurpose): List { + if (userId != null && !isUserIdValid(userId)) { + throw UnboundUserIdException(OpenPgpFingerprint.of(keys), userId.toString(), + getLatestUserIdCertification(userId), getUserIdRevocation(userId)) + } + return getEncryptionSubkeys(purpose) + } + + /** + * Return a list of all subkeys which can be used to encrypt a message, given the purpose. + * + * @return subkeys which can be used for encryption + */ + fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { + primaryKeyExpirationDate?.let { + if (it < referenceDate) { + LOGGER.debug("Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}") + return listOf() + } + } + + return keys.publicKeys.asSequence().filter { + if (!isKeyValidlyBound(it.keyID)) { + LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") + return@filter false + } + + getSubkeyExpirationDate(it.keyID)?.let { exp -> + if (exp < referenceDate) { + LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") + return@filter false + } + } + + if (!it.isEncryptionKey) { + LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") + return@filter false + } + + val keyFlags = getKeyFlagsOf(it.keyID) + when (purpose) { + EncryptionPurpose.COMMUNICATIONS -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) + EncryptionPurpose.STORAGE -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + EncryptionPurpose.ANY -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + } + }.toList() + } + + /** + * List of all subkeys that can be used to sign a message. + */ + val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + + /** + * Whether the key is usable for encryption. + */ + val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) + + /** + * Return, whether the key is usable for encryption, given the purpose. + * + * @return true, if the key can be used to encrypt a message according to the encryption-purpose. + */ + fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean { + return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() + } + + /** + * Whether the key is capable of signing messages. + * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret + * key is unavailable, e.g. because it was moved to a smart-card. + * + * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. + */ + val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() + + /** + * Whether the key is actually usable to sign messages. + */ + val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + + /** + * Return the primary user-ID, even if it is possibly expired. + * + * @return possibly expired primary user-ID + */ + fun getPossiblyExpiredPrimaryUserId(): String? = primaryUserId ?: userIds + .mapNotNull { userId -> + getLatestUserIdCertification(userId)?.let { userId to it } + } + .sortedByDescending { it.second.creationTime } + .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID }?.first + + /** + * Return the most-recently created self-signature on the key. + */ + private fun getMostRecentSignature(): PGPSignature? = + setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature).asSequence() + .plus(signatures.userIdCertifications.values) + .plus(signatures.userIdRevocations.values) + .plus(signatures.subkeyBindings.values) + .plus(signatures.subkeyRevocations.values) + .maxByOrNull { creationDate } + /** + * Return the creation time of the latest added subkey. + * + * @return latest key creation time + */ + fun getLatestKeyCreationDate(): Date = validSubkeys.maxByOrNull { creationDate }?.creationTime + ?: throw AssertionError("Apparently there is no validly bound key in this key ring.") + + /** + * Return the latest certification self-signature for the given user-ID. + * + * @return latest self-certification for the given user-ID. + */ + fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = signatures.userIdCertifications[userId] + + /** + * Return the latest revocation self-signature for the given user-ID + * + * @return latest user-ID revocation for the given user-ID + */ + fun getUserIdRevocation(userId: CharSequence): PGPSignature? = signatures.userIdRevocations[userId] + + /** + * Return the current binding signature for the subkey with the given key-ID. + * + * @return current subkey binding signature + */ + fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = signatures.subkeyBindings[keyId] + + /** + * Return the current revocation signature for the subkey with the given key-ID. + * + * @return current subkey revocation signature + */ + fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = signatures.subkeyRevocations[keyId] + + /** + * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. + * @param keyId key-id + * @return list of key flags + */ + fun getKeyFlagsOf(keyId: Long): List = + if (keyId == publicKey.keyID) { + latestDirectKeySelfSignature?.let { sig -> + SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> + return flags + } + } + + primaryUserId?.let { + SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags -> + return flags + } + } + listOf() + } else { + getCurrentSubkeyBindingSignature(keyId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> + return flags + } + } + listOf() + } + + /** + * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. + * + * @param userId user-id + * @return key flags + */ + fun getKeyFlagsOf(userId: CharSequence): List = + if (!isUserIdValid(userId)) { + listOf() + } else { + getLatestUserIdCertification(userId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf() + } ?: throw AssertionError("While user-id '$userId' was reported as valid, there appears to be no certification for it.") + } + + /** + * Return the public key with the given key id from the provided key ring. + * + * @param keyId key id + * @return public key or null + */ + fun getPublicKey(keyId: Long): PGPPublicKey? = keys.getPublicKey(keyId) + + /** + * Return the secret key with the given key id. + * + * @param keyId key id + * @return secret key or null + */ + fun getSecretKey(keyId: Long): PGPSecretKey? = when(keys) { + is PGPSecretKeyRing -> keys.getSecretKey(keyId) + else -> null + } + + /** + * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a smart-card). + * + * @return availability of the secret key + */ + fun isSecretKeyAvailable(keyId: Long): Boolean { + return getSecretKey(keyId)?.let { + return if (it.s2K == null) true // Unencrypted key + else it.s2K.type !in 100..110 // Secret key on smart-card + } ?: false // Missing secret key + } + + /** + * Return the public key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return public key or null + */ + fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = keys.getPublicKey(fingerprint.keyId) + + /** + * Return the secret key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return secret key or null + */ + fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = when(keys) { + is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) + else -> null + } + + /** + * Return the public key matching the given [SubkeyIdentifier]. + * + * @return public key + * @throws IllegalArgumentException if the identifier's primary key does not match the primary key of the key. + */ + fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? { + require(identifier.primaryKeyId == publicKey.keyID) { + "Mismatching primary key ID." + } + return getPublicKey(identifier.subkeyId) + } + + /** + * Return the secret key matching the given [SubkeyIdentifier]. + * + * @return secret key + * @throws IllegalArgumentException if the identifier's primary key does not match the primary key of the key. + */ + fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = when(keys) { + is PGPSecretKeyRing -> { + require(identifier.primaryKeyId == publicKey.keyID) { + "Mismatching primary key ID." + } + keys.getSecretKey(identifier.subkeyId) + } + else -> null + } + + /** + * Return true if the public key with the given key id is bound to the key ring properly. + * + * @param keyId key id + * @return true if key is bound validly + */ + fun isKeyValidlyBound(keyId: Long): Boolean { + val publicKey = keys.getPublicKey(keyId) ?: return false + + // Primary key -> Check Primary Key Revocation + if (publicKey.keyID == this.publicKey.keyID) { + return if (signatures.primaryKeyRevocation != null && isHardRevocation(signatures.primaryKeyRevocation)) { + false + } else signatures.primaryKeyRevocation == null + } + + // Else Subkey -> Check Subkey Revocation + val binding = signatures.subkeyBindings[keyId] + val revocation = signatures.subkeyRevocations[keyId] + + // No valid binding + if (binding == null || isSignatureExpired(binding)) { + return false + } + + // Revocation + return if (revocation != null) { + if (isHardRevocation(revocation)) { + // Subkey is hard revoked + false + } else { + // Key is soft-revoked, not yet re-bound + (isSignatureExpired(revocation) || !revocation.creationTime.after(binding.creationTime)) + } + } else true + } + + /** + * Return the current primary user-id of the key ring. + *

+ * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, + * this method returns the first user-id on the key, otherwise null. + * + * @return primary user-id or null + */ + private fun findPrimaryUserId(): String? { + if (userIds.isEmpty()) { + return null + } + + return signatures.userIdCertifications.filter { (_, certification) -> + certification.hashedSubPackets.isPrimaryUserID + }.entries.maxByOrNull { (_, certification) -> + certification.creationTime + }?.key ?: signatures.userIdCertifications.keys.firstOrNull() + } + + /** + * Return true, if the primary user-ID, as well as the given user-ID are valid and bound. + */ + fun isUserIdValid(userId: CharSequence) = + if (primaryUserId == null) { + false + } else { + isUserIdBound(primaryUserId) && (if (userId == primaryUserId) true else isUserIdBound(userId)) + } + + /** + * Return true, if the given user-ID is validly bound. + */ + fun isUserIdBound(userId: CharSequence) = + signatures.userIdCertifications[userId]?.let { sig -> + if (sig.isExpired(referenceDate)) { + // certification expired + return false + } + if (sig.hashedSubPackets.isPrimaryUserID) { + SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> + // key expired? + if (expirationDate < referenceDate) return false + } + } + signatures.userIdRevocations[userId]?.let { rev -> + if (rev.isHardRevocation()) { + return false // hard revoked -> invalid + } + sig.creationTime > rev.creationTime// re-certification after soft revocation? + } ?: true // certification, but no revocation + } ?: false // no certification + + /** + * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. + */ + val preferredHashAlgorithms: Set + get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) + + /** + * [HashAlgorithm] preferences of the given user-ID. + */ + fun getPreferredHashAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredHashAlgorithms + } + + /** + * [HashAlgorithm] preferences of the given key. + */ + fun getPreferredHashAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms + } + + /** + * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. + */ + val preferredSymmetricKeyAlgorithms: Set + get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) + + /** + * [SymmetricKeyAlgorithm] preferences of the given user-ID. + */ + fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms + } + + /** + * [SymmetricKeyAlgorithm] preferences of the given key. + */ + fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredSymmetricKeyAlgorithms + } + + /** + * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. + */ + val preferredCompressionAlgorithms: Set + get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) + + /** + * [CompressionAlgorithm] preferences of the given user-ID. + */ + fun getPreferredCompressionAlgorithms(userId: CharSequence): Set { + return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms + } + + /** + * [CompressionAlgorithm] preferences of the given key. + */ + fun getPreferredCompressionAlgorithms(keyId: Long): Set { + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredCompressionAlgorithms + } + + val isUsableForThirdPartyCertification: Boolean = + isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) + + private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor { + if (getPublicKey(keyId) == null) { + throw NoSuchElementException("No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") + } + if (userId != null && !userIds.contains(userId)) { + throw NoSuchElementException("No user-id '$userId' found on this key.") + } + return if (userId != null) { + KeyAccessor.ViaUserId(this, SubkeyIdentifier(keys, keyId), userId) + } else { + KeyAccessor.ViaKeyId(this, SubkeyIdentifier(keys, keyId)) + } + } + + companion object { + + /** + * Evaluate the key for the given signature. + */ + @JvmStatic + fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) = KeyRingInfo(keys, signature.creationTime!!) + + private val PATTERN_EMAIL_FROM_USERID = "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern() + private val PATTERN_EMAIL_EXPLICIT = "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern() + + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) + + } + + private class Signatures( + val keys: PGPKeyRing, + val referenceDate: Date, + val policy: Policy) { + val primaryKeyRevocation: PGPSignature? = SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) + val primaryKeySelfSignature: PGPSignature? = SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) + val userIdRevocations = mutableMapOf() + val userIdCertifications = mutableMapOf() + val subkeyRevocations = mutableMapOf() + val subkeyBindings = mutableMapOf() + + init { + KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId -> + SignaturePicker.pickCurrentUserIdRevocationSignature(keys, userId, policy, referenceDate)?.let { + userIdRevocations[userId] = it + } + SignaturePicker.pickLatestUserIdCertificationSignature(keys, userId, policy, referenceDate)?.let { + userIdCertifications[userId] = it + } + } + keys.publicKeys.asSequence().drop(1).forEach { subkey -> + SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keys, subkey, policy, referenceDate)?.let { + subkeyRevocations[subkey.keyID] = it + } + SignaturePicker.pickLatestSubkeyBindingSignature(keys, subkey, policy, referenceDate)?.let { + subkeyBindings[subkey.keyID] = it + } + } + } + } +} \ No newline at end of file From 19063454cb37b6ab73881c44d403a1213f797dea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 14:35:29 +0200 Subject: [PATCH 149/351] Add PGPSecretKey.unlock() methods --- .../extensions/PGPSecretKeyExtensions.kt | 43 +++++++++++++++++++ .../OpenPgpMessageInputStream.kt | 8 ++-- .../encryption_signing/SigningOptions.kt | 10 ++--- .../key/generation/KeyRingBuilder.kt | 4 +- .../key/protection/fixes/S2KUsageFix.kt | 4 +- .../org/pgpainless/key/util/KeyRingUtils.kt | 6 ++- 6 files changed, 60 insertions(+), 15 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt index cba7bdba..f5d4f522 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -5,7 +5,50 @@ package org.bouncycastle.extensions import org.bouncycastle.bcpg.S2K +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.exception.KeyIntegrityException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.util.Passphrase + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param passphrase passphrase to unlock the secret key with. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + * @throws WrongPassphraseException + */ +@Throws(PGPException::class, KeyIntegrityException::class) +fun PGPSecretKey.unlock(passphrase: Passphrase): PGPPrivateKey = + UnlockSecretKey.unlockSecretKey(this, passphrase) + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param protector protector to unlock the secret key. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + */ +@Throws(PGPException::class, KeyIntegrityException::class) +@JvmOverloads +fun PGPSecretKey.unlock(protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()): PGPPrivateKey = + UnlockSecretKey.unlockSecretKey(this, protector) + +/** + * Unlock the secret key to get its [PGPPrivateKey]. + * + * @param decryptor decryptor to unlock the secret key. + * @throws PGPException if the key cannot be unlocked + * @throws KeyIntegrityException if the public key part was tampered with + */ +@Throws(PGPException::class, KeyIntegrityException::class) +fun PGPSecretKey.unlock(decryptor: PBESecretKeyDecryptor?): PGPPrivateKey = + UnlockSecretKey.unlockSecretKey(this, decryptor) /** * Returns indication that the secret key is encrypted. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 87b0847a..11d5675f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -7,6 +7,7 @@ package org.pgpainless.decryption_verification import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException +import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory @@ -21,7 +22,6 @@ import org.pgpainless.decryption_verification.syntax_check.StackSymbol import org.pgpainless.exception.* import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.SubkeyIdentifier -import org.pgpainless.key.protection.UnlockSecretKey import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.signature.SignatureUtils @@ -302,7 +302,7 @@ class OpenPgpMessageInputStream( continue } - val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + val privateKey = secretKey.unlock(protector) if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true } @@ -325,7 +325,7 @@ class OpenPgpMessageInputStream( continue } - val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + val privateKey = secretKey.unlock(protector) if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true } @@ -351,7 +351,7 @@ class OpenPgpMessageInputStream( LOGGER.debug("Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue - val privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector) + val privateKey = secretKey.unlock(protector) if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { return true } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt index b9208ed5..512e074b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless.Companion.getPolicy import org.pgpainless.PGPainless.Companion.inspectKeyRing @@ -17,7 +18,6 @@ import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint.Companion.of import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey import org.pgpainless.policy.Policy import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback import org.pgpainless.signature.subpackets.SignatureSubpackets @@ -157,7 +157,7 @@ class SigningOptions { for (signingPubKey in signingPubKeys) { val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) @@ -199,7 +199,7 @@ class SigningOptions { val signingSecKey = signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey = unlockSecretKey(signingSecKey, signingKeyProtector) + val signingSubkey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) @@ -292,7 +292,7 @@ class SigningOptions { for (signingPubKey in signingPubKeys) { val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) @@ -332,7 +332,7 @@ class SigningOptions { if (signingPubKey.keyID == keyId) { val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) - val signingSubkey: PGPPrivateKey = unlockSecretKey(signingSecKey, signingKeyProtector) + val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketsCallback) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 2e4a6642..530f1e55 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.generation +import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor @@ -14,7 +15,6 @@ import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.SignatureType import org.pgpainless.implementation.ImplementationFactory -import org.pgpainless.key.protection.UnlockSecretKey import org.pgpainless.policy.Policy import org.pgpainless.provider.ProviderFactory import org.pgpainless.signature.subpackets.SelfSignatureSubpackets @@ -129,7 +129,7 @@ class KeyRingBuilder : KeyRingBuilderInterface { // Attempt to add additional user-ids to the primary public key var primaryPubKey = secretKeys.next().publicKey - val privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.secretKey, secretKeyDecryptor) + val privateKey = secretKeyRing.secretKey.unlock(secretKeyDecryptor) val userIdIterator = userIds.entries.iterator() if (userIdIterator.hasNext()) { userIdIterator.next() // Skip primary userId diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt index aeef0654..c02486ac 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -5,13 +5,13 @@ package org.pgpainless.key.protection.fixes import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.key.protection.UnlockSecretKey.Companion.unlockSecretKey /** * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. @@ -62,7 +62,7 @@ class S2KUsageFix { throw WrongPassphraseException("Missing passphrase for key with ID " + java.lang.Long.toHexString(keyId)) } - val privateKey = unlockSecretKey(key, protector) + val privateKey = key.unlock(protector) // This constructor makes use of USAGE_SHA1 by default val fixedKey = PGPSecretKey( privateKey, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index d3c77ab6..15132f9a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -8,12 +8,12 @@ import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.extensions.certificate +import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.util.Strings import org.pgpainless.exception.MissingPassphraseException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.key.protection.UnlockSecretKey import org.pgpainless.key.protection.fixes.S2KUsageFix import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -164,8 +164,10 @@ class KeyRingUtils { * @throws PGPException if something goes wrong (e.g. wrong passphrase) */ @JvmStatic + @Deprecated("Deprecated in favor of secretKey.unlock(protector)", + ReplaceWith("secretKey.unlock(protector)")) fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { - return UnlockSecretKey.unlockSecretKey(secretKey, protector) + return secretKey.unlock(protector) } /** From a0b01f121a9c0d54aeaf6fc4e43d292d99dda496 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 14:36:42 +0200 Subject: [PATCH 150/351] Remove KeyRingUtils.unlockSecretKey() --- .../org/pgpainless/key/util/KeyRingUtils.kt | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 15132f9a..22e78410 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -8,7 +8,6 @@ import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.extensions.certificate -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.util.Strings import org.pgpainless.exception.MissingPassphraseException @@ -154,22 +153,6 @@ class KeyRingUtils { .toList()) } - /** - * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. - * - * @param secretKey secret key - * @param protector protector to unlock the secret key - * @return private key - * - * @throws PGPException if something goes wrong (e.g. wrong passphrase) - */ - @JvmStatic - @Deprecated("Deprecated in favor of secretKey.unlock(protector)", - ReplaceWith("secretKey.unlock(protector)")) - fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { - return secretKey.unlock(protector) - } - /** * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. * From de3ea580e3dd9538072fb28db14b11eb4fb66ca7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 15:01:26 +0200 Subject: [PATCH 151/351] Add extension methods to PGPKeyRing, PGPSecretKeyRing and PGPSignature --- .../extensions/PGPKeyRingExtensions.kt | 48 ++++++++++++++++- .../extensions/PGPSecretKeyRingExtensions.kt | 51 +++++++++++++++++-- .../extensions/PGPSignatureExtensions.kt | 4 ++ .../org/pgpainless/key/OpenPgpFingerprint.kt | 2 + 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index 43669bd1..2a3c85f5 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -5,6 +5,10 @@ package org.bouncycastle.extensions import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier /** @@ -12,4 +16,46 @@ import org.pgpainless.key.SubkeyIdentifier */ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = this.publicKey.keyID == subkeyIdentifier.primaryKeyId && - this.getPublicKey(subkeyIdentifier.subkeyId) != null \ No newline at end of file + this.getPublicKey(subkeyIdentifier.subkeyId) != null + +/** + * Return true, if the [PGPKeyRing] contains a public key with the given key-ID. + * + * @param keyId keyId + * @return true if key with the given key-ID is present, false otherwise + */ +fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = + this.getPublicKey(keyId) != null + +/** + * Return true, if the [PGPKeyRing] contains a public key with the given fingerprint. + * + * @param fingerprint fingerprint + * @return true if key with the given fingerprint is present, false otherwise + */ +fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = + this.getPublicKey(fingerprint) != null + +/** + * Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present. + * + * @param fingerprint fingerprint + * @return public key + */ +fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = + this.getPublicKey(fingerprint.bytes) + +/** + * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. + * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to + * identify the [PGPPublicKey] via its key-ID. + */ +fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = + signature.getFingerprint()?.let { this.getPublicKey(it) } ?: + this.getPublicKey(signature.keyID) + +/** + * Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. + */ +fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = + this.getPublicKey(onePassSignature.keyID) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index 3a9c2918..5be7e128 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -4,8 +4,53 @@ package org.bouncycastle.extensions -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.* +import org.pgpainless.key.OpenPgpFingerprint +/** + * OpenPGP certificate containing the public keys of this OpenPGP key. + */ val PGPSecretKeyRing.certificate: PGPPublicKeyRing - get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) \ No newline at end of file + get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) + +/** + * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given key-ID. + * + * @param keyId keyId of the secret key + * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise + */ +fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = + this.getSecretKey(keyId) != null + +/** + * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint. + * + * @param fingerprint fingerprint + * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise + */ +fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = + this.getSecretKey(fingerprint) != null + +/** + * Return the [PGPSecretKey] with the given [OpenPgpFingerprint]. + * + * @param fingerprint fingerprint of the secret key + * @return the secret key or null + */ +fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = + this.getSecretKey(fingerprint.bytes) + +/** + * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. + * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to + * identify the [PGPSecretKey] via its key-ID. + */ +fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = + signature.getFingerprint()?.let { this.getSecretKey(it) } ?: + this.getSecretKey(signature.keyID) + +/** + * Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. + */ +fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = + this.getSecretKey(onePassSignature.keyID) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 16f61304..80200aa6 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -8,6 +8,7 @@ import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.RevocationState import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.signature.SignatureUtils +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import java.util.* /** @@ -51,3 +52,6 @@ fun PGPSignature?.toRevocationState() = else if (isHardRevocation()) RevocationState.hardRevoked() else RevocationState.softRevoked(creationTime) + +fun PGPSignature.getFingerprint(): OpenPgpFingerprint? = + SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt index c5a2a24f..fdcad306 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -16,6 +16,7 @@ import java.nio.charset.Charset */ abstract class OpenPgpFingerprint : CharSequence, Comparable { val fingerprint: String + val bytes: ByteArray /** * Return the version of the fingerprint. @@ -41,6 +42,7 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable throw IllegalArgumentException("Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.") } this.fingerprint = prep + this.bytes = Hex.decode(prep) } constructor(bytes: ByteArray): this(Hex.toHexString(bytes)) From 76cf6173e87a75bdfd5490f9c82b2f3969faf6a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 15:03:02 +0200 Subject: [PATCH 152/351] Add test for OpenPgpFingerprint.getBytes() --- .../test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java index d61aeedd..43a3dfba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/OpenPgpV4FingerprintTest.java @@ -4,6 +4,7 @@ package org.pgpainless.key; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -112,6 +113,7 @@ public class OpenPgpV4FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); } @Test @@ -122,6 +124,7 @@ public class OpenPgpV4FingerprintTest { OpenPgpFingerprint fingerprint = OpenPgpFingerprint.parseFromBinary(binary); assertTrue(fingerprint instanceof OpenPgpV4Fingerprint); assertEquals(hex, fingerprint.toString()); + assertArrayEquals(binary, fingerprint.getBytes()); } @Test From bb796143ff4bec8344fb123f8947c1e67458b2bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 15:43:55 +0200 Subject: [PATCH 153/351] Improve public/secret key selection --- .../extensions/PGPSecretKeyRingExtensions.kt | 8 +- .../ConsumerOptions.kt | 6 + .../OpenPgpMessageInputStream.kt | 114 +++++++++++------- 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index 5be7e128..a4a1621e 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -53,4 +53,10 @@ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = * Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = - this.getSecretKey(onePassSignature.keyID) \ No newline at end of file + this.getSecretKey(onePassSignature.keyID) + +fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? = + when(pkesk.version) { + 3 -> this.getSecretKey(pkesk.keyID) + else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") + } \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index 5bbe098e..dbff0551 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification +import org.bouncycastle.extensions.getPublicKeyFor import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy @@ -385,6 +386,11 @@ class ConsumerOptions { fun getCertificate(keyId: Long): PGPPublicKeyRing? { return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } } + + fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? = + explicitCertificates.firstOrNull { + it.getPublicKeyFor(signature) != null + } } companion object { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 11d5675f..7e059b48 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -7,6 +7,8 @@ package org.pgpainless.decryption_verification import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException +import org.bouncycastle.extensions.getPublicKeyFor +import org.bouncycastle.extensions.getSecretKeyFor import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory @@ -280,31 +282,28 @@ class OpenPgpMessageInputStream( val postponedDueToMissingPassphrase = mutableListOf>() // try (known) secret keys - for (pkesk in esks.pkesks) { - val keyId = pkesk.keyID - LOGGER.debug("Encountered PKESK for recipient ${keyId.openPgpKeyId()}") - val decryptionKeys = getDecryptionKey(keyId) - if (decryptionKeys == null) { - LOGGER.debug("Skipping PKESK because no matching key ${keyId.openPgpKeyId()} was provided.") - continue - } - val secretKey = decryptionKeys.getSecretKey(keyId) - val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) - if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { - continue - } + esks.pkesks.forEach { pkesk -> + LOGGER.debug("Encountered PKESK for recipient ${pkesk.keyID.openPgpKeyId()}") + val decryptionKeyCandidates = getDecryptionKeys(pkesk) + for (decryptionKeys in decryptionKeyCandidates) { + val secretKey = decryptionKeys.getSecretKeyFor(pkesk)!! + val decryptionKeyId = SubkeyIdentifier(decryptionKeys, secretKey.keyID) + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue + } - LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") - val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue - if (!protector.hasPassphraseFor(keyId)) { - LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") - postponedDueToMissingPassphrase.add(secretKey to pkesk) - continue - } + LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue + if (!protector.hasPassphraseFor(secretKey.keyID)) { + LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + postponedDueToMissingPassphrase.add(secretKey to pkesk) + continue + } - val privateKey = secretKey.unlock(protector) - if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { - return true + val privateKey = secretKey.unlock(protector) + if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { + return true + } } } @@ -343,7 +342,7 @@ class OpenPgpMessageInputStream( } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { val keyId = secretKey.keyID - val decryptionKeys = getDecryptionKey(keyId)!! + val decryptionKeys = getDecryptionKey(pkesk)!! val decryptionKeyId = SubkeyIdentifier(decryptionKeys, keyId) if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { continue @@ -541,6 +540,25 @@ class OpenPgpMessageInputStream( }) } + private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { + it.getSecretKeyFor(pkesk) != null && PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + when (pkesk.version) { + 3 -> pkesk.keyID == subkey.keyID + else -> throw NotImplementedError("Version 6 PKESK not yet supported.") + } + } + } + + private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = + options.getDecryptionKeys().filter { + it.getSecretKeyFor(pkesk) != null && PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + when (pkesk.version) { + 3 -> pkesk.keyID == subkey.keyID + else -> throw NotImplementedError("Version 6 PKESK not yet supported.") + } + } + } + private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List> { val algorithm = pkesk.algorithm val candidates = mutableListOf>() @@ -638,22 +656,24 @@ class OpenPgpMessageInputStream( } fun initializeSignature(signature: PGPSignature): SignatureCheck? { - val keyId = SignatureUtils.determineIssuerKeyId(signature) - val certificate = findCertificate(keyId) ?: return null - - val verifierKey = SubkeyIdentifier(certificate, keyId) - initialize(signature, certificate, keyId) + val certificate = findCertificate(signature) ?: return null + val publicKey = certificate.getPublicKeyFor(signature) ?: return null + val verifierKey = SubkeyIdentifier(certificate, publicKey.keyID) + initialize(signature, publicKey) return SignatureCheck(signature, certificate, verifierKey) } fun addOnePassSignature(signature: PGPOnePassSignature) { - val certificate = findCertificate(signature.keyID) + val certificate = findCertificate(signature) if (certificate != null) { - val ops = OnePassSignatureCheck(signature, certificate) - initialize(signature, certificate) - onePassSignatures.add(ops) - literalOPS.add(ops) + val publicKey = certificate.getPublicKeyFor(signature) + if (publicKey != null) { + val ops = OnePassSignatureCheck(signature, certificate) + initialize(signature, publicKey) + onePassSignatures.add(ops) + literalOPS.add(ops) + } } if (signature.isContaining) { enterNesting() @@ -710,14 +730,26 @@ class OpenPgpMessageInputStream( opsUpdateStack.removeFirst() } - fun findCertificate(keyId: Long): PGPPublicKeyRing? { - val cert = options.getCertificateSource().getCertificate(keyId) + private fun findCertificate(signature: PGPSignature): PGPPublicKeyRing? { + val cert = options.getCertificateSource().getCertificate(signature) if (cert != null) { return cert } if (options.getMissingCertificateCallback() != null) { - return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(keyId) + return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(signature.keyID) + } + return null // TODO: Missing cert for sig + } + + private fun findCertificate(signature: PGPOnePassSignature): PGPPublicKeyRing? { + val cert = options.getCertificateSource().getCertificate(signature.keyID) + if (cert != null) { + return cert + } + + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(signature.keyID) } return null // TODO: Missing cert for sig } @@ -831,22 +863,22 @@ class OpenPgpMessageInputStream( companion object { @JvmStatic - private fun initialize(signature: PGPSignature, certificate: PGPPublicKeyRing, keyId: Long) { + private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { val verifierProvider = ImplementationFactory.getInstance() .pgpContentVerifierBuilderProvider try { - signature.init(verifierProvider, certificate.getPublicKey(keyId)) + signature.init(verifierProvider, publicKey) } catch (e : PGPException) { throw RuntimeException(e) } } @JvmStatic - private fun initialize(ops: PGPOnePassSignature, certificate: PGPPublicKeyRing) { + private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { val verifierProvider = ImplementationFactory.getInstance() .pgpContentVerifierBuilderProvider try { - ops.init(verifierProvider, certificate.getPublicKey(ops.keyID)) + ops.init(verifierProvider, publicKey) } catch (e : PGPException) { throw RuntimeException(e) } From 68af0a4f0e984aff2f5218d29f667457e00f6706 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 21:55:39 +0200 Subject: [PATCH 154/351] Introduce more extension methods --- .../src/main/kotlin/openpgp/DateExtensions.kt | 23 ++++++ .../extensions/PGPKeyRingExtensions.kt | 16 +++- .../extensions/PGPPublicKeyExtensions.kt | 13 +++ .../extensions/PGPSecretKeyExtensions.kt | 15 ++++ .../extensions/PGPSecretKeyRingExtensions.kt | 19 ++++- .../extensions/PGPSignatureExtensions.kt | 62 +++++++++++---- .../org/pgpainless/key/info/KeyRingInfo.kt | 6 +- .../org/pgpainless/key/util/KeyRingUtils.kt | 6 +- .../pgpainless/signature/SignatureUtils.kt | 79 +++++++------------ 9 files changed, 168 insertions(+), 71 deletions(-) create mode 100644 pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt new file mode 100644 index 00000000..db98fb44 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package openpgp + +import java.util.* + + /** + * Return a new date which represents this date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification + * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ + fun Date.plusSeconds(seconds: Long): Date? { + require(Long.MAX_VALUE - time > seconds) { "Adding $seconds seconds to this date would cause time to overflow." } + return if (seconds == 0L) null + else Date(this.time + 1000 * seconds) + } diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index 2a3c85f5..7ccddb42 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -8,6 +8,7 @@ import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier @@ -51,11 +52,22 @@ fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = * identify the [PGPPublicKey] via its key-ID. */ fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = - signature.getFingerprint()?.let { this.getPublicKey(it) } ?: + signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID) /** * Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = - this.getPublicKey(onePassSignature.keyID) \ No newline at end of file + this.getPublicKey(onePassSignature.keyID) + +/** + * Return the [OpenPgpFingerprint] of this OpenPGP key. + */ +val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) + +/** + * Return this OpenPGP key as an ASCII armored String. + */ +fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt index a1ed72fe..ad51c6f4 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -11,6 +11,7 @@ import org.bouncycastle.bcpg.EdDSAPublicBCPGKey import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.type.eddsa.EdDSACurve /** @@ -35,3 +36,15 @@ fun PGPPublicKey.getCurveName(): String { .let { it to ECUtil.getCurveName(it) } .let { if (it.second != null) return it.second else throw IllegalArgumentException("Unknown curve: ${it.first}") } } + +/** + * Return the [PublicKeyAlgorithm] of this key. + */ +val PGPPublicKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(algorithm) + +/** + * Return the [OpenPgpFingerprint] of this key. + */ +val PGPPublicKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt index f5d4f522..3d759d1a 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -7,10 +7,13 @@ package org.bouncycastle.extensions import org.bouncycastle.bcpg.S2K import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.UnlockSecretKey import org.pgpainless.util.Passphrase @@ -70,3 +73,15 @@ fun PGPSecretKey?.isDecrypted(): Boolean = (this == null) || (s2KUsage == 0) * @return true if secret key has S2K of type GNU_DUMMY_S2K, false otherwise. */ fun PGPSecretKey?.hasDummyS2K(): Boolean = (this != null) && (s2K?.type == S2K.GNU_DUMMY_S2K) + +/** + * Return the [PublicKeyAlgorithm] of this key. + */ +val PGPSecretKey.publicKeyAlgorithm: PublicKeyAlgorithm + get() = publicKey.publicKeyAlgorithm + +/** + * Return the [OpenPgpFingerprint] of this key. + */ +val PGPSecretKey.openPgpFingerprint: OpenPgpFingerprint + get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index a4a1621e..d0529d51 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -4,6 +4,7 @@ package org.bouncycastle.extensions +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.* import org.pgpainless.key.OpenPgpFingerprint @@ -40,13 +41,29 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = this.getSecretKey(fingerprint.bytes) +/** + * Return the [PGPSecretKey] with the given key-ID. + * + * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given key-ID + */ +fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = + getSecretKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +/** + * Return the [PGPSecretKey] with the given fingerprint. + * + * @throws NoSuchElementException of the OpenPGP key doesn't contain a secret key with the given fingerprint + */ +fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = + getSecretKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + /** * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to * identify the [PGPSecretKey] via its key-ID. */ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = - signature.getFingerprint()?.let { this.getSecretKey(it) } ?: + signature.fingerprint?.let { this.getSecretKey(it) } ?: this.getSecretKey(signature.keyID) /** diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 80200aa6..a27e68e0 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -4,10 +4,12 @@ package org.bouncycastle.extensions +import openpgp.plusSeconds import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.RevocationState +import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.OpenPgpFingerprint -import org.pgpainless.signature.SignatureUtils +import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import java.util.* @@ -16,42 +18,74 @@ import java.util.* * such a subpacket. */ fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = - SignatureUtils.getKeyExpirationDate(keyCreationDate, this) + SignatureSubpacketsUtil.getKeyExpirationTime(this) + ?.let { keyCreationDate.plusSeconds(it.time) } /** * Return the value of the signature ExpirationTime subpacket, or null, if the signature * does not carry such a subpacket. */ -fun PGPSignature.getSignatureExpirationDate(): Date? = - SignatureUtils.getSignatureExpirationDate(this) +val PGPSignature.signatureExpirationDate: Date? + get() = SignatureSubpacketsUtil.getSignatureExpirationTime(this) + ?.let { this.creationTime.plusSeconds(it.time) } /** * Return true, if the signature is expired at the given reference time. */ fun PGPSignature.isExpired(referenceTime: Date = Date()) = - SignatureUtils.isSignatureExpired(this, referenceTime) + signatureExpirationDate?.let { referenceTime >= it } ?: false /** * Return the key-ID of the issuer, determined by examining the IssuerKeyId and IssuerFingerprint * subpackets of the signature. */ -fun PGPSignature.getIssuerKeyId() = SignatureUtils.determineIssuerKeyId(this) +val PGPSignature.issuerKeyId: Long + get() = when (version) { + 2, 3 -> keyID + else -> { + SignatureSubpacketsUtil.getIssuerKeyIdAsLong(this) + ?.let { if (it != 0L) it else null } + ?: fingerprint?.keyId + ?: 0L + } + } /** - * Return true, if the signature was likely issued by the key with the given fingerprint. + * Return true, if the signature was likely issued by a key with the given fingerprint. */ -fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint) = SignatureUtils.wasIssuedBy(fingerprint, this) +fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = + this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) + +/** + * Return true, if the signature was likely issued by a key with the given fingerprint. + * @param fingerprint fingerprint bytes + */ +@Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.") +fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = + try { + wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) + } catch (e : IllegalArgumentException) { + // Unknown fingerprint length / format + false + } /** * Return true, if this signature is a hard revocation. */ -fun PGPSignature.isHardRevocation() = SignatureUtils.isHardRevocation(this) +val PGPSignature.isHardRevocation + get() = when (SignatureType.requireFromCode(signatureType)) { + SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION, SignatureType.CERTIFICATION_REVOCATION -> { + SignatureSubpacketsUtil.getRevocationReason(this) + ?.let { Reason.isHardRevocation(it.revocationReason) } + ?: true // no reason -> hard revocation + } + else -> false // Not a revocation + } fun PGPSignature?.toRevocationState() = if (this == null) RevocationState.notRevoked() - else - if (isHardRevocation()) RevocationState.hardRevoked() - else RevocationState.softRevoked(creationTime) + else if (isHardRevocation) RevocationState.hardRevoked() + else RevocationState.softRevoked(creationTime) -fun PGPSignature.getFingerprint(): OpenPgpFingerprint? = - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) +val PGPSignature.fingerprint: OpenPgpFingerprint? + get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 79cc8683..df6023c4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -121,7 +121,7 @@ class KeyRingInfo( val validAndExpiredUserIds: List = userIds.filter { val certification = signatures.userIdCertifications[it] ?: return@filter false val revocation = signatures.userIdRevocations[it] ?: return@filter true - return@filter !revocation.isHardRevocation() && certification.creationTime > revocation.creationTime + return@filter !revocation.isHardRevocation && certification.creationTime > revocation.creationTime } /** @@ -272,7 +272,7 @@ class KeyRingInfo( * @return true, if the given user-ID is hard-revoked. */ fun isHardRevoked(userId: CharSequence): Boolean { - return signatures.userIdRevocations[userId]?.isHardRevocation() ?: false + return signatures.userIdRevocations[userId]?.isHardRevocation ?: false } /** @@ -632,7 +632,7 @@ class KeyRingInfo( } } signatures.userIdRevocations[userId]?.let { rev -> - if (rev.isHardRevocation()) { + if (rev.isHardRevocation) { return false // hard revoked -> invalid } sig.creationTime > rev.creationTime// re-certification after soft revocation? diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 22e78410..79215408 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -8,6 +8,7 @@ import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket import org.bouncycastle.extensions.certificate +import org.bouncycastle.extensions.requireSecretKey import org.bouncycastle.openpgp.* import org.bouncycastle.util.Strings import org.pgpainless.exception.MissingPassphraseException @@ -34,9 +35,10 @@ class KeyRingUtils { * @return primary secret key */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSecretKeyRing extension function.", + ReplaceWith("secretKeys.requireSecretKey(keyId)")) fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { - return getPrimarySecretKeyFrom(secretKeys) - ?: throw NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key.") + return secretKeys.requireSecretKey(secretKeys.publicKey.keyID) } /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 4ca30e86..0edb6d71 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -4,7 +4,9 @@ package org.pgpainless.signature +import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.Streams @@ -17,6 +19,7 @@ import org.pgpainless.util.ArmorUtils import java.io.IOException import java.io.InputStream import java.util.* +import kotlin.math.sign const val MAX_ITERATIONS = 10000 @@ -32,10 +35,10 @@ class SignatureUtils { * @return key expiration date as given by the signature */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.getKeyExpirationDate(keyCreationDate)")) fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { - val expirationPacket: KeyExpirationTime = SignatureSubpacketsUtil.getKeyExpirationTime(signature) ?: return null - val expiresInSeconds = expirationPacket.time - return datePlusSeconds(keyCreationDate, expiresInSeconds) + return signature.getKeyExpirationDate(keyCreationDate) } /** @@ -46,12 +49,9 @@ class SignatureUtils { * @return expiration date of the signature, or null if it does not expire. */ @JvmStatic - fun getSignatureExpirationDate(signature: PGPSignature): Date? { - val expirationTime = SignatureSubpacketsUtil.getSignatureExpirationTime(signature) ?: return null - - val expiresInSeconds = expirationTime.time - return datePlusSeconds(signature.creationTime, expiresInSeconds) - } + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.signatureExpirationDate")) + fun getSignatureExpirationDate(signature: PGPSignature): Date? = signature.signatureExpirationDate /** * Return a new date which represents the given date plus the given amount of seconds added. @@ -64,11 +64,10 @@ class SignatureUtils { * @return date plus seconds or null if seconds is '0' */ @JvmStatic + @Deprecated("Deprecated in favor of Date extension method.", + ReplaceWith("date.plusSeconds(seconds)")) fun datePlusSeconds(date: Date, seconds: Long): Date? { - if (seconds == 0L) { - return null - } - return Date(date.time + 1000 * seconds) + return date.plusSeconds(seconds) } /** @@ -79,8 +78,10 @@ class SignatureUtils { * @return true if expired, false otherwise */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired()")) fun isSignatureExpired(signature: PGPSignature): Boolean { - return isSignatureExpired(signature, Date()) + return signature.isExpired() } /** @@ -92,9 +93,10 @@ class SignatureUtils { * @return true if sig is expired at reference date, false otherwise */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired(referenceTime)")) fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { - val expirationDate = getSignatureExpirationDate(signature) ?: return false - return referenceTime >= expirationDate + return signature.isExpired(referenceTime) } /** @@ -106,15 +108,10 @@ class SignatureUtils { * @return true if signature is a hard revocation */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension function.", + ReplaceWith("signature.isHardRevocation()")) fun isHardRevocation(signature: PGPSignature): Boolean { - val type = SignatureType.requireFromCode(signature.signatureType) - if (type != SignatureType.KEY_REVOCATION && type != SignatureType.SUBKEY_REVOCATION && type != SignatureType.CERTIFICATION_REVOCATION) { - // Not a revocation - return false - } - - val reason = SignatureSubpacketsUtil.getRevocationReason(signature) ?: return true // no reason -> hard revocation - return Reason.isHardRevocation(reason.revocationReason) + return signature.isHardRevocation } @JvmStatic @@ -181,22 +178,10 @@ class SignatureUtils { * @return signatures issuing key id */ @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.issuerKeyId")) fun determineIssuerKeyId(signature: PGPSignature): Long { - if (signature.version == 3) { - // V3 sigs do not contain subpackets - return signature.keyID - } - - val issuerKeyId = SignatureSubpacketsUtil.getIssuerKeyId(signature) - val issuerFingerprint = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) - - if (issuerKeyId != null && issuerKeyId.keyID != 0L) { - return issuerKeyId.keyID - } - if (issuerKeyId == null && issuerFingerprint != null) { - return issuerFingerprint.keyId - } - return 0 + return signature.issuerKeyId } /** @@ -211,21 +196,17 @@ class SignatureUtils { } @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method", + ReplaceWith("signature.wasIssuedBy(fingerprint)")) fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { - return try { - val pgpFingerprint = OpenPgpFingerprint.parseFromBinary(fingerprint) - wasIssuedBy(pgpFingerprint, signature) - } catch (e : IllegalArgumentException) { - // Unknown fingerprint length - false - } + return signature.wasIssuedBy(fingerprint) } @JvmStatic + @Deprecated("Deprecated in favor of PGPSignature extension method", + ReplaceWith("signature.wasIssuedBy(fingerprint)")) fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { - val issuerFp = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature) - ?: return fingerprint.keyId == signature.keyID - return fingerprint == issuerFp + return signature.wasIssuedBy(fingerprint) } /** From 4719d6ccea30ada6036f1ad19332c78af1576d9e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 12 Sep 2023 22:07:17 +0200 Subject: [PATCH 155/351] Migrate further to extension methods --- .../OpenPgpMessageInputStream.kt | 10 ++++---- .../org/pgpainless/key/info/KeyRingInfo.kt | 15 ++++-------- .../pgpainless/signature/SignatureUtils.kt | 23 ++++++++----------- .../subpackets/SignatureSubpacketsUtil.kt | 5 ++-- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 7e059b48..b823fdaf 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -9,6 +9,7 @@ import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException import org.bouncycastle.extensions.getPublicKeyFor import org.bouncycastle.extensions.getSecretKeyFor +import org.bouncycastle.extensions.issuerKeyId import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory @@ -26,7 +27,6 @@ import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy -import org.pgpainless.signature.SignatureUtils import org.pgpainless.signature.consumer.CertificateValidator import org.pgpainless.signature.consumer.OnePassSignatureCheck import org.pgpainless.signature.consumer.SignatureCheck @@ -197,7 +197,7 @@ class OpenPgpMessageInputStream( return } - val keyId = SignatureUtils.determineIssuerKeyId(signature) + val keyId = signature.issuerKeyId if (isSigForOps) { LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with @@ -632,7 +632,7 @@ class OpenPgpMessageInputStream( fun addDetachedSignature(signature: PGPSignature) { val check = initializeSignature(signature) - val keyId = SignatureUtils.determineIssuerKeyId(signature) + val keyId = signature.issuerKeyId if (check != null) { detachedSignatures.add(check) } else { @@ -644,7 +644,7 @@ class OpenPgpMessageInputStream( fun addPrependedSignature(signature: PGPSignature) { val check = initializeSignature(signature) - val keyId = SignatureUtils.determineIssuerKeyId(signature) + val keyId = signature.issuerKeyId if (check != null) { prependedSignatures.add(check) } else { @@ -682,7 +682,7 @@ class OpenPgpMessageInputStream( fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer, policy: Policy) { var found = false - val keyId = SignatureUtils.determineIssuerKeyId(signature) + val keyId = signature.issuerKeyId for ((i, check) in onePassSignatures.withIndex().reversed()) { if (check.onePassSignature.keyID != keyId) { continue diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index df6023c4..6ccdc81d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -14,17 +14,12 @@ import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy -import org.pgpainless.signature.SignatureUtils -import org.pgpainless.signature.SignatureUtils.Companion.isHardRevocation -import org.pgpainless.signature.SignatureUtils.Companion.isSignatureExpired import org.pgpainless.signature.consumer.SignaturePicker import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate import org.pgpainless.util.DateUtil import org.slf4j.LoggerFactory -import java.security.Key import java.util.* -import kotlin.NoSuchElementException class KeyRingInfo( val keys: PGPKeyRing, @@ -237,7 +232,7 @@ class KeyRingInfo( if (publicKey.keyID == keyId) return primaryKeyExpirationDate val subkey = getPublicKey(keyId) ?: throw NoSuchElementException("No subkey with key-ID ${keyId.openPgpKeyId()} found.") val bindingSig = getCurrentSubkeyBindingSignature(keyId) ?: throw AssertionError("Subkey has no valid binding signature.") - return SignatureUtils.getKeyExpirationDate(subkey.creationTime, bindingSig) + return bindingSig.getKeyExpirationDate(subkey.creationTime) } /** @@ -560,7 +555,7 @@ class KeyRingInfo( // Primary key -> Check Primary Key Revocation if (publicKey.keyID == this.publicKey.keyID) { - return if (signatures.primaryKeyRevocation != null && isHardRevocation(signatures.primaryKeyRevocation)) { + return if (signatures.primaryKeyRevocation != null && signatures.primaryKeyRevocation.isHardRevocation) { false } else signatures.primaryKeyRevocation == null } @@ -570,18 +565,18 @@ class KeyRingInfo( val revocation = signatures.subkeyRevocations[keyId] // No valid binding - if (binding == null || isSignatureExpired(binding)) { + if (binding == null || binding.isExpired(referenceDate)) { return false } // Revocation return if (revocation != null) { - if (isHardRevocation(revocation)) { + if (revocation.isHardRevocation) { // Subkey is hard revoked false } else { // Key is soft-revoked, not yet re-bound - (isSignatureExpired(revocation) || !revocation.creationTime.after(binding.creationTime)) + (revocation.isExpired(referenceDate) || !revocation.creationTime.after(binding.creationTime)) } } else true } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 0edb6d71..492c4fe4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -10,16 +10,13 @@ import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.Streams -import org.pgpainless.algorithm.SignatureType import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.util.ArmorUtils import java.io.IOException import java.io.InputStream import java.util.* -import kotlin.math.sign const val MAX_ITERATIONS = 10000 @@ -36,21 +33,21 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.getKeyExpirationDate(keyCreationDate)")) + ReplaceWith("signature.getKeyExpirationDate(keyCreationDate)", "org.bouncycastle.extensions.getKeyExpirationDate")) fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { return signature.getKeyExpirationDate(keyCreationDate) } /** * Return the expiration date of the signature. - * If the signature has no expiration date, [datePlusSeconds] will return null. + * If the signature has no expiration date, this will return null. * * @param signature signature * @return expiration date of the signature, or null if it does not expire. */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.signatureExpirationDate")) + ReplaceWith("signature.signatureExpirationDate", "org.bouncycastle.extensions.signatureExpirationDate")) fun getSignatureExpirationDate(signature: PGPSignature): Date? = signature.signatureExpirationDate /** @@ -65,7 +62,7 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of Date extension method.", - ReplaceWith("date.plusSeconds(seconds)")) + ReplaceWith("date.plusSeconds(seconds)", "openpgp.plusSeconds")) fun datePlusSeconds(date: Date, seconds: Long): Date? { return date.plusSeconds(seconds) } @@ -79,7 +76,7 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.isExpired()")) + ReplaceWith("signature.isExpired()", "org.bouncycastle.extensions.isExpired")) fun isSignatureExpired(signature: PGPSignature): Boolean { return signature.isExpired() } @@ -94,7 +91,7 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.isExpired(referenceTime)")) + ReplaceWith("signature.isExpired(referenceTime)", "org.bouncycastle.extensions.isExpired")) fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { return signature.isExpired(referenceTime) } @@ -109,7 +106,7 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension function.", - ReplaceWith("signature.isHardRevocation()")) + ReplaceWith("signature.isHardRevocation", "org.bouncycastle.extensions.isHardRevocation")) fun isHardRevocation(signature: PGPSignature): Boolean { return signature.isHardRevocation } @@ -179,7 +176,7 @@ class SignatureUtils { */ @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.issuerKeyId")) + ReplaceWith("signature.issuerKeyId", "org.bouncycastle.extensions.issuerKeyId")) fun determineIssuerKeyId(signature: PGPSignature): Long { return signature.issuerKeyId } @@ -197,14 +194,14 @@ class SignatureUtils { @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method", - ReplaceWith("signature.wasIssuedBy(fingerprint)")) + ReplaceWith("signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { return signature.wasIssuedBy(fingerprint) } @JvmStatic @Deprecated("Deprecated in favor of PGPSignature extension method", - ReplaceWith("signature.wasIssuedBy(fingerprint)")) + ReplaceWith("signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { return signature.wasIssuedBy(fingerprint) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index 172ef32f..2cf79d72 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -5,6 +5,7 @@ package org.pgpainless.signature.subpackets import openpgp.openPgpKeyId +import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature @@ -116,7 +117,7 @@ class SignatureSubpacketsUtil { @JvmStatic fun getSignatureExpirationTimeAsDate(signature: PGPSignature): Date? = getSignatureExpirationTime(signature)?.let { - SignatureUtils.datePlusSeconds(signature.creationTime, it.time) + signature.creationTime.plusSeconds(it.time) } /** @@ -146,7 +147,7 @@ class SignatureSubpacketsUtil { "Provided key (${signingKey.keyID.openPgpKeyId()}) did not create the signature (${signature.keyID.openPgpKeyId()})" }.run { getKeyExpirationTime(signature)?.let { - SignatureUtils.datePlusSeconds(signingKey.creationTime, it.time) + signingKey.creationTime.plusSeconds(it.time) } } From ec8ae3eff01759b354b81dd02105594640bcab38 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 13 Sep 2023 15:05:58 +0200 Subject: [PATCH 156/351] Kotlin conversion: SecretKeyRingEditor --- .../key/modification/package-info.java | 8 - .../secretkeyring/SecretKeyRingEditor.java | 810 ------------------ .../SecretKeyRingEditorInterface.java | 652 -------------- .../secretkeyring/package-info.java | 8 - .../extensions/PGPKeyRingExtensions.kt | 7 + .../org/pgpainless/key/info/KeyRingInfo.kt | 86 +- .../secretkeyring/SecretKeyRingEditor.kt | 493 +++++++++++ .../SecretKeyRingEditorInterface.kt | 560 ++++++++++++ .../pgpainless/key/info/KeyRingInfoTest.java | 2 +- .../key/modification/AddUserIdTest.java | 7 +- .../key/protection/fixes/S2KUsageFixTest.java | 2 +- 11 files changed, 1110 insertions(+), 1525 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java deleted file mode 100644 index 9fa73e88..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to OpenPGP keys. - */ -package org.pgpainless.key.modification; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java deleted file mode 100644 index 663c6e41..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ /dev/null @@ -1,810 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import static org.pgpainless.key.util.KeyRingUtils.changePassphrase; -import static org.pgpainless.util.CollectionUtils.concat; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPublicKey; -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.AlgorithmSuite; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnprotectedKeysProtector; -import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.builder.DirectKeySelfSignatureBuilder; -import org.pgpainless.signature.builder.PrimaryKeyBindingSignatureBuilder; -import org.pgpainless.signature.builder.RevocationSignatureBuilder; -import org.pgpainless.signature.builder.SelfSignatureBuilder; -import org.pgpainless.signature.builder.SubkeyBindingSignatureBuilder; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { - - private PGPSecretKeyRing secretKeyRing; - private final Date referenceTime; - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing) { - this(secretKeyRing, new Date()); - } - - public SecretKeyRingEditor(@Nonnull PGPSecretKeyRing secretKeyRing, - @Nonnull Date referenceTime) { - this.secretKeyRing = secretKeyRing; - this.referenceTime = referenceTime; - } - - @Nonnull - @Override - public Date getReferenceTime() { - return referenceTime; - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return addUserId(userId, null, secretKeyRingProtector); - } - - @Override - public SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String sanitizeUserId = sanitizeUserId(userId); - - // user-id certifications live on the primary key - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - - // retain key flags from previous signature - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (info.isHardRevoked(userId.toString())) { - throw new IllegalArgumentException("User-ID " + userId + " is hard revoked and cannot be re-certified."); - } - List keyFlags = info.getKeyFlagsOf(info.getKeyId()); - - Set hashAlgorithmPreferences; - Set symmetricKeyAlgorithmPreferences; - Set compressionAlgorithmPreferences; - try { - hashAlgorithmPreferences = info.getPreferredHashAlgorithms(); - symmetricKeyAlgorithmPreferences = info.getPreferredSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = info.getPreferredCompressionAlgorithms(); - } catch (IllegalStateException e) { - // missing user-id sig - AlgorithmSuite algorithmSuite = AlgorithmSuite.getDefaultAlgorithmSuite(); - hashAlgorithmPreferences = algorithmSuite.getHashAlgorithms(); - symmetricKeyAlgorithmPreferences = algorithmSuite.getSymmetricKeyAlgorithms(); - compressionAlgorithmPreferences = algorithmSuite.getCompressionAlgorithms(); - } - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, protector); - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - builder.setSignatureType(SignatureType.POSITIVE_CERTIFICATION); - - // Retain signature subpackets of previous signatures - builder.getHashedSubpackets().setKeyFlags(keyFlags); - builder.getHashedSubpackets().setPreferredHashAlgorithms(hashAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences); - builder.getHashedSubpackets().setPreferredCompressionAlgorithms(compressionAlgorithmPreferences); - builder.getHashedSubpackets().setFeatures(Feature.MODIFICATION_DETECTION); - - builder.applyCallback(signatureSubpacketCallback); - - PGPSignature signature = builder.build(primaryKey.getPublicKey(), sanitizeUserId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, sanitizeUserId, signature); - - return this; - } - - @Override - public SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - - // Determine previous key expiration date - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - String primaryUserId = info.getPrimaryUserId(); - PGPSignature signature = primaryUserId == null ? - info.getLatestDirectKeySelfSignature() : info.getLatestUserIdCertification(primaryUserId); - final Date previousKeyExpiration = signature == null ? null : - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(signature, primaryKey); - - // Add new primary user-id signature - addUserId( - userId, - new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(); - if (previousKeyExpiration != null) { - hashedSubpackets.setKeyExpirationTime(primaryKey, previousKeyExpiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }, - protector); - - // unmark previous primary user-ids to be non-primary - info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String otherUserId : info.getValidAndExpiredUserIds()) { - if (userId.toString().equals(otherUserId)) { - continue; - } - - // We need to unmark this user-id as primary - PGPSignature userIdCertification = info.getLatestUserIdCertification(otherUserId); - assert (userIdCertification != null); - - if (userIdCertification.getHashedSubPackets().isPrimaryUserID()) { - addUserId(otherUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setPrimaryUserId(null); - hashedSubpackets.setKeyExpirationTime(null); // non-primary - } - }, protector); - } - } - return this; - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException { - RevocationAttributes revocationAttributes = RevocationAttributes.createCertificateRevocation() - .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) - .withoutDescription(); - return revokeUserIds(userIdSelector, - protector, - revocationAttributes); - } - - @Override - public SecretKeyRingEditorInterface removeUserId( - CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException { - return removeUserId( - SelectUserId.exactMatch(userId.toString()), - protector); - } - - @Override - public SecretKeyRingEditorInterface replaceUserId(@Nonnull CharSequence oldUserId, - @Nonnull CharSequence newUserId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - String oldUID = oldUserId.toString().trim(); - String newUID = newUserId.toString().trim(); - if (oldUID.isEmpty()) { - throw new IllegalArgumentException("Old user-id cannot be empty."); - } - - if (newUID.isEmpty()) { - throw new IllegalArgumentException("New user-id cannot be empty."); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - if (!info.isUserIdValid(oldUID)) { - throw new NoSuchElementException("Key does not carry user-id '" + oldUID + "', or it is not valid."); - } - - PGPSignature oldCertification = info.getLatestUserIdCertification(oldUID); - if (oldCertification == null) { - throw new AssertionError("Certification for old user-id MUST NOT be null."); - } - - // Bind new user-id - addUserId(newUserId, new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getHashedSubPackets(), (SignatureSubpackets) hashedSubpackets); - // Primary user-id - if (oldUID.equals(info.getPrimaryUserId())) { - // Implicit primary user-id - if (!oldCertification.getHashedSubPackets().isPrimaryUserID()) { - hashedSubpackets.setPrimaryUserId(); - } - } - } - - @Override - public void modifyUnhashedSubpackets(SelfSignatureSubpackets unhashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.getUnhashedSubPackets(), (SignatureSubpackets) unhashedSubpackets); - } - }, protector); - - return revokeUserId(oldUID, protector); - } - - // TODO: Move to utility class? - private String sanitizeUserId(@Nonnull CharSequence userId) { - // TODO: Further research how to sanitize user IDs. - // eg. what about newlines? - return userId.toString().trim(); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subKeyPassphrase); - - SelfSignatureSubpackets.Callback callback = new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - SignatureSubpacketsHelper.applyFrom(keySpec.getSubpackets(), (SignatureSubpackets) hashedSubpackets); - } - }; - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, callback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nullable Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec); - - SecretKeyRingProtector subKeyProtector = PasswordBasedSecretKeyRingProtector - .forKeyId(keyPair.getKeyID(), subkeyPassphrase); - - List keyFlags = KeyFlag.fromBitmask(keySpec.getSubpackets().getKeyFlags()); - KeyFlag firstFlag = keyFlags.remove(0); - KeyFlag[] otherFlags = keyFlags.toArray(new KeyFlag[0]); - - return addSubKey(keyPair, subpacketsCallback, subKeyProtector, secretKeyRingProtector, firstFlag, otherFlags); - } - - @Override - public SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException { - KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); - PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); - - // check key against public key algorithm policy - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); - int bitStrength = subkey.getPublicKey().getBitStrength(); - if (!PGPainless.getPolicy().getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) { - throw new IllegalArgumentException("Public key algorithm policy violation: " + - publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable."); - } - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - HashAlgorithm hashAlgorithm = HashAlgorithmNegotiator - .negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(info.getPreferredHashAlgorithms()); - - PGPSecretKey secretSubkey = new PGPSecretKey(subkey.getPrivateKey(), subkey.getPublicKey(), ImplementationFactory.getInstance() - .getV4FingerprintCalculator(), false, subkeyProtector.getEncryptor(subkey.getKeyID())); - - SubkeyBindingSignatureBuilder skBindingBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm); - skBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - skBindingBuilder.getHashedSubpackets().setKeyFlags(flags); - - if (subkeyAlgorithm.isSigningCapable()) { - PrimaryKeyBindingSignatureBuilder pkBindingBuilder = new PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm); - pkBindingBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - PGPSignature pkBinding = pkBindingBuilder.build(primaryKey.getPublicKey()); - skBindingBuilder.getHashedSubpackets().addEmbeddedSignature(pkBinding); - } - - skBindingBuilder.applyCallback(bindingSignatureCallback); - PGPSignature skBinding = skBindingBuilder.build(secretSubkey.getPublicKey()); - - secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBinding); - secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey); - return this; - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revoke(secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revoke(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - return revokeSubKey(secretKeyRing.getSecretKey().getKeyID(), secretKeyRingProtector, subpacketsCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long subKeyId, - SecretKeyRingProtector protector, - RevocationAttributes revocationAttributes) - throws PGPException { - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return revokeSubKey(subKeyId, protector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeSubKey(long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - // retrieve subkey to be revoked - PGPPublicKey revokeeSubKey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, keyID); - // create revocation - PGPSignature subKeyRevocation = generateRevocation(secretKeyRingProtector, revokeeSubKey, - subpacketsCallback); - // inject revocation sig into key ring - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, revokeeSubKey, subKeyRevocation); - return this; - } - - @Override - public PGPSignature createRevocation(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubKey = secretKeyRing.getPublicKey(); - PGPSignature revocationCertificate = generateRevocation( - secretKeyRingProtector, revokeeSubKey, callbackFromRevocationAttributes(revocationAttributes)); - return revocationCertificate; - } - - @Override - public PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - RevocationSignatureSubpackets.Callback callback = callbackFromRevocationAttributes(revocationAttributes); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, callback); - } - - @Override - public PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException { - PGPPublicKey revokeeSubkey = KeyRingUtils.requirePublicKeyFrom(secretKeyRing, subkeyId); - return generateRevocation(secretKeyRingProtector, revokeeSubkey, certificateSubpacketsCallback); - } - - private PGPSignature generateRevocation(@Nonnull SecretKeyRingProtector protector, - @Nonnull PGPPublicKey revokeeSubKey, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - SignatureType signatureType = revokeeSubKey.isMasterKey() ? - SignatureType.KEY_REVOCATION : SignatureType.SUBKEY_REVOCATION; - - RevocationSignatureBuilder signatureBuilder = - new RevocationSignatureBuilder(signatureType, primaryKey, protector); - signatureBuilder.applyCallback(callback); - PGPSignature revocation = signatureBuilder.build(revokeeSubKey); - return revocation; - } - - private static RevocationSignatureSubpackets.Callback callbackFromRevocationAttributes( - @Nullable RevocationAttributes attributes) { - return new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (attributes != null) { - hashedSubpackets.setRevocationReason(attributes); - } - } - }; - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - if (revocationAttributes != null) { - RevocationAttributes.Reason reason = revocationAttributes.getReason(); - if (reason != RevocationAttributes.Reason.NO_REASON - && reason != RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { - throw new IllegalArgumentException("Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID"); - } - } - - RevocationSignatureSubpackets.Callback callback = new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - if (revocationAttributes != null) { - hashedSubpackets.setRevocationReason(false, revocationAttributes); - } - } - }; - - return revokeUserId(userId, secretKeyRingProtector, callback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException { - String sanitized = sanitizeUserId(userId); - return revokeUserIds( - SelectUserId.exactMatch(sanitized), - secretKeyRingProtector, - subpacketCallback); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return revokeUserIds( - userIdSelector, - secretKeyRingProtector, - new RevocationSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(RevocationSignatureSubpackets hashedSubpackets) { - hashedSubpackets.setRevocationReason(revocationAttributes); - } - }); - } - - @Override - public SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException { - List selected = userIdSelector.selectUserIds(secretKeyRing); - if (selected.isEmpty()) { - throw new NoSuchElementException("No matching user-ids found on the key."); - } - - for (String userId : selected) { - doRevokeUserId(userId, secretKeyRingProtector, subpacketsCallback); - } - - return this; - } - - private SecretKeyRingEditorInterface doRevokeUserId( - @Nonnull String userId, - @Nonnull SecretKeyRingProtector protector, - @Nullable RevocationSignatureSubpackets.Callback callback) - throws PGPException { - PGPSecretKey primarySecretKey = secretKeyRing.getSecretKey(); - RevocationSignatureBuilder signatureBuilder = new RevocationSignatureBuilder( - SignatureType.CERTIFICATION_REVOCATION, - primarySecretKey, - protector); - if (referenceTime != null) { - signatureBuilder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - - signatureBuilder.applyCallback(callback); - - PGPSignature revocationSignature = signatureBuilder.build(userId); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, userId, revocationSignature); - return this; - } - - @Override - public SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - if (!primaryKey.isMasterKey()) { - throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key."); - } - - // reissue direct key sig - PGPSignature prevDirectKeySig = getPreviousDirectKeySignature(); - if (prevDirectKeySig != null) { - PGPSignature directKeySig = reissueDirectKeySignature(expiration, secretKeyRingProtector, prevDirectKeySig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryKey.getPublicKey(), directKeySig); - } - - // reissue primary user-id sig - String primaryUserId = PGPainless.inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId(); - if (primaryUserId != null) { - PGPSignature prevUserIdSig = getPreviousUserIdSignatures(primaryUserId); - PGPSignature userIdSig = reissuePrimaryUserIdSig(expiration, secretKeyRingProtector, primaryUserId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - for (String userId : info.getValidUserIds()) { - if (userId.equals(primaryUserId)) { - continue; - } - - PGPSignature prevUserIdSig = info.getLatestUserIdCertification(userId); - if (prevUserIdSig == null) { - throw new AssertionError("A valid user-id shall never have no user-id signature."); - } - - if (prevUserIdSig.getHashedSubPackets().isPrimaryUserID()) { - assert (primaryUserId != null); - PGPSignature userIdSig = reissueNonPrimaryUserId(secretKeyRingProtector, userId, prevUserIdSig); - secretKeyRing = KeyRingUtils.injectCertification(secretKeyRing, primaryUserId, userIdSig); - } - } - - return this; - } - - @Override - public PGPPublicKeyRing createMinimalRevocationCertificate( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException { - // Check reason - if (keyRevocationAttributes != null && !RevocationAttributes.Reason.isKeyRevocation(keyRevocationAttributes.getReason())) { - throw new IllegalArgumentException("Revocation reason MUST be applicable to a key revocation."); - } - - PGPSignature revocation = createRevocation(secretKeyRingProtector, keyRevocationAttributes); - PGPPublicKey primaryKey = secretKeyRing.getSecretKey().getPublicKey(); - primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey); - primaryKey = PGPPublicKey.addCertification(primaryKey, revocation); - return new PGPPublicKeyRing(Collections.singletonList(primaryKey)); - } - - private PGPSignature reissueNonPrimaryUserId( - SecretKeyRingProtector secretKeyRingProtector, - String userId, - PGPSignature prevUserIdSig) - throws PGPException { - SelfSignatureBuilder builder = new SelfSignatureBuilder(secretKeyRing.getSecretKey(), secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - // unmark as primary - hashedSubpackets.setPrimaryUserId(null); - } - }); - return builder.build(secretKeyRing.getPublicKey(), userId); - } - - private PGPSignature reissuePrimaryUserIdSig( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nonnull String primaryUserId, - @Nonnull PGPSignature prevUserIdSig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - - SelfSignatureBuilder builder = new SelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevUserIdSig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(true, publicKey.getCreationTime(), expiration); - } else { - hashedSubpackets.setKeyExpirationTime(new KeyExpirationTime(true, 0)); - } - hashedSubpackets.setPrimaryUserId(); - } - }); - return builder.build(publicKey, primaryUserId); - } - - private PGPSignature reissueDirectKeySignature( - Date expiration, - SecretKeyRingProtector secretKeyRingProtector, - PGPSignature prevDirectKeySig) - throws PGPException { - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPPublicKey publicKey = primaryKey.getPublicKey(); - final Date keyCreationTime = publicKey.getCreationTime(); - - DirectKeySelfSignatureBuilder builder = new DirectKeySelfSignatureBuilder(primaryKey, secretKeyRingProtector, prevDirectKeySig); - if (referenceTime != null) { - builder.getHashedSubpackets().setSignatureCreationTime(referenceTime); - } - builder.applyCallback(new SelfSignatureSubpackets.Callback() { - @Override - public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) { - if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(keyCreationTime, expiration); - } else { - hashedSubpackets.setKeyExpirationTime(null); - } - } - }); - - return builder.build(publicKey); - } - - private PGPSignature getPreviousDirectKeySignature() { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestDirectKeySelfSignature(); - } - - private PGPSignature getPreviousUserIdSignatures(String userId) { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeyRing, referenceTime); - return info.getLatestUserIdCertification(userId); - } - - @Override - public WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - SecretKeyRingProtector protector = new PasswordBasedSecretKeyRingProtector( - oldProtectionSettings, - new SolitaryPassphraseProvider(oldPassphrase)); - - return new WithKeyRingEncryptionSettingsImpl(null, protector); - } - - @Override - public WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase( - @Nonnull Long keyId, - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings) { - Map passphraseMap = Collections.singletonMap(keyId, oldPassphrase); - SecretKeyRingProtector protector = new CachingSecretKeyRingProtector( - passphraseMap, oldProtectionSettings, null); - - return new WithKeyRingEncryptionSettingsImpl(keyId, protector); - } - - @Override - public PGPSecretKeyRing done() { - return secretKeyRing; - } - - private final class WithKeyRingEncryptionSettingsImpl implements WithKeyRingEncryptionSettings { - - private final Long keyId; - // Protector to unlock the key with the old passphrase - private final SecretKeyRingProtector oldProtector; - - /** - * Builder for selecting protection settings. - * - * If the keyId is null, the whole keyRing will get the same new passphrase. - * - * @param keyId id of the subkey whose passphrase will be changed, or null. - * @param oldProtector protector do unlock the key/ring. - */ - private WithKeyRingEncryptionSettingsImpl(Long keyId, SecretKeyRingProtector oldProtector) { - this.keyId = keyId; - this.oldProtector = oldProtector; - } - - @Override - public WithPassphrase withSecureDefaultSettings() { - return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()); - } - - @Override - public WithPassphrase withCustomSettings(KeyRingProtectionSettings settings) { - return new WithPassphraseImpl(keyId, oldProtector, settings); - } - } - - private final class WithPassphraseImpl implements WithPassphrase { - - private final SecretKeyRingProtector oldProtector; - private final KeyRingProtectionSettings newProtectionSettings; - private final Long keyId; - - private WithPassphraseImpl( - Long keyId, - SecretKeyRingProtector oldProtector, - KeyRingProtectionSettings newProtectionSettings) { - this.keyId = keyId; - this.oldProtector = oldProtector; - this.newProtectionSettings = newProtectionSettings; - } - - @Override - public SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) - throws PGPException { - SecretKeyRingProtector newProtector = new PasswordBasedSecretKeyRingProtector( - newProtectionSettings, new SolitaryPassphraseProvider(passphrase)); - - PGPSecretKeyRing secretKeys = changePassphrase( - keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - - @Override - public SecretKeyRingEditorInterface toNoPassphrase() - throws PGPException { - SecretKeyRingProtector newProtector = new UnprotectedKeysProtector(); - - PGPSecretKeyRing secretKeys = changePassphrase( - keyId, SecretKeyRingEditor.this.secretKeyRing, oldProtector, newProtector); - SecretKeyRingEditor.this.secretKeyRing = secretKeys; - - return SecretKeyRingEditor.this; - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java deleted file mode 100644 index dd7ed499..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ /dev/null @@ -1,652 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.modification.secretkeyring; - -import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.selection.userid.SelectUserId; - -public interface SecretKeyRingEditorInterface { - - /** - * Return the editors reference time. - * - * @return reference time - */ - @Nonnull - Date getReferenceTime(); - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param secretKeyRingProtector protector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Add a user-id to the key ring. - * - * @param userId user-id - * @param signatureSubpacketCallback callback that can be used to modify signature subpackets of the - * certification signature. - * @param protector protector to unlock the primary secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addUserId( - @Nonnull CharSequence userId, - @Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * Add a user-id to the key ring and mark it as primary. - * If the user-id is already present, a new certification signature will be created. - * - * @param userId user id - * @param protector protector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a signature for the user-id - */ - SecretKeyRingEditorInterface addPrimaryUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke selected user-ids using soft revocation signatures. - * The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}, so that the user-id - * can be re-certified at a later point. - * - * @param userIdSelector selector to select user-ids - * @param protector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface removeUserId(SelectUserId userIdSelector, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Convenience method to revoke a single user-id using a soft revocation signature. - * The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}. so that the user-id - * can be re-certified at a later point. - * - * @param userId user-id to revoke - * @param protector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface removeUserId(CharSequence userId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Replace a user-id on the key with a new one. - * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the - * old one, with one exception: - * If the old user-id was implicitly primary (did not carry a {@link org.bouncycastle.bcpg.sig.PrimaryUserID} packet, - * but effectively was primary, then the new user-id will be explicitly marked as primary. - * - * @param oldUserId old user-id - * @param newUserId new user-id - * @param protector protector to unlock the secret key - * @return the builder - * @throws PGPException in case we cannot generate a revocation and certification signature - * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId - * was already invalid - */ - SecretKeyRingEditorInterface replaceUserId(CharSequence oldUserId, - CharSequence newUserId, - SecretKeyRingProtector protector) - throws PGPException; - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key specification - * @param subKeyPassphrase passphrase to encrypt the sub key - * @param secretKeyRingProtector protector to unlock the secret key of the key ring - * @return the builder - * - * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key - * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subKeyPassphrase, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException; - - /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided {@link KeySpec}. - * - * @param keySpec key spec of the subkey - * @param subkeyPassphrase passphrase to encrypt the subkey - * @param subpacketsCallback callback to modify the subpackets of the subkey binding signature - * @param secretKeyRingProtector protector to unlock the primary key - * @return builder - * - * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key - * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull KeySpec keySpec, - @Nonnull Passphrase subkeyPassphrase, - @Nullable SelfSignatureSubpackets.Callback subpacketsCallback, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException; - - /** - * Add a subkey to the key ring. - * - * @param subkey subkey key pair - * @param bindingSignatureCallback callback to modify the subpackets of the subkey binding signature - * @param subkeyProtector protector to unlock and encrypt the subkey - * @param primaryKeyProtector protector to unlock the primary key - * @param keyFlag first key flag for the subkey - * @param additionalKeyFlags optional additional key flags - * @return builder - * - * @throws PGPException in case we cannot generate a binding signature for the subkey - * @throws IOException in case of an IO error - */ - SecretKeyRingEditorInterface addSubKey( - @Nonnull PGPKeyPair subkey, - @Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback, - @Nonnull SecretKeyRingProtector subkeyProtector, - @Nonnull SecretKeyRingProtector primaryKeyProtector, - @Nonnull KeyFlag keyFlag, - KeyFlag... additionalKeyFlags) - throws PGPException, IOException; - - /** - * Revoke the key ring. - * The revocation will be a hard revocation, rendering the whole key invalid for any past or future signatures. - * - * @param secretKeyRingProtector protector of the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - default SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revoke(secretKeyRingProtector, (RevocationAttributes) null); - } - - /** - * Revoke the key ring using the provided revocation attributes. - * The attributes define, whether the revocation was a hard revocation or not. - * - * @param secretKeyRingProtector protector of the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the key ring. - * You can use the {@link RevocationSignatureSubpackets.Callback} to modify the revocation signatures - * subpackets, e.g. in order to define whether this is a hard or soft revocation. - * - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature - */ - SecretKeyRingEditorInterface revoke( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * Note: This method will hard-revoke the provided subkey, meaning it cannot be re-certified at a later point. - * If you instead want to temporarily "deactivate" the subkey, provide a soft revocation reason, - * e.g. by calling {@link #revokeSubKey(OpenPgpFingerprint, SecretKeyRingProtector, RevocationAttributes)} - * and provide a suitable {@link RevocationAttributes} object. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - @Nonnull OpenPgpFingerprint fingerprint, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeSubKey(fingerprint, secretKeyRingProtector, null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param fingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - OpenPgpFingerprint fingerprint, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException { - return revokeSubKey(fingerprint.getKeyId(), - secretKeyRingProtector, - revocationAttributes); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - SecretKeyRingProtector secretKeyRingProtector, - RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * Note: This method will hard-revoke the subkey, meaning it cannot be re-bound at a later point. - * If you intend to re-bind the subkey in order to make it usable again at a later point in time, - * consider using {@link #revokeSubKey(long, SecretKeyRingProtector, RevocationAttributes)} - * and provide a soft revocation reason. - * - * @param subKeyId id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - default SecretKeyRingEditorInterface revokeSubKey( - long subKeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - - return revokeSubKey( - subKeyId, - secretKeyRingProtector, - (RevocationSignatureSubpackets.Callback) null); - } - - /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown. - * - * The provided subpackets callback is used to modify the revocation signatures subpackets. - * - * @param keyID id of the subkey - * @param secretKeyRingProtector protector to unlock the secret key ring - * @param subpacketsCallback callback which can be used to modify the subpackets of the revocation - * signature - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the subkey - */ - SecretKeyRingEditorInterface revokeSubKey( - long keyID, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException; - - /** - * Revoke the given userID. - * The revocation will be a hard revocation, rendering the user-id invalid for any past or future signatures. - * If you intend to re-certify the user-id at a later point in time, consider using - * {@link #revokeUserId(CharSequence, SecretKeyRingProtector, RevocationAttributes)} instead and provide - * a soft revocation reason. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - default SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException { - return revokeUserId(userId, secretKeyRingProtector, (RevocationAttributes) null); - } - - /** - * Revoke the given userID using the provided revocation attributes. - * - * @param userId userId to revoke - * @param secretKeyRingProtector protector to unlock the primary key - * @param revocationAttributes reason for the revocation - * @return the builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke the provided user-id. - * Note: If you don't provide a {@link RevocationSignatureSubpackets.Callback} which - * sets a revocation reason ({@link RevocationAttributes}), the revocation might be considered hard. - * So if you intend to re-certify the user-id at a later point to make it valid again, - * make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback. - * - * @param userId userid to be revoked - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserId( - @Nonnull CharSequence userId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketCallback) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link RevocationAttributes} will be set as reason for revocation in each - * revocation signature. - * - * Note: If you intend to re-certify these user-ids at a later point, make sure to choose - * a soft revocation reason. See {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param revocationAttributes revocation attributes - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Revoke all user-ids that match the provided {@link SelectUserId} filter. - * The provided {@link RevocationSignatureSubpackets.Callback} will be used to modify the - * revocation signatures subpackets. - * - * Note: If you intend to re-certify these user-ids at a later point, make sure to set - * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. - * - * See {@link RevocationAttributes.Reason} for more information. - * - * @param userIdSelector user-id selector - * @param secretKeyRingProtector protector to unlock the primary secret key - * @param subpacketsCallback callback to modify the revocations subpackets - * @return builder - * - * @throws PGPException in case we cannot generate a revocation signature for the user-id - */ - SecretKeyRingEditorInterface revokeUserIds( - @Nonnull SelectUserId userIdSelector, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) - throws PGPException; - - /** - * Set the expiration date for the primary key of the key ring. - * If the key is supposed to never expire, then an expiration date of null is expected. - * - * @param expiration new expiration date or null - * @param secretKeyRingProtector to unlock the secret key - * @return the builder - * - * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date - */ - SecretKeyRingEditorInterface setExpirationDate( - @Nullable Date expiration, - @Nonnull SecretKeyRingProtector secretKeyRingProtector) - throws PGPException; - - /** - * Create a minimal, self-authorizing revocation certificate, containing only the primary key - * and a revocation signature. - * This type of revocation certificates was introduced in OpenPGP v6. - * This method has no side effects on the original key and will leave it intact. - * - * @param secretKeyRingProtector protector to unlock the primary key. - * @param keyRevocationAttributes reason for the revocation (key revocation) - * @return minimal revocation certificate - * - * @throws PGPException in case we cannot generate a revocation signature - */ - PGPPublicKeyRing createMinimalRevocationCertificate(@Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes keyRevocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the whole key. - * The original key will not be modified by this method. - * - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyId id of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param certificateSubpacketsCallback callback to modify the subpackets of the revocation certificate. - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - PGPSignature createRevocation( - long subkeyId, - @Nonnull SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback) - throws PGPException; - - /** - * Create a detached revocation certificate, which can be used to revoke the specified subkey. - * The original key will not be modified by this method. - * - * @param subkeyFingerprint fingerprint of the subkey to be revoked - * @param secretKeyRingProtector protector to unlock the primary key. - * @param revocationAttributes reason for the revocation - * @return revocation certificate - * - * @throws PGPException in case we cannot generate a revocation certificate - */ - default PGPSignature createRevocation( - OpenPgpFingerprint subkeyFingerprint, - SecretKeyRingProtector secretKeyRingProtector, - @Nullable RevocationAttributes revocationAttributes) - throws PGPException { - - return createRevocation( - subkeyFingerprint.getKeyId(), - secretKeyRingProtector, - revocationAttributes); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @return next builder step - */ - default WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase) { - return changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - /** - * Change the passphrase of the whole key ring. - * - * @param oldPassphrase old passphrase or null, if the key was unprotected - * @param oldProtectionSettings custom settings for the old passphrase - * @return next builder step - */ - WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase( - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - /** - * Change the passphrase of a single subkey in the key ring. - * - * Note: While it is a valid use-case to have different passphrases per subKey, - * this is one of the reasons why OpenPGP sucks in practice. - * - * @param keyId id of the subkey - * @param oldPassphrase old passphrase - * @return next builder step - */ - default WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase( - @Nonnull Long keyId, - @Nullable Passphrase oldPassphrase) { - return changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()); - } - - WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase( - @Nonnull Long keyId, - @Nullable Passphrase oldPassphrase, - @Nonnull KeyRingProtectionSettings oldProtectionSettings); - - interface WithKeyRingEncryptionSettings { - - /** - * Set secure default settings for the symmetric passphrase encryption. - * Note that this obviously has no effect if you decide to set {@link WithPassphrase#toNoPassphrase()}. - * - * @return next builder step - */ - WithPassphrase withSecureDefaultSettings(); - - /** - * Set custom settings for the symmetric passphrase encryption. - * - * @param settings custom settings - * @return next builder step - */ - WithPassphrase withCustomSettings(KeyRingProtectionSettings settings); - - } - - interface WithPassphrase { - - /** - * Set the passphrase. - * - * @param passphrase passphrase - * @return editor builder - * - * @throws PGPException in case the passphrase cannot be changed - */ - SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase) - throws PGPException; - - /** - * Leave the key unprotected. - * - * @return editor builder - * - * @throws PGPException in case the passphrase cannot be changed - */ - SecretKeyRingEditorInterface toNoPassphrase() throws PGPException; - } - - /** - * Return the {@link PGPSecretKeyRing}. - * @return the key - */ - PGPSecretKeyRing done(); - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java deleted file mode 100644 index 6b3eb3b3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes that deal with modifications made to {@link org.bouncycastle.openpgp.PGPSecretKeyRing PGPSecretKeyRings}. - */ -package org.pgpainless.key.modification.secretkeyring; diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index 7ccddb42..afa38aa6 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -4,6 +4,7 @@ package org.bouncycastle.extensions +import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPPublicKey @@ -46,6 +47,12 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = this.getPublicKey(fingerprint.bytes) +fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = + getPublicKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + +fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = + getPublicKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + /** * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 6ccdc81d..208d8060 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -212,6 +212,48 @@ class KeyRingInfo( else userIdExpirationDate } + /** + * List of all subkeys that can be used to sign a message. + */ + val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + + /** + * Whether the key is usable for encryption. + */ + val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) + + /** + * Whether the key is capable of signing messages. + * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret + * key is unavailable, e.g. because it was moved to a smart-card. + * + * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. + */ + val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() + + /** + * Whether the key is actually usable to sign messages. + */ + val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + + /** + * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. + */ + val preferredHashAlgorithms: Set + get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) + + /** + * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. + */ + val preferredSymmetricKeyAlgorithms: Set + get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) + + /** + * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. + */ + val preferredCompressionAlgorithms: Set + get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) + /** * Return the expiration date of the subkey with the provided fingerprint. * @@ -331,16 +373,6 @@ class KeyRingInfo( }.toList() } - /** - * List of all subkeys that can be used to sign a message. - */ - val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } - - /** - * Whether the key is usable for encryption. - */ - val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) - /** * Return, whether the key is usable for encryption, given the purpose. * @@ -350,20 +382,6 @@ class KeyRingInfo( return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() } - /** - * Whether the key is capable of signing messages. - * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret - * key is unavailable, e.g. because it was moved to a smart-card. - * - * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. - */ - val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() - - /** - * Whether the key is actually usable to sign messages. - */ - val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } - /** * Return the primary user-ID, even if it is possibly expired. * @@ -621,7 +639,7 @@ class KeyRingInfo( return false } if (sig.hashedSubPackets.isPrimaryUserID) { - SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> + getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> // key expired? if (expirationDate < referenceDate) return false } @@ -634,12 +652,6 @@ class KeyRingInfo( } ?: true // certification, but no revocation } ?: false // no certification - /** - * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. - */ - val preferredHashAlgorithms: Set - get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) - /** * [HashAlgorithm] preferences of the given user-ID. */ @@ -654,12 +666,6 @@ class KeyRingInfo( return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms } - /** - * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. - */ - val preferredSymmetricKeyAlgorithms: Set - get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) - /** * [SymmetricKeyAlgorithm] preferences of the given user-ID. */ @@ -674,12 +680,6 @@ class KeyRingInfo( return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredSymmetricKeyAlgorithms } - /** - * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. - */ - val preferredCompressionAlgorithms: Set - get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) - /** * [CompressionAlgorithm] preferences of the given user-ID. */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt new file mode 100644 index 00000000..2fde469c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -0,0 +1,493 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.extensions.getKeyExpirationDate +import org.bouncycastle.extensions.publicKeyAlgorithm +import org.bouncycastle.extensions.requirePublicKey +import org.bouncycastle.openpgp.* +import org.pgpainless.PGPainless +import org.pgpainless.PGPainless.Companion.inspectKeyRing +import org.pgpainless.algorithm.AlgorithmSuite +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.* +import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.KeyRingUtils.Companion.changePassphrase +import org.pgpainless.key.util.KeyRingUtils.Companion.injectCertification +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.builder.* +import org.pgpainless.signature.subpackets.* +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId +import java.util.* +import javax.annotation.Nonnull + +class SecretKeyRingEditor( + var secretKeyRing: PGPSecretKeyRing, + override val referenceTime: Date = Date() +) : SecretKeyRingEditorInterface { + + override fun addUserId(userId: CharSequence, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val sanitizedUserId = sanitizeUserId(userId).toString() + val primaryKey = secretKeyRing.secretKey + + val info = inspectKeyRing(secretKeyRing, referenceTime) + require(!info.isHardRevoked(userId)) { + "User-ID $userId is hard revoked and cannot be re-certified." + } + + val (hashAlgorithmPreferences, symmetricKeyAlgorithmPreferences, compressionAlgorithmPreferences) = try { + Triple(info.preferredHashAlgorithms, info.preferredSymmetricKeyAlgorithms, info.preferredCompressionAlgorithms) + } catch (e : IllegalStateException) { // missing user-id sig + val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + Triple(algorithmSuite.hashAlgorithms, algorithmSuite.symmetricKeyAlgorithms, algorithmSuite.compressionAlgorithms) + } + + val builder = SelfSignatureBuilder(primaryKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + setSignatureType(SignatureType.POSITIVE_CERTIFICATION) + } + builder.hashedSubpackets.apply { + setKeyFlags(info.getKeyFlagsOf(primaryKey.keyID)) + setPreferredHashAlgorithms(hashAlgorithmPreferences) + setPreferredSymmetricKeyAlgorithms(symmetricKeyAlgorithmPreferences) + setPreferredCompressionAlgorithms(compressionAlgorithmPreferences) + setFeatures(Feature.MODIFICATION_DETECTION) + } + builder.applyCallback(callback) + secretKeyRing = injectCertification(secretKeyRing, sanitizedUserId, builder.build(primaryKey.publicKey, sanitizedUserId)) + return this + } + + override fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val uid = sanitizeUserId(userId) + val primaryKey = secretKeyRing.publicKey + var info = inspectKeyRing(secretKeyRing, referenceTime) + val primaryUserId = info.primaryUserId + val signature = if (primaryUserId == null) info.latestDirectKeySelfSignature else info.getLatestUserIdCertification(primaryUserId) + val previousKeyExpiration = signature?.getKeyExpirationDate(primaryKey.creationTime) + + // Add new primary user-id signature + addUserId(uid, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId() + if (previousKeyExpiration != null) setKeyExpirationTime(primaryKey, previousKeyExpiration) + else setKeyExpirationTime(null) + } + } + }, protector) + + // unmark previous primary user-ids to be non-primary + info = inspectKeyRing(secretKeyRing, referenceTime) + info.validAndExpiredUserIds.filterNot { it == uid }.forEach { otherUserId -> + if (info.getLatestUserIdCertification(otherUserId)!!.hashedSubPackets.isPrimaryUserID) { + // We need to unmark this user-id as primary + addUserId(otherUserId, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId(null) + setKeyExpirationTime(null) // non-primary + } + } + }, protector) + } + } + return this + } + + override fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + return revokeUserIds(selector, protector, RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription()) + } + + override fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + return removeUserId(SelectUserId.exactMatch(userId), protector) + } + + override fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val oldUID = sanitizeUserId(oldUserId) + val newUID = sanitizeUserId(newUserId) + require(oldUID.isNotBlank()) { "Old user-ID cannot be empty." } + require(newUID.isNotBlank()) { "New user-ID cannot be empty." } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + if (!info.isUserIdValid(oldUID)) { + throw NoSuchElementException("Key does not carry user-ID '$oldUID', or it is not valid.") + } + + val oldCertification = info.getLatestUserIdCertification(oldUID) + ?: throw AssertionError("Certification for old user-ID MUST NOT be null.") + + addUserId(newUID, object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) + if (oldUID == info.primaryUserId && !oldCertification.hashedSubPackets.isPrimaryUserID) { + hashedSubpackets.setPrimaryUserId() + } + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(oldCertification.unhashedSubPackets, unhashedSubpackets as SignatureSubpackets) + } + }, protector) + + return revokeUserId(oldUID, protector) + } + + override fun addSubKey(keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val callback = object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + SignatureSubpacketsHelper.applyFrom(keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + } + } + return addSubKey(keySpec, subkeyPassphrase, callback, protector) + } + + override fun addSubKey(keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + val keyPair = KeyRingBuilder.generateKeyPair(keySpec) + val subkeyProtector = PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) + val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() + return addSubKey(keyPair, callback, subkeyProtector, protector, keyFlags.removeFirst(), *keyFlags.toTypedArray()) + } + + override fun addSubKey(subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface { + val flags = listOf(keyFlag).plus(keyFlags) + val subkeyAlgorithm = subkey.publicKey.publicKeyAlgorithm + SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm) + + val bitStrength = subkey.publicKey.bitStrength + require(PGPainless.getPolicy().publicKeyAlgorithmPolicy.isAcceptable(subkeyAlgorithm, bitStrength)) { + "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." + } + + val primaryKey = secretKeyRing.secretKey + val info = inspectKeyRing(secretKeyRing, referenceTime) + val hashAlgorithm = HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm(info.preferredHashAlgorithms) + + var secretSubkey = PGPSecretKey(subkey.privateKey, subkey.publicKey, + ImplementationFactory.getInstance().v4FingerprintCalculator, + false, subkeyProtector.getEncryptor(subkey.keyID)) + val skBindingBuilder = SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) + skBindingBuilder.apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.setKeyFlags(flags) + if (subkeyAlgorithm.isSigningCapable()) { + val pkBindingBuilder = PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) + pkBindingBuilder.hashedSubpackets.setSignatureCreationTime(referenceTime) + hashedSubpackets.addEmbeddedSignature(pkBindingBuilder.build(primaryKey.publicKey)) + } + applyCallback(callback) + } + secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) + secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey) + return this + } + + override fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revoke(protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + return revokeSubKey(secretKeyRing.secretKey.keyID, protector, callback) + } + + override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revokeSubKey(subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + val revokeeSubKey = secretKeyRing.requirePublicKey(subkeyId) + val subkeyRevocation = generateRevocation(protector, revokeeSubKey, callback) + secretKeyRing = injectCertification(secretKeyRing, revokeeSubKey, subkeyRevocation) + return this + } + + override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + if (revocationAttributes != null) { + require(revocationAttributes.reason == RevocationAttributes.Reason.NO_REASON || + revocationAttributes.reason == RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { + "Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID" + } + } + + return revokeUserId(userId, protector, object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(false, revocationAttributes) + } + } + }) + } + + override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + return revokeUserIds(SelectUserId.exactMatch(sanitizeUserId(userId)), protector, callback) + } + + override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + return revokeUserIds(selector, protector, object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(revocationAttributes) + } + } + }) + } + + override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + selector.selectUserIds(secretKeyRing).also { + if (it.isEmpty()) throw NoSuchElementException("No matching user-ids found on the key.") + }.forEach { userId -> doRevokeUserId(userId, protector, callback) } + return this + } + + override fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + require(secretKeyRing.secretKey.isMasterKey) { + "OpenPGP key does not appear to contain a primary secret key." + } + + val prevDirectKeySig = getPreviousDirectKeySignature() + // reissue direct key sig + if (prevDirectKeySig != null) { + secretKeyRing = injectCertification(secretKeyRing, secretKeyRing.publicKey, + reissueDirectKeySignature(expiration, protector, prevDirectKeySig)) + } + + val primaryUserId = inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() + if (primaryUserId != null) { + val prevUserIdSig = getPreviousUserIdSignatures(primaryUserId) + val userIdSig = reissuePrimaryUserIdSig(expiration, protector, primaryUserId, prevUserIdSig!!) + secretKeyRing = injectCertification(secretKeyRing, primaryUserId, userIdSig) + } + + val info = inspectKeyRing(secretKeyRing, referenceTime) + for (userId in info.validUserIds) { + if (userId == primaryUserId) { + continue + } + + val prevUserIdSig = info.getLatestUserIdCertification(userId) ?: throw AssertionError("A valid user-id shall never have no user-id signature.") + if (prevUserIdSig.hashedSubPackets.isPrimaryUserID) { + secretKeyRing = injectCertification(secretKeyRing, primaryUserId!!, + reissueNonPrimaryUserId(protector, userId, prevUserIdSig)) + } + } + + return this + } + + override fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPPublicKeyRing { + // Check reason + if (revocationAttributes != null) { + require(RevocationAttributes.Reason.isKeyRevocation(revocationAttributes.reason)) { + "Revocation reason MUST be applicable to a key revocation." + } + } + + val revocation = createRevocation(protector, revocationAttributes) + var primaryKey = secretKeyRing.secretKey.publicKey + primaryKey = KeyRingUtils.getStrippedDownPublicKey(primaryKey) + primaryKey = PGPPublicKey.addCertification(primaryKey, revocation) + return PGPPublicKeyRing(listOf(primaryKey)) + } + + override fun createRevocation(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.publicKey, callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callback) + } + + override fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { + return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyFingerprint), callbackFromRevocationAttributes(revocationAttributes)) + } + + override fun changePassphraseFromOldPassphrase(oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl(this, null, + PasswordBasedSecretKeyRingProtector(oldProtectionSettings, SolitaryPassphraseProvider(oldPassphrase))) + } + + override fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl(this, keyId, + CachingSecretKeyRingProtector(mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) + } + + override fun done(): PGPSecretKeyRing { + return secretKeyRing + } + + private fun sanitizeUserId(userId: CharSequence): CharSequence = + // TODO: Further research how to sanitize user IDs. + // eg. what about newlines? + userId.toString().trim() + + private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (attributes != null) { + hashedSubpackets.setRevocationReason(attributes) + } + } + } + + private fun generateRevocation(protector: SecretKeyRingProtector, + revokeeSubkey: PGPPublicKey, + callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + val primaryKey = secretKeyRing.secretKey + val signatureType = + if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION + else SignatureType.SUBKEY_REVOCATION + + return RevocationSignatureBuilder(signatureType, primaryKey, protector) + .apply { applyCallback(callback) } + .build(revokeeSubkey) + } + + private fun doRevokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + RevocationSignatureBuilder(SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(callback) + }.let { + secretKeyRing = injectCertification(secretKeyRing, userId, it.build(userId.toString())) + } + return this + } + + + private fun getPreviousDirectKeySignature(): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.latestDirectKeySelfSignature + } + + private fun getPreviousUserIdSignatures(userId: String): PGPSignature? { + val info = inspectKeyRing(secretKeyRing, referenceTime) + return info.getLatestUserIdCertification(userId) + } + + @Throws(PGPException::class) + private fun reissueNonPrimaryUserId( + secretKeyRingProtector: SecretKeyRingProtector, + userId: String, + prevUserIdSig: PGPSignature): PGPSignature { + val builder = SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + builder.hashedSubpackets.setSignatureCreationTime(referenceTime) + builder.applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + // unmark as primary + hashedSubpackets.setPrimaryUserId(null) + } + }) + return builder.build(secretKeyRing.publicKey, userId) + } + + @Throws(PGPException::class) + private fun reissuePrimaryUserIdSig( + expiration: Date?, + @Nonnull secretKeyRingProtector: SecretKeyRingProtector, + @Nonnull primaryUserId: String, + @Nonnull prevUserIdSig: PGPSignature): PGPSignature { + return SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime(true, secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(KeyExpirationTime(true, 0)) + } + hashedSubpackets.setPrimaryUserId() + } + }) + }.build(secretKeyRing.publicKey, primaryUserId) + } + + @Throws(PGPException::class) + private fun reissueDirectKeySignature( + expiration: Date?, + secretKeyRingProtector: SecretKeyRingProtector, + prevDirectKeySig: PGPSignature): PGPSignature { + return DirectKeySelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + if (expiration != null) { + hashedSubpackets.setKeyExpirationTime(secretKeyRing.publicKey.creationTime, expiration) + } else { + hashedSubpackets.setKeyExpirationTime(null) + } + } + }) + }.build(secretKeyRing.publicKey) + } + + private class WithKeyRingEncryptionSettingsImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + + override fun withSecureDefaultSettings(): SecretKeyRingEditorInterface.WithPassphrase { + return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()) + } + + override fun withCustomSettings(settings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithPassphrase { + return WithPassphraseImpl(editor, keyId, oldProtector, settings) + } + } + + private class WithPassphraseImpl( + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector, + private val newProtectionSettings: KeyRingProtectionSettings + ) : SecretKeyRingEditorInterface.WithPassphrase { + + override fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface { + val protector = PasswordBasedSecretKeyRingProtector(newProtectionSettings, SolitaryPassphraseProvider(passphrase)) + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + + override fun toNoPassphrase(): SecretKeyRingEditorInterface { + val protector = UnprotectedKeysProtector() + val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) + editor.secretKeyRing = secretKeys + return editor + } + } + +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt new file mode 100644 index 00000000..45a09fb2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -0,0 +1,560 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification.secretkeyring + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPKeyPair +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.protection.KeyRingProtectionSettings +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.util.Passphrase +import org.pgpainless.util.selection.userid.SelectUserId +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* + +interface SecretKeyRingEditorInterface { + + /** + * Editors reference time. + * This time is used as creation date for new signatures, or as reference when evaluating expiration of + * existing signatures. + */ + val referenceTime: Date + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId(userId: CharSequence, protector: SecretKeyRingProtector) = addUserId(userId, null, protector) + + /** + * Add a user-id to the key ring. + * + * @param userId user-id + * @param callback callback to modify the self-signature subpackets + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addUserId(userId: CharSequence, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a user-id to the key ring and mark it as primary. + * If the user-id is already present, a new certification signature will be created. + * + * @param userId user id + * @param protector protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a signature for the user-id + */ + @Throws(PGPException::class) + fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke selected user-ids using soft revocation signatures. + * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the user-id + * can be re-certified at a later point. + * + * @param selector selector to select user-ids + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Convenience method to revoke a single user-id using a soft revocation signature. + * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID] so that the user-id + * can be re-certified at a later point. + * + * @param userId user-id to revoke + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Replace a user-id on the key with a new one. + * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the + * old one, with one exception: + * If the old user-id was implicitly primary (did not carry a [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, + * but effectively was primary), then the new user-id will be explicitly marked as primary. + * + * @param oldUserId old user-id + * @param newUserId new user-id + * @param protector protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a revocation and certification signature + * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId + * was already invalid + */ + @Throws(PGPException::class) + fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. + * The subkey will be generated from the provided [KeySpec]. + * + * @param keySpec key specification + * @param subkeyPassphrase passphrase to encrypt the sub key + * @param callback callback to modify the subpackets of the subkey binding signature + * @param protector protector to unlock the secret key of the key ring + * @return the builder + * + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key + * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class, InvalidAlgorithmParameterException::class, NoSuchAlgorithmException::class) + fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Add a subkey to the key ring. + * + * @param subkey subkey key pair + * @param callback callback to modify the subpackets of the subkey binding signature + * @param subkeyProtector protector to unlock and encrypt the subkey + * @param primaryKeyProtector protector to unlock the primary key + * @param keyFlag first mandatory key flag for the subkey + * @param keyFlags optional additional key flags + * @return builder + * + * @throws PGPException in case we cannot generate a binding signature for the subkey + * @throws IOException in case of an IO error + */ + @Throws(PGPException::class, IOException::class) + fun addSubKey(subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface + + /** + * Revoke the key ring using a hard revocation. + * + * @param protector protector of the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector) = revoke(protector, null as RevocationAttributes?) + + /** + * Revoke the key ring using the provided revocation attributes. + * The attributes define, whether the revocation was a hard revocation or not. + * + * @param protector protector of the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the key ring. + * You can use the [RevocationSignatureSubpackets.Callback] to modify the revocation signatures + * subpackets, e.g. in order to define whether this is a hard or soft revocation. + * + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided fingerprint will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector) = revokeSubKey(fingerprint, protector, null) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided fingerprint will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param fingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface = + revokeSubKey(fingerprint.keyId, protector, revocationAttributes) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = + revokeSubKey(subkeyId, protector, null as RevocationAttributes?) + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the subkey binding signature of a subkey. + * The subkey with the provided key-id will be revoked. + * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * + * The provided subpackets callback is used to modify the revocation signatures subpackets. + * + * @param subkeyId id of the subkey + * @param protector protector to unlock the secret key ring + * @param callback callback which can be used to modify the subpackets of the revocation + * signature + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the subkey + */ + @Throws(PGPException::class) + fun revokeSubKey(subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Hard-revoke the given userID. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector) = revokeUserId(userId, protector, null as RevocationAttributes?) + + /** + * Revoke the given userID using the provided revocation attributes. + * + * @param userId userId to revoke + * @param protector protector to unlock the primary key + * @param revocationAttributes reason for the revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + + /** + * Revoke the provided user-id. + * Note: If you don't provide a [RevocationSignatureSubpackets.Callback] which + * sets a revocation reason ([RevocationAttributes]), the revocation will be considered hard. + * So if you intend to re-certify the user-id at a later point to make it valid again, + * make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback. + * + * @param userId userid to be revoked + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserId(userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationAttributes] will be set as reason for revocation in each + * revocation signature. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose + * a soft revocation reason. See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param revocationAttributes revocation attributes + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(selector: SelectUserId, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationSignatureSubpackets.Callback] will be used to modify the + * revocation signatures subpackets. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to set + * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. + * + * See [RevocationAttributes.Reason] for more information. + * + * @param selector user-id selector + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(selector: SelectUserId, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + + /** + * Set the expiration date for the primary key of the key ring. + * If the key is supposed to never expire, then an expiration date of null is expected. + * + * @param expiration new expiration date or null + * @param protector to unlock the secret key + * @return the builder + * + * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date + */ + @Throws(PGPException::class) + fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + + /** + * Create a minimal, self-authorizing revocation certificate, containing only the primary key + * and a revocation signature. + * This type of revocation certificates was introduced in OpenPGP v6. + * This method has no side effects on the original key and will leave it intact. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation (key revocation) + * @return minimal revocation certificate + * + * @throws PGPException in case we cannot generate a revocation signature + */ + @Throws(PGPException::class) + fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPPublicKeyRing + + /** + * Create a detached revocation certificate, which can be used to revoke the whole key. + * The original key will not be modified by this method. + * + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyId id of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param callback callback to modify the subpackets of the revocation certificate. + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?): PGPSignature + + /** + * Create a detached revocation certificate, which can be used to revoke the specified subkey. + * The original key will not be modified by this method. + * + * @param subkeyFingerprint fingerprint of the subkey to be revoked + * @param protector protector to unlock the primary key. + * @param revocationAttributes reason for the revocation + * @return revocation certificate + * + * @throws PGPException in case we cannot generate a revocation certificate + */ + @Throws(PGPException::class) + fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?): PGPSignature + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @return next builder step + */ + fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase) = changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of the whole key ring. + * + * @param oldPassphrase old passphrase (empty, if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings = KeyRingProtectionSettings.secureDefaultSettings()): WithKeyRingEncryptionSettings + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, + * this is one of the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase) = + changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + + /** + * Change the passphrase of a single subkey in the key ring. + * + * Note: While it is a valid use-case to have different passphrases per subKey, + * this is one of the reasons why OpenPGP sucks in practice. + * + * @param keyId id of the subkey + * @param oldPassphrase old passphrase (empty if the key was unprotected) + * @param oldProtectionSettings custom settings for the old passphrase + * @return next builder step + */ + fun changeSubKeyPassphraseFromOldPassphrase( + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings): WithKeyRingEncryptionSettings + + interface WithKeyRingEncryptionSettings { + + /** + * Set secure default settings for the symmetric passphrase encryption. + * Note that this obviously has no effect if you decide to set [WithPassphrase.toNoPassphrase]. + * + * @return next builder step + */ + fun withSecureDefaultSettings(): WithPassphrase + + /** + * Set custom settings for the symmetric passphrase encryption. + * + * @param settings custom settings + * @return next builder step + */ + fun withCustomSettings(settings: KeyRingProtectionSettings): WithPassphrase + } + + interface WithPassphrase { + + /** + * Set the passphrase. + * + * @param passphrase passphrase + * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) + fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface + + /** + * Leave the key unprotected. + * + * @return editor builder + * + * @throws PGPException in case the passphrase cannot be changed + */ + @Throws(PGPException::class) + fun toNoPassphrase(): SecretKeyRingEditorInterface + } + + /** + * Return the [PGPSecretKeyRing]. + * @return the key + */ + fun done(): PGPSecretKeyRing + fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index d62d5bad..e5e452f2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -149,7 +149,7 @@ public class KeyRingInfoTest { private static PGPSecretKeyRing encryptSecretKeys(PGPSecretKeyRing secretKeys) throws PGPException { return PGPainless.modifyKeyRing(secretKeys) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("sw0rdf1sh")) .done(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java index 19f84930..7e15c998 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddUserIdTest.java @@ -12,9 +12,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Date; import java.util.Iterator; import java.util.NoSuchElementException; +import openpgp.DateExtensionsKt; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -113,16 +115,17 @@ public class AddUserIdTest { @Test public void addNewPrimaryUserIdTest() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + Date now = new Date(); PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); UserId bob = UserId.newBuilder().withName("Bob").noEmail().noComment().build(); assertNotEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); - secretKeys = PGPainless.modifyKeyRing(secretKeys) + secretKeys = PGPainless.modifyKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 1)) .addPrimaryUserId(bob, SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys).getPrimaryUserId()); + assertEquals("Bob", PGPainless.inspectKeyRing(secretKeys, DateExtensionsKt.plusSeconds(now, 2)).getPrimaryUserId()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java index f324892f..ba6673e5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/fixes/S2KUsageFixTest.java @@ -84,7 +84,7 @@ public class S2KUsageFixTest { } PGPSecretKeyRing after = PGPainless.modifyKeyRing(unprotected) - .changePassphraseFromOldPassphrase(null) + .changePassphraseFromOldPassphrase(Passphrase.emptyPassphrase()) .withSecureDefaultSettings() .toNewPassphrase(Passphrase.fromPassword("after")) .done(); From 68ac5af255961c43c2e0fae903e2bd339a154525 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 14 Sep 2023 15:50:29 +0200 Subject: [PATCH 157/351] Kotlin conversion: UserId --- .../java/org/pgpainless/key/util/UserId.java | 356 ------------------ .../kotlin/org/pgpainless/key/util/UserId.kt | 206 ++++++++++ .../java/org/pgpainless/key/UserIdTest.java | 7 +- 3 files changed, 209 insertions(+), 360 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java deleted file mode 100644 index b115afda..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/UserId.java +++ /dev/null @@ -1,356 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub , 2021 Flowcrypt a.s. -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.util.Comparator; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public final class UserId implements CharSequence { - - // Email regex: https://emailregex.com/ - // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international characters - // \\p{L} = Unicode Letters - // \u0900-\u097F = Hindi Letters - private static final Pattern emailPattern = Pattern.compile("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + - "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + - "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + - "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + - "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])"); - - // User-ID Regex - // "Firstname Lastname (Comment) " - // All groups are optional - // https://www.rfc-editor.org/rfc/rfc5322#page-16 - private static final Pattern nameAddrPattern = Pattern.compile("^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$"); - - public static final class Builder { - private String name; - private String comment; - private String email; - - private Builder() { - } - - private Builder(String name, String comment, String email) { - this.name = name; - this.comment = comment; - this.email = email; - } - - public Builder withName(@Nonnull String name) { - this.name = name; - return this; - } - - public Builder withComment(@Nonnull String comment) { - this.comment = comment; - return this; - } - - public Builder withEmail(@Nonnull String email) { - this.email = email; - return this; - } - - public Builder noName() { - name = null; - return this; - } - - public Builder noComment() { - comment = null; - return this; - } - - public Builder noEmail() { - email = null; - return this; - } - - public UserId build() { - return new UserId(name, comment, email); - } - } - - /** - * Parse a {@link UserId} from free-form text,

name-addr
or
mailbox
string and split it - * up into its components. - * Example inputs for this method: - *
    - *
  • john@pgpainless.org
  • - *
  • <john@pgpainless.org>
  • - *
  • John Doe
  • - *
  • John Doe <john@pgpainless.org>
  • - *
  • John Doe (work email) <john@pgpainless.org>
  • - *
- * In these cases, this method will detect email addresses, names and comments and expose those - * via the respective getters. - * This method does not support parsing mail addresses of the following formats: - *
    - *
  • Local domains without TLDs (
    user@localdomain1
    )
  • - *
  • " "@example.org
    (spaces between the quotes)
  • - *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • - *
- * Note: This method does not guarantee that
string.equals(UserId.parse(string).toString())
is true. - * For example,
UserId.parse("alice@pgpainless.org").toString()
wraps the mail address in angled brackets. - * - * @see RFC5322 §3.4. Address Specification - * @param string user-id - * @return parsed {@link UserId} object - */ - public static UserId parse(@Nonnull String string) { - Builder builder = newBuilder(); - string = string.trim(); - Matcher matcher = nameAddrPattern.matcher(string); - if (matcher.find()) { - String name = matcher.group(2); - String comment = matcher.group(4); - String mail = matcher.group(6); - matcher = emailPattern.matcher(mail); - if (!matcher.matches()) { - throw new IllegalArgumentException("Malformed email address"); - } - - if (name != null) { - builder.withName(name); - } - if (comment != null) { - builder.withComment(comment); - } - builder.withEmail(mail); - } else { - matcher = emailPattern.matcher(string); - if (matcher.matches()) { - builder.withEmail(string); - } else { - throw new IllegalArgumentException("Malformed email address"); - } - } - return builder.build(); - } - - private final String name; - private final String comment; - private final String email; - private long hash = Long.MAX_VALUE; - - private UserId(@Nullable String name, @Nullable String comment, @Nullable String email) { - this.name = name == null ? null : name.trim(); - this.comment = comment == null ? null : comment.trim(); - this.email = email == null ? null : email.trim(); - } - - public static UserId onlyEmail(@Nonnull String email) { - return new UserId(null, null, email); - } - - public static UserId nameAndEmail(@Nonnull String name, @Nonnull String email) { - return new UserId(name, null, email); - } - - public static Builder newBuilder() { - return new Builder(); - } - - public Builder toBuilder() { - return new Builder(name, comment, email); - } - - public String getName() { - return getName(false); - } - - public String getName(boolean preserveQuotes) { - if (name == null || name.isEmpty()) { - return name; - } - - if (name.startsWith("\"")) { - if (preserveQuotes) { - return name; - } - String withoutQuotes = name.substring(1); - if (withoutQuotes.endsWith("\"")) { - withoutQuotes = withoutQuotes.substring(0, withoutQuotes.length() - 1); - } - return withoutQuotes; - } - return name; - } - - public String getComment() { - return comment; - } - - public String getEmail() { - return email; - } - - @Override - public int length() { - return toString().length(); - } - - @Override - public char charAt(int i) { - return toString().charAt(i); - } - - @Override - public @Nonnull CharSequence subSequence(int i, int i1) { - return toString().subSequence(i, i1); - } - - @Override - public @Nonnull String toString() { - StringBuilder sb = new StringBuilder(); - if (name != null && !name.isEmpty()) { - sb.append(getName(true)); - } - if (comment != null && !comment.isEmpty()) { - if (sb.length() > 0) { - sb.append(' '); - } - sb.append('(').append(comment).append(')'); - } - if (email != null && !email.isEmpty()) { - if (sb.length() > 0) { - sb.append(' '); - } - sb.append('<').append(email).append('>'); - } - return sb.toString(); - } - - /** - * Returns a string representation of the object. - * @return a string representation of the object. - * @deprecated use {@link #toString()} instead. - */ - @Deprecated - public String asString() { - return toString(); - } - - @Override - public boolean equals(Object o) { - if (o == null) return false; - if (o == this) return true; - if (!(o instanceof UserId)) return false; - final UserId other = (UserId) o; - return isEqualComponent(name, other.name, false) - && isEqualComponent(comment, other.comment, false) - && isEqualComponent(email, other.email, true); - } - - @Override - public int hashCode() { - if (hash != Long.MAX_VALUE) { - return (int) hash; - } else { - int hashCode = 7; - hashCode = 31 * hashCode + (name == null ? 0 : name.hashCode()); - hashCode = 31 * hashCode + (comment == null ? 0 : comment.hashCode()); - hashCode = 31 * hashCode + (email == null ? 0 : email.toLowerCase().hashCode()); - this.hash = hashCode; - return hashCode; - } - } - - private static boolean isEqualComponent(String value, String otherValue, boolean ignoreCase) { - final boolean valueIsNull = (value == null); - final boolean otherValueIsNull = (otherValue == null); - return (valueIsNull && otherValueIsNull) - || (!valueIsNull && !otherValueIsNull - && (ignoreCase ? value.equalsIgnoreCase(otherValue) : value.equals(otherValue))); - } - - public static int compare(@Nullable UserId o1, @Nullable UserId o2, @Nonnull Comparator comparator) { - return comparator.compare(o1, o2); - } - - public static class DefaultComparator implements Comparator { - - @Override - public int compare(UserId o1, UserId o2) { - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - - NullSafeStringComparator c = new NullSafeStringComparator(); - int cName = c.compare(o1.getName(), o2.getName()); - if (cName != 0) { - return cName; - } - - int cComment = c.compare(o1.getComment(), o2.getComment()); - if (cComment != 0) { - return cComment; - } - - return c.compare(o1.getEmail(), o2.getEmail()); - } - } - - public static class DefaultIgnoreCaseComparator implements Comparator { - - @Override - public int compare(UserId o1, UserId o2) { - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - - NullSafeStringComparator c = new NullSafeStringComparator(); - int cName = c.compare(lower(o1.getName()), lower(o2.getName())); - if (cName != 0) { - return cName; - } - - int cComment = c.compare(lower(o1.getComment()), lower(o2.getComment())); - if (cComment != 0) { - return cComment; - } - - return c.compare(lower(o1.getEmail()), lower(o2.getEmail())); - } - - private static String lower(String string) { - return string == null ? null : string.toLowerCase(); - } - } - - private static class NullSafeStringComparator implements Comparator { - - @Override - public int compare(String o1, String o2) { - // noinspection StringEquality - if (o1 == o2) { - return 0; - } - if (o1 == null) { - return -1; - } - if (o2 == null) { - return 1; - } - return o1.compareTo(o2); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt new file mode 100644 index 00000000..b461f6b4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +class UserId internal constructor( + name: String?, + comment: String?, + email: String? +) : CharSequence { + + private val _name: String? + val comment: String? + val email: String? + + init { + this._name = name?.trim() + this.comment = comment?.trim() + this.email = email?.trim() + } + + val full: String = buildString { + if (name?.isNotBlank() == true) { + append(getName(true)) + } + if (comment?.isNotBlank() == true) { + if (isNotEmpty()) { + append(' ') + } + append("($comment)") + } + if (email?.isNotBlank() == true) { + if (isNotEmpty()) { + append(' ') + } + append("<$email>") + } + } + + override val length: Int + get() = full.length + + val name: String? + get() = getName(false) + + fun getName(preserveQuotes: Boolean): String? { + return if (preserveQuotes || _name.isNullOrBlank()) { + _name + } else _name.removeSurrounding("\"") + } + + override fun equals(other: Any?): Boolean { + if (other === null) { + return false + } + if (this === other) { + return true + } + if (other !is UserId) { + return false + } + return isComponentEqual(_name, other._name, false) + && isComponentEqual(comment, other.comment, false) + && isComponentEqual(email, other.email, true) + } + + override fun get(index: Int): Char { + return full[index] + } + + override fun hashCode(): Int { + return toString().hashCode() + } + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return full.subSequence(startIndex, endIndex) + } + + override fun toString(): String { + return full + } + + private fun isComponentEqual(value: String?, otherValue: String?, ignoreCase: Boolean): Boolean = value.equals(otherValue, ignoreCase) + + fun toBuilder() = builder().also { builder -> + if (this._name != null) builder.withName(_name) + if (this.comment != null) builder.withComment(comment) + if (this.email != null) builder.withEmail(email) + } + + companion object { + + // Email regex: https://emailregex.com/ + // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international characters + // \\p{L} = Unicode Letters + // \u0900-\u097F = Hindi Letters + @JvmStatic + private val emailPattern = ("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])").toPattern() + + // User-ID Regex + // "Firstname Lastname (Comment) " + // All groups are optional + // https://www.rfc-editor.org/rfc/rfc5322#page-16 + @JvmStatic + private val nameAddrPattern = "^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$".toPattern() + + /** + * Parse a [UserId] from free-form text,
name-addr
or
mailbox
string and split it + * up into its components. + * Example inputs for this method: + *
    + *
  • john@pgpainless.org
  • + *
  • <john@pgpainless.org>
  • + *
  • John Doe
  • + *
  • John Doe <john@pgpainless.org>
  • + *
  • John Doe (work email) <john@pgpainless.org>
  • + *
+ * In these cases, this method will detect email addresses, names and comments and expose those + * via the respective getters. + * This method does not support parsing mail addresses of the following formats: + *
    + *
  • Local domains without TLDs (
    user@localdomain1
    )
  • + *
  • " "@example.org
    (spaces between the quotes)
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
+ * Note: This method does not guarantee that
string.equals(UserId.parse(string).toString())
is true. + * For example,
UserId.parse("alice@pgpainless.org").toString()
wraps the mail address in angled brackets. + * + * @see RFC5322 §3.4. Address Specification + * @param string user-id + * @return parsed UserId object + */ + @JvmStatic + fun parse(string: String): UserId { + val trimmed = string.trim() + nameAddrPattern.matcher(trimmed).let { nameAddrMatcher -> + if (nameAddrMatcher.find()) { + val name = nameAddrMatcher.group(2) + val comment = nameAddrMatcher.group(4) + val mail = nameAddrMatcher.group(6) + require(emailPattern.matcher(mail).matches()) { "Malformed email address" } + return UserId(name, comment, mail) + } else { + require(emailPattern.matcher(trimmed).matches()) { "Malformed email address" } + return UserId(null, null, trimmed) + } + } + } + + @JvmStatic + fun onlyEmail(email: String) = UserId(null, null, email) + + @JvmStatic + fun nameAndEmail(name: String, email: String) = UserId(name, null, email) + + @JvmStatic + fun compare(u1: UserId?, u2: UserId?, comparator: Comparator) = comparator.compare(u1, u2) + + @JvmStatic + @Deprecated("Deprecated in favor of builde() method.", ReplaceWith("builder()")) + fun newBuilder() = builder() + + @JvmStatic + fun builder() = Builder() + } + + class Builder internal constructor() { + var name: String? = null + var comment: String? = null + var email: String? = null + + fun withName(name: String) = apply { this.name = name } + fun withComment(comment: String) = apply { this.comment = comment} + fun withEmail(email: String) = apply { this.email = email } + + fun noName() = apply { this.name = null } + fun noComment() = apply { this.comment = null } + fun noEmail() = apply { this.email = null } + + fun build() = UserId(name, comment, email) + } + + class DefaultComparator : Comparator { + override fun compare(o1: UserId?, o2: UserId?): Int { + return compareBy { it?._name } + .thenBy { it?.comment } + .thenBy { it?.email } + .compare(o1, o2) + } + } + + class DefaultIgnoreCaseComparator : Comparator { + override fun compare(p0: UserId?, p1: UserId?): Int { + return compareBy { it?._name?.lowercase() } + .thenBy { it?.comment?.lowercase() } + .thenBy { it?.email?.lowercase() } + .compare(p0, p1) + } + + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java index 93cb6922..1290ad9c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/UserIdTest.java @@ -197,15 +197,14 @@ public class UserIdTest { } @Test - public void asStringTest() { - UserId id = UserId.newBuilder() + public void toStringTest() { + UserId id = UserId.builder() .withName("Alice") .withComment("Work Email") .withEmail("alice@pgpainless.org") .build(); - // noinspection deprecation - assertEquals(id.toString(), id.asString()); + assertEquals(id.toString(), id.toString()); } @Test From a6198aadb3664b3379ecac34600adb146489f4e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 14 Sep 2023 16:31:52 +0200 Subject: [PATCH 158/351] Kotlin conversion: RevocationAttributes --- .../key/util/RevocationAttributes.java | 253 ------------------ .../key/util/RevocationAttributes.kt | 155 +++++++++++ 2 files changed, 155 insertions(+), 253 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java deleted file mode 100644 index f84af5d3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/RevocationAttributes.java +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import javax.annotation.Nonnull; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public final class RevocationAttributes { - - /** - * Reason for revocation. - * There are two kinds of reasons: hard and soft reason. - * - * Soft revocation reasons gracefully disable keys or user-ids. - * Softly revoked keys can no longer be used to encrypt data to or to generate signatures. - * Any signature made after a key has been soft revoked is deemed invalid. - * Any signature made before the key has been soft revoked stays valid. - * Soft revoked info can be re-certified at a later point. - * - * Hard revocation reasons on the other hand renders the key or user-id invalid immediately. - * Hard reasons are suitable to use if for example a key got compromised. - * Any signature made before or after a key has been hard revoked is no longer considered valid. - * Hard revoked information can also not be re-certified. - */ - public enum Reason { - /** - * The key or certification is being revoked without a reason. - * This is a HARD revocation reason and cannot be undone. - */ - NO_REASON((byte) 0), - /** - * The key was superseded by another key. - * This is a SOFT revocation reason and can be undone. - */ - KEY_SUPERSEDED((byte) 1), - /** - * The key has potentially been compromised. - * This is a HARD revocation reason and cannot be undone. - */ - KEY_COMPROMISED((byte) 2), - /** - * The key was retired and shall no longer be used. - * This is a SOFT revocation reason can can be undone. - */ - KEY_RETIRED((byte) 3), - /** - * The user-id is no longer valid. - * This is a SOFT revocation reason and can be undone. - */ - USER_ID_NO_LONGER_VALID((byte) 32), - ; - - private static final Map MAP = new ConcurrentHashMap<>(); - static { - for (Reason r : Reason.values()) { - MAP.put(r.reasonCode, r); - } - } - - /** - * Decode a machine-readable reason code. - * - * @param code byte - * @return reason - */ - public static Reason fromCode(byte code) { - Reason reason = MAP.get(code); - if (reason == null) { - throw new IllegalArgumentException("Invalid revocation reason: " + code); - } - return reason; - } - - /** - * Return true if the {@link Reason} the provided code encodes is a hard revocation reason, false - * otherwise. - * Hard revocations cannot be undone, while keys or certifications with soft revocations can be - * re-certified by placing another signature on them. - * - * @param code reason code - * @return is hard - */ - public static boolean isHardRevocation(byte code) { - Reason reason = MAP.get(code); - return reason != KEY_SUPERSEDED && reason != KEY_RETIRED && reason != USER_ID_NO_LONGER_VALID; - } - - /** - * Return true if the given {@link Reason} is a hard revocation, false otherwise. - * Hard revocations cannot be undone, while keys or certifications with soft revocations can be - * re-certified by placing another signature on them. - * - * @param reason reason - * @return is hard - */ - public static boolean isHardRevocation(@Nonnull Reason reason) { - return isHardRevocation(reason.reasonCode); - } - - /** - * Return true if the given {@link Reason} denotes a key revocation. - * @param reason reason - * @return is key revocation - */ - public static boolean isKeyRevocation(@Nonnull Reason reason) { - return isKeyRevocation(reason.code()); - } - - /** - * Return true if the given reason code denotes a key revocation. - * @param code reason code - * @return is key revocation - */ - public static boolean isKeyRevocation(byte code) { - Reason reason = MAP.get(code); - return reason != USER_ID_NO_LONGER_VALID; - } - - private final byte reasonCode; - - Reason(byte reasonCode) { - this.reasonCode = reasonCode; - } - - public byte code() { - return reasonCode; - } - - @Override - public String toString() { - return code() + " - " + name(); - } - } - - public enum RevocationType { - KEY_REVOCATION, - CERT_REVOCATION - } - - private final Reason reason; - private final String description; - - private RevocationAttributes(Reason reason, String description) { - this.reason = reason; - this.description = description; - } - - /** - * Return the machine-readable reason for revocation. - * - * @return reason - */ - public @Nonnull Reason getReason() { - return reason; - } - - /** - * Return the human-readable description for the revocation reason. - * @return description - */ - public @Nonnull String getDescription() { - return description; - } - - /** - * Build a {@link RevocationAttributes} object suitable for key revocations. - * Key revocations are revocations for keys or subkeys. - * - * @return builder - */ - public static WithReason createKeyRevocation() { - return new WithReason(RevocationType.KEY_REVOCATION); - } - - /** - * Build a {@link RevocationAttributes} object suitable for certification (e.g. user-id) revocations. - * - * @return builder - */ - public static WithReason createCertificateRevocation() { - return new WithReason(RevocationType.CERT_REVOCATION); - } - - public static final class WithReason { - - private final RevocationType type; - - private WithReason(RevocationType type) { - this.type = type; - } - - /** - * Set the machine-readable reason. - * Note that depending on whether this is a key-revocation or certification-revocation, - * only certain reason codes are valid. - * Invalid input will result in an {@link IllegalArgumentException} to be thrown. - * - * @param reason reason - * @throws IllegalArgumentException in case of an invalid revocation reason - * @return builder - */ - public WithDescription withReason(Reason reason) { - throwIfReasonTypeMismatch(reason, type); - return new WithDescription(reason); - } - - private void throwIfReasonTypeMismatch(Reason reason, RevocationType type) { - if (type == RevocationType.KEY_REVOCATION) { - if (reason == Reason.USER_ID_NO_LONGER_VALID) { - throw new IllegalArgumentException("Reason " + reason + " can only be used for certificate revocations, not to revoke keys."); - } - } else if (type == RevocationType.CERT_REVOCATION) { - switch (reason) { - case KEY_SUPERSEDED: - case KEY_COMPROMISED: - case KEY_RETIRED: - throw new IllegalArgumentException("Reason " + reason + " can only be used for key revocations, not to revoke certificates."); - } - } - } - - } - - public static final class WithDescription { - - private final Reason reason; - - private WithDescription(Reason reason) { - this.reason = reason; - } - - /** - * Set a human-readable description of the revocation reason. - * - * @param description description - * @return revocation attributes - */ - public RevocationAttributes withDescription(@Nonnull String description) { - return new RevocationAttributes(reason, description); - } - - /** - * Set an empty human-readable description. - * @return revocation attributes - */ - public RevocationAttributes withoutDescription() { - return withDescription(""); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt new file mode 100644 index 00000000..39f69fbe --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +class RevocationAttributes( + val reason: Reason, + val description: String) { + + /** + * Reason for revocation. + * There are two kinds of reasons: hard and soft reason. + * + * Soft revocation reasons gracefully disable keys or user-ids. + * Softly revoked keys can no longer be used to encrypt data to or to generate signatures. + * Any signature made after a key has been soft revoked is deemed invalid. + * Any signature made before the key has been soft revoked stays valid. + * Soft revoked info can be re-certified at a later point. + * + * Hard revocation reasons on the other hand renders the key or user-id invalid immediately. + * Hard reasons are suitable to use if for example a key got compromised. + * Any signature made before or after a key has been hard revoked is no longer considered valid. + * Hard revoked information can also not be re-certified. + */ + enum class Reason(val code: Byte) { + /** + * The key or certification is being revoked without a reason. + * This is a HARD revocation reason and cannot be undone. + */ + NO_REASON(0), + /** + * The key was superseded by another key. + * This is a SOFT revocation reason and can be undone. + */ + KEY_SUPERSEDED(1), + /** + * The key has potentially been compromised. + * This is a HARD revocation reason and cannot be undone. + */ + KEY_COMPROMISED(2), + /** + * The key was retired and shall no longer be used. + * This is a SOFT revocation reason can can be undone. + */ + KEY_RETIRED(3), + /** + * The user-id is no longer valid. + * This is a SOFT revocation reason and can be undone. + */ + USER_ID_NO_LONGER_VALID(32), + ; + + fun code() = code + + override fun toString(): String { + return "$code - $name" + } + + companion object { + + @JvmStatic + private val MAP = values().associateBy { it.code } + + /** + * Decode a machine-readable reason code. + * + * @param code byte + * @return reason + */ + @JvmStatic + fun fromCode(code: Byte) = MAP[code] ?: throw IllegalArgumentException("Invalid revocation reason: $code") + + /** + * Return true if the [Reason] the provided code encodes is a hard revocation reason, false + * otherwise. + * Hard revocations cannot be undone, while keys or certifications with soft revocations can be + * re-certified by placing another signature on them. + * + * @param code reason code + * @return is hard + */ + @JvmStatic + fun isHardRevocation(code: Byte) = MAP[code]?.let { isHardRevocation(it) } ?: true + + /** + * Return true if the given [Reason] is a hard revocation, false otherwise. + * Hard revocations cannot be undone, while keys or certifications with soft revocations can be + * re-certified by placing another signature on them. + * + * @param reason reason + * @return is hard + */ + @JvmStatic + fun isHardRevocation(reason: Reason) = when (reason) { + KEY_SUPERSEDED, KEY_RETIRED, USER_ID_NO_LONGER_VALID -> false + else -> true + } + + /** + * Return true if the given reason code denotes a key revocation. + * @param code reason code + * @return is key revocation + */ + @JvmStatic + fun isKeyRevocation(code: Byte) = MAP[code]?.let { isKeyRevocation(it) } ?: false + + /** + * Return true if the given [Reason] denotes a key revocation. + * @param reason reason + * @return is key revocation + */ + @JvmStatic + fun isKeyRevocation(reason: Reason) = when (reason) { + USER_ID_NO_LONGER_VALID -> false + else -> true + } + } + } + + enum class RevocationType { + KEY_REVOCATION, + CERT_REVOCATION + } + + companion object { + @JvmStatic + fun createKeyRevocation() = WithReason(RevocationType.KEY_REVOCATION) + + @JvmStatic + fun createCertificateRevocation() = WithReason(RevocationType.CERT_REVOCATION) + } + + class WithReason(val type: RevocationType) { + + fun withReason(reason: Reason): WithDescription { + require(reasonTypeMatches(reason, type)) { + "Reason $reason can only be used for ${if (type == RevocationType.KEY_REVOCATION) "certificate" else "key"} revocations." + } + return WithDescription(reason) + } + + private fun reasonTypeMatches(reason: Reason, type: RevocationType): Boolean { + return when (type) { + RevocationType.KEY_REVOCATION -> reason != Reason.USER_ID_NO_LONGER_VALID + RevocationType.CERT_REVOCATION -> reason == Reason.USER_ID_NO_LONGER_VALID || reason == Reason.NO_REASON + } + } + } + + class WithDescription(val reason: Reason) { + fun withDescription(description: String): RevocationAttributes = RevocationAttributes(reason, description) + fun withoutDescription() = RevocationAttributes(reason, "") + } +} \ No newline at end of file From 9ee29f7a53c38ad0bff94ab9c5cc09208012247e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Sep 2023 13:09:10 +0200 Subject: [PATCH 159/351] Kotlin conversion: IntegrityProtectedInputStream --- .../IntegrityProtectedInputStream.java | 60 ------------------- .../IntegrityProtectedInputStream.kt | 42 +++++++++++++ 2 files changed, 42 insertions(+), 60 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java deleted file mode 100644 index 37dcfca4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPException; -import org.pgpainless.exception.ModificationDetectionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class IntegrityProtectedInputStream extends InputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); - - private final InputStream inputStream; - private final PGPEncryptedData encryptedData; - private final ConsumerOptions options; - private boolean closed = false; - - public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { - this.inputStream = inputStream; - this.encryptedData = encryptedData; - this.options = options; - } - - @Override - public int read() throws IOException { - return inputStream.read(); - } - - @Override - public int read(@Nonnull byte[] b, int offset, int length) throws IOException { - return inputStream.read(b, offset, length); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - closed = true; - - if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - try { - if (!encryptedData.verify()) { - throw new ModificationDetectionException(); - } - LOGGER.debug("Integrity Protection check passed"); - } catch (PGPException e) { - throw new IOException("Data appears to not be integrity protected.", e); - } - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt new file mode 100644 index 00000000..a1e095f8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.PGPEncryptedData +import org.bouncycastle.openpgp.PGPException +import org.pgpainless.exception.ModificationDetectionException +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.InputStream + +class IntegrityProtectedInputStream( + private val inputStream: InputStream, + private val encryptedData: PGPEncryptedData, + private val options: ConsumerOptions +) : InputStream() { + private var closed: Boolean = false + + override fun read() = inputStream.read() + override fun read(b: ByteArray, off: Int, len: Int) = inputStream.read(b, off, len) + override fun close() { + if (closed) return + + closed = true + if (encryptedData.isIntegrityProtected && !options.isIgnoreMDCErrors()) { + try { + if (!encryptedData.verify()) throw ModificationDetectionException() + LOGGER.debug("Integrity Protection check passed.") + } catch (e : PGPException) { + throw IOException("Data appears to not be integrity protected.", e) + } + } + } + + companion object { + @JvmStatic + val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java) + } +} \ No newline at end of file From ea57c4aec0fe186d69c802dbb1e54c0f410571c1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 16 Sep 2023 14:22:25 +0200 Subject: [PATCH 160/351] Kotlin conversion: EncryptionStream --- .../encryption_signing/EncryptionStream.java | 313 ------------------ .../encryption_signing/EncryptionStream.kt | 251 ++++++++++++++ 2 files changed, 251 insertions(+), 313 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt 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 deleted file mode 100644 index 7af5a7b3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ /dev/null @@ -1,313 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.BufferedOutputStream; -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.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.BCPGOutputStream; -import org.bouncycastle.openpgp.PGPCompressedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralDataGenerator; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both, - * depending on its configuration. - * - * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. - * @see Source - */ -public final class EncryptionStream extends OutputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionStream.class); - - private final ProducerOptions options; - private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder(); - - private boolean closed = false; - // 1 << 8 causes wrong partial body length encoding - // 1 << 9 fixes this. - // see https://github.com/pgpainless/pgpainless/issues/160 - private static final int BUFFER_SIZE = 1 << 9; - - OutputStream outermostStream; - OutputStream signatureLayerStream; - - 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 ProducerOptions options) - throws IOException, PGPException { - this.options = options; - outermostStream = targetOutputStream; - - prepareArmor(); - prepareEncryption(); - prepareCompression(); - prepareOnePassSignatures(); - prepareLiteralDataProcessing(); - prepareSigningStream(); - prepareInputEncoding(); - } - - private void prepareArmor() { - if (!options.isAsciiArmor()) { - LOGGER.debug("Output will be unarmored"); - return; - } - - // ArmoredOutputStream better be buffered - outermostStream = new BufferedOutputStream(outermostStream); - - LOGGER.debug("Wrap encryption output in ASCII armor"); - armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options); - outermostStream = armorOutputStream; - } - - private void prepareEncryption() throws IOException, PGPException { - EncryptionOptions encryptionOptions = options.getEncryptionOptions(); - if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) { - // No encryption options/methods -> no encryption - resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL); - return; - } - - SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions); - resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm); - LOGGER.debug("Encrypt message using {}", encryptionAlgorithm); - PGPDataEncryptorBuilder dataEncryptorBuilder = - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm); - dataEncryptorBuilder.setWithIntegrityPacket(true); - - PGPEncryptedDataGenerator encryptedDataGenerator = - new PGPEncryptedDataGenerator(dataEncryptorBuilder); - for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) { - encryptedDataGenerator.addMethod(encryptionMethod); - } - - for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) { - resultBuilder.addRecipient(recipientSubkeyIdentifier); - } - - publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); - outermostStream = publicKeyEncryptedStream; - } - - private void prepareCompression() throws IOException { - CompressionAlgorithm compressionAlgorithm = EncryptionBuilder.negotiateCompressionAlgorithm(options); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - compressedDataGenerator = new PGPCompressedDataGenerator( - compressionAlgorithm.getAlgorithmId()); - if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) { - return; - } - - LOGGER.debug("Compress using {}", compressionAlgorithm); - basicCompressionStream = new BCPGOutputStream(compressedDataGenerator.open(outermostStream)); - outermostStream = basicCompressionStream; - } - - private void prepareOnePassSignatures() throws IOException, PGPException { - signatureLayerStream = outermostStream; - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - // No singing options/methods -> no signing - return; - } - - int sigIndex = 0; - for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) { - sigIndex++; - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier); - - if (!signingMethod.isDetached()) { - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - // The last sig is not nested, all others are - boolean nested = sigIndex != signingOptions.getSigningMethods().size(); - signatureGenerator.generateOnePassVersion(nested).encode(outermostStream); - } - } - } - - private void prepareLiteralDataProcessing() throws IOException { - if (options.isCleartextSigned()) { - int[] algorithmIds = collectHashAlgorithmsForCleartextSigning(); - armorOutputStream.beginClearText(algorithmIds); - return; - } - - literalDataGenerator = new PGPLiteralDataGenerator(); - literalDataStream = literalDataGenerator.open(outermostStream, options.getEncoding().getCode(), - options.getFileName(), options.getModificationDate(), new byte[BUFFER_SIZE]); - outermostStream = literalDataStream; - - resultBuilder.setFileName(options.getFileName()) - .setModificationDate(options.getModificationDate()) - .setFileEncoding(options.getEncoding()); - } - - public void prepareSigningStream() { - outermostStream = new SignatureGenerationStream(outermostStream, options.getSigningOptions()); - } - - public void prepareInputEncoding() { - // By buffering here, we drastically improve performance - // Reason is that CRLFGeneratorStream only implements write(int), so we need BufferedOutputStream to - // "convert" to write(buf) calls again - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outermostStream); - CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(bufferedOutputStream, - options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY); - outermostStream = crlfGeneratorStream; - } - - private int[] collectHashAlgorithmsForCleartextSigning() { - SigningOptions signOpts = options.getSigningOptions(); - Set hashAlgorithms = new HashSet<>(); - if (signOpts != null) { - for (SigningOptions.SigningMethod method : signOpts.getSigningMethods().values()) { - hashAlgorithms.add(method.getHashAlgorithm()); - } - } - - int[] algorithmIds = new int[hashAlgorithms.size()]; - Iterator iterator = hashAlgorithms.iterator(); - for (int i = 0; i < algorithmIds.length; i++) { - algorithmIds[i] = iterator.next().getAlgorithmId(); - } - - return algorithmIds; - } - - @Override - public void write(int data) throws IOException { - outermostStream.write(data); - } - - @Override - public void write(@Nonnull byte[] buffer) throws IOException { - write(buffer, 0, buffer.length); - } - - - @Override - public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { - outermostStream.write(buffer, 0, len); - } - - @Override - public void flush() throws IOException { - outermostStream.flush(); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - - outermostStream.close(); - - // Literal Data - if (literalDataStream != null) { - literalDataStream.flush(); - literalDataStream.close(); - } - if (literalDataGenerator != null) { - literalDataGenerator.close(); - } - - if (options.isCleartextSigned()) { - // Add linebreak between body and signatures - // TODO: We should only add this line if required. - // I.e. if the message already ends with \n, don't add another linebreak. - armorOutputStream.write('\r'); - armorOutputStream.write('\n'); - armorOutputStream.endClearText(); - } - - try { - writeSignatures(); - } catch (PGPException e) { - throw new IOException("Exception while writing signatures.", e); - } - - // Compressed Data - compressedDataGenerator.close(); - - // Public Key Encryption - if (publicKeyEncryptedStream != null) { - publicKeyEncryptedStream.flush(); - publicKeyEncryptedStream.close(); - } - - // Armor - if (armorOutputStream != null) { - armorOutputStream.flush(); - armorOutputStream.close(); - } - closed = true; - } - - private void writeSignatures() throws PGPException, IOException { - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - - // One-Pass-Signatures are bracketed. That means we have to append the signatures in reverse order - // compared to the one-pass-signature packets. - List signingKeys = new ArrayList<>(signingOptions.getSigningMethods().keySet()); - for (int i = signingKeys.size() - 1; i >= 0; i--) { - SubkeyIdentifier signingKey = signingKeys.get(i); - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - PGPSignature signature = signatureGenerator.generate(); - if (signingMethod.isDetached()) { - resultBuilder.addDetachedSignature(signingKey, signature); - } - if (!signingMethod.isDetached() || options.isCleartextSigned()) { - signature.encode(signatureLayerStream); - } - } - } - - public EncryptionResult getResult() { - if (!closed) { - throw new IllegalStateException("EncryptionStream must be closed before accessing the Result."); - } - return resultBuilder.build(); - } - - public boolean isClosed() { - return closed; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt new file mode 100644 index 00000000..50397905 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.BCPGOutputStream +import org.bouncycastle.openpgp.PGPCompressedDataGenerator +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPLiteralDataGenerator +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.slf4j.LoggerFactory +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.OutputStream + +// 1 << 8 causes wrong partial body length encoding +// 1 << 9 fixes this. +// see https://github.com/pgpainless/pgpainless/issues/160 +const val BUFFER_SIZE = 1 shl 9 + +/** + * OutputStream that produces an OpenPGP message. The message can be encrypted, signed, or both, + * depending on its configuration. + * + * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. + * @see Source + */ +class EncryptionStream( + private var outermostStream: OutputStream, + private val options: ProducerOptions, +) : OutputStream() { + + private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() + private var closed: Boolean = false + + private var signatureLayerStream: OutputStream? = null + private var armorOutputStream: ArmoredOutputStream? = null + private var publicKeyEncryptedStream: OutputStream? = null + private var compressedDataGenerator: PGPCompressedDataGenerator? = null + private var basicCompressionStream: BCPGOutputStream? = null + private var literalDataGenerator: PGPLiteralDataGenerator? = null + private var literalDataStream: OutputStream? = null + + init { + prepareArmor() + prepareEncryption() + prepareCompression() + prepareOnePassSignatures() + prepareLiteralDataProcessing() + prepareSigningStream() + prepareInputEncoding() + } + + private fun prepareArmor() { + if (!options.isAsciiArmor) { + LOGGER.debug("Output will be unarmored.") + return + } + + outermostStream = BufferedOutputStream(outermostStream) + LOGGER.debug("Wrap encryption output in ASCII armor.") + armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options) + .also { outermostStream = it } + } + + @Throws(IOException::class, PGPException::class) + private fun prepareEncryption() { + if (options.encryptionOptions == null) { + // No encryption options -> no encryption + resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL) + return + } + require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { + "If EncryptionOptions are provided, at least one encryption method MUST be provided as well." + } + + EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { + resultBuilder.setEncryptionAlgorithm(it) + LOGGER.debug("Encrypt message using symmetric algorithm $it.") + val encryptedDataGenerator = PGPEncryptedDataGenerator( + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it) + .apply { setWithIntegrityPacket(true) }) + options.encryptionOptions.encryptionMethods.forEach { m -> + encryptedDataGenerator.addMethod(m) + } + options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> + resultBuilder.addRecipient(r) + } + + publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)) + .also { stream -> outermostStream = stream } + } + } + + @Throws(IOException::class) + private fun prepareCompression() { + EncryptionBuilder.negotiateCompressionAlgorithm(options).let { + resultBuilder.setCompressionAlgorithm(it) + compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId) + if (it == CompressionAlgorithm.UNCOMPRESSED) return + + LOGGER.debug("Compress using $it.") + basicCompressionStream = BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)) + .also { stream -> outermostStream = stream } + } + } + + @Throws(IOException::class, PGPException::class) + private fun prepareOnePassSignatures() { + signatureLayerStream = outermostStream + if (options.signingOptions == null) { + return + } + require(options.signingOptions.signingMethods.isNotEmpty()) { + "If SigningOptions are provided, at least one SigningMethod MUST be provided." + } + for ((index, method) in options.signingOptions.signingMethods.values.withIndex()) { + if (!method.isDetached) { + // The last sig is not nested, all others are + val nested = index + 1 < options.signingOptions.signingMethods.size + method.signatureGenerator.generateOnePassVersion(nested).encode(outermostStream) + } + } + } + + @Throws(IOException::class) + private fun prepareLiteralDataProcessing() { + if (options.isCleartextSigned) { + val hashAlgorithms = collectHashAlgorithmsForCleartextSigning() + armorOutputStream!!.beginClearText(*hashAlgorithms.toIntArray()) + return + } + + literalDataGenerator = PGPLiteralDataGenerator().also { gen -> + literalDataStream = gen.open(outermostStream, options.encoding.code, options.fileName, + options.modificationDate, ByteArray(BUFFER_SIZE)).also { stream -> + outermostStream = stream + } + } + resultBuilder.apply { + setFileName(options.fileName) + setModificationDate(options.modificationDate) + setFileEncoding(options.encoding) + } + } + + private fun prepareSigningStream() { + outermostStream = SignatureGenerationStream(outermostStream, options.signingOptions) + } + + private fun prepareInputEncoding() { + outermostStream = CRLFGeneratorStream( + // By buffering here, we drastically improve performance + // Reason is that CRLFGeneratorStream only implements write(int), so we need BufferedOutputStream to + // "convert" to write(buf) calls again + BufferedOutputStream(outermostStream), + if (options.isApplyCRLFEncoding) StreamEncoding.UTF8 else StreamEncoding.BINARY) + } + + private fun collectHashAlgorithmsForCleartextSigning(): Array { + return options.signingOptions?.signingMethods?.values + ?.map { it.hashAlgorithm }?.toSet() + ?.map { it.algorithmId }?.toTypedArray() + ?: arrayOf() + } + + @Throws(IOException::class) + override fun write(data: Int) = outermostStream.write(data) + + @Throws(IOException::class) + override fun write(buffer: ByteArray) = write(buffer, 0, buffer.size) + + @Throws(IOException::class) + override fun write(buffer: ByteArray, off: Int, len: Int) = outermostStream.write(buffer, off, len) + + @Throws(IOException::class) + override fun flush() = outermostStream.flush() + + @Throws(IOException::class) + override fun close() { + if (closed) return + + outermostStream.close() + literalDataStream?.apply { flush(); close() } + literalDataGenerator?.close() + + if (options.isCleartextSigned) { + armorOutputStream?.apply { + write('\r'.code) + write('\n'.code) + endClearText() + } + } + + try { + writeSignatures() + } catch (e : PGPException) { + throw IOException("Exception while writing signatures.", e) + } + + compressedDataGenerator?.close() + + publicKeyEncryptedStream?.apply { + flush() + close() + } + + armorOutputStream?.apply { + flush() + close() + } + closed = true + } + + @Throws(PGPException::class, IOException::class) + private fun writeSignatures() { + if (options.signingOptions == null) { + return + } + + options.signingOptions.signingMethods.entries.reversed().forEach { (key, method) -> + method.signatureGenerator.generate().let { sig -> + if (method.isDetached) { + resultBuilder.addDetachedSignature(key, sig) + } + if (!method.isDetached || options.isCleartextSigned) { + sig.encode(signatureLayerStream) + } + } + } + } + + val result: EncryptionResult + get() = check(closed) { "EncryptionStream must be closed before accessing the result." } + .let { resultBuilder.build() } + + val isClosed + get() = closed + + companion object { + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) + } +} \ No newline at end of file From befb1c8c0fc45e548dc5ebf232f15890255f0545 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Sep 2023 13:18:48 +0200 Subject: [PATCH 161/351] Kotlin conversion: MessageInspector --- .../MessageInspector.java | 146 ------------------ .../MessageInspector.kt | 108 +++++++++++++ 2 files changed, 108 insertions(+), 146 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java deleted file mode 100644 index 3dda0f5d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageInspector.java +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPUtil; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmorUtils; - -/** - * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected. - */ -public final class MessageInspector { - - public static class EncryptionInfo { - private final List keyIds = new ArrayList<>(); - private boolean isPassphraseEncrypted = false; - private boolean isSignedOnly = false; - - /** - * Return a list of recipient key ids for whom the message is encrypted. - * @return recipient key ids - */ - public List getKeyIds() { - return Collections.unmodifiableList(keyIds); - } - - public boolean isPassphraseEncrypted() { - return isPassphraseEncrypted; - } - - /** - * Return true, if the message is encrypted. - * - * @return true if encrypted - */ - public boolean isEncrypted() { - return isPassphraseEncrypted || !keyIds.isEmpty(); - } - - /** - * Return true, if the message is not encrypted, but signed using {@link org.bouncycastle.openpgp.PGPOnePassSignature OnePassSignatures}. - * - * @return true if message is signed only - */ - public boolean isSignedOnly() { - return isSignedOnly; - } - } - - private MessageInspector() { - - } - - /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. - * - * @param message OpenPGP message - * @return encryption info - * - * @throws PGPException in case the message is broken - * @throws IOException in case of an IO error - */ - public static EncryptionInfo determineEncryptionInfoForMessage(String message) throws PGPException, IOException { - @SuppressWarnings("CharsetObjectCanBeUsed") - Charset charset = Charset.forName("UTF-8"); - return determineEncryptionInfoForMessage(new ByteArrayInputStream(message.getBytes(charset))); - } - - /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. - * Note: This method does not rewind the passed in Stream, so you might need to take care of that yourselves. - * - * @param dataIn openpgp message - * @return encryption information - * - * @throws IOException in case of an IO error - * @throws PGPException if the message is broken - */ - public static EncryptionInfo determineEncryptionInfoForMessage(InputStream dataIn) throws IOException, PGPException { - InputStream decoded = ArmorUtils.getDecoderStream(dataIn); - EncryptionInfo info = new EncryptionInfo(); - - processMessage(decoded, info); - - return info; - } - - private static void processMessage(InputStream dataIn, EncryptionInfo info) throws PGPException, IOException { - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(dataIn); - - Object next; - while ((next = objectFactory.nextObject()) != null) { - if (next instanceof PGPOnePassSignatureList) { - PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) next; - if (!signatures.isEmpty()) { - info.isSignedOnly = true; - return; - } - } - - if (next instanceof PGPEncryptedDataList) { - PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) next; - for (PGPEncryptedData encryptedData : encryptedDataList) { - if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pubKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; - info.keyIds.add(pubKeyEncryptedData.getKeyID()); - } else if (encryptedData instanceof PGPPBEEncryptedData) { - info.isPassphraseEncrypted = true; - } - } - // Data is encrypted, we cannot go deeper - return; - } - - if (next instanceof PGPCompressedData) { - PGPCompressedData compressed = (PGPCompressedData) next; - InputStream decompressed = compressed.getDataStream(); - InputStream decoded = PGPUtil.getDecoderStream(decompressed); - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoded); - } - - if (next instanceof PGPLiteralData) { - return; - } - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt new file mode 100644 index 00000000..64b2a5f3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmorUtils +import java.io.IOException +import java.io.InputStream + +/** + * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected. + */ +class MessageInspector { + + /** + * Info about an OpenPGP message. + * + * @param keyIds List of recipient key ids for whom the message is encrypted. + * @param isPassphraseEncrypted true, if the message is encrypted for a passphrase + * @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures + */ + data class EncryptionInfo( + val keyIds: List, + val isPassphraseEncrypted: Boolean, + val isSignedOnly: Boolean) { + + val isEncrypted: Boolean + get() = isPassphraseEncrypted || keyIds.isNotEmpty() + } + + companion object { + + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. + * + * @param message OpenPGP message + * @return encryption info + * + * @throws PGPException in case the message is broken + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = determineEncryptionInfoForMessage(message.byteInputStream()) + + /** + * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. + * Note: This method does not rewind the passed in Stream, so you might need to take care of that yourselves. + * + * @param inputStream openpgp message + * @return encryption information + * + * @throws IOException in case of an IO error + * @throws PGPException if the message is broken + */ + @JvmStatic + @Throws(PGPException::class, IOException::class) + fun determineEncryptionInfoForMessage(inputStream: InputStream): EncryptionInfo { + return processMessage(ArmorUtils.getDecoderStream(inputStream)) + } + + @JvmStatic + @Throws(PGPException::class, IOException::class) + private fun processMessage(inputStream: InputStream): EncryptionInfo { + var objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(inputStream) + + var n: Any? + while (objectFactory.nextObject().also { n = it } != null) { + when (val next = n!!) { + + is PGPOnePassSignatureList -> { + if (!next.isEmpty) { + return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = true) + } + } + + is PGPEncryptedDataList -> { + var isPassphraseEncrypted = false + val keyIds = mutableListOf() + for (encryptedData in next) { + if (encryptedData is PGPPublicKeyEncryptedData) { + keyIds.add(encryptedData.keyID) + } else if (encryptedData is PGPPBEEncryptedData) { + isPassphraseEncrypted = true + } + } + // Data is encrypted, we cannot go deeper + return EncryptionInfo(keyIds, isPassphraseEncrypted, false) + } + + is PGPCompressedData -> { + objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( + PGPUtil.getDecoderStream(next.dataStream)) + continue + } + + is PGPLiteralData -> { + break + } + } + } + return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) + } + } +} \ No newline at end of file From 0fa09065cfce21ff5339e5a74f7d6cff790878c6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Sep 2023 13:37:15 +0200 Subject: [PATCH 162/351] Kotlin conversion: TeeBCPGInputStream --- .../TeeBCPGInputStream.java | 159 ------------------ .../TeeBCPGInputStream.kt | 136 +++++++++++++++ 2 files changed, 136 insertions(+), 159 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java deleted file mode 100644 index 725c6f6e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.MarkerPacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.OpenPgpPacket; - -import javax.annotation.Nonnull; - -/** - * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. - * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since - * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and - * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. - * - * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying - * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, - * we need to delay teeing out that byte to signature verifiers. - * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using - * {@link DelayedTeeInputStream#squeeze()}. - */ -public class TeeBCPGInputStream { - - protected final DelayedTeeInputStream delayedTee; - // InputStream of OpenPGP packets of the current layer - protected final BCPGInputStream packetInputStream; - - public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { - this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); - this.packetInputStream = BCPGInputStream.wrap(delayedTee); - } - - public OpenPgpPacket nextPacketTag() throws IOException { - int tag = packetInputStream.nextPacketTag(); - if (tag == -1) { - return null; - } - - return OpenPgpPacket.requireFromTag(tag); - } - - public Packet readPacket() throws IOException { - return packetInputStream.readPacket(); - } - - public PGPCompressedData readCompressedData() throws IOException { - delayedTee.squeeze(); - PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); - return compressedData; - } - - public PGPLiteralData readLiteralData() throws IOException { - delayedTee.squeeze(); - return new PGPLiteralData(packetInputStream); - } - - public PGPEncryptedDataList readEncryptedDataList() throws IOException { - delayedTee.squeeze(); - return new PGPEncryptedDataList(packetInputStream); - } - - public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { - PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); - delayedTee.squeeze(); - return onePassSignature; - } - - public PGPSignature readSignature() throws PGPException, IOException { - PGPSignature signature = new PGPSignature(packetInputStream); - delayedTee.squeeze(); - return signature; - } - - public MarkerPacket readMarker() throws IOException { - MarkerPacket markerPacket = (MarkerPacket) readPacket(); - delayedTee.squeeze(); - return markerPacket; - } - - public void close() throws IOException { - this.packetInputStream.close(); - } - - public static class DelayedTeeInputStream extends InputStream { - - private int last = -1; - private final InputStream inputStream; - private final OutputStream outputStream; - - public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { - this.inputStream = inputStream; - this.outputStream = outputStream; - } - - @Override - public int read() throws IOException { - if (last != -1) { - outputStream.write(last); - } - try { - last = inputStream.read(); - return last; - } catch (IOException e) { - if (e.getMessage().contains("crc check failed in armored message")) { - throw e; - } - return -1; - } - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - if (last != -1) { - outputStream.write(last); - } - - int r = inputStream.read(b, off, len); - if (r > 0) { - outputStream.write(b, off, r - 1); - last = b[off + r - 1]; - } else { - last = -1; - } - return r; - } - - /** - * Squeeze the last byte out and update the output stream. - * - * @throws IOException in case of an IO error - */ - public void squeeze() throws IOException { - if (last != -1) { - outputStream.write(last); - } - last = -1; - } - - @Override - public void close() throws IOException { - inputStream.close(); - outputStream.close(); - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt new file mode 100644 index 00000000..f6c5a454 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.MarkerPacket +import org.bouncycastle.bcpg.Packet +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPEncryptedDataList +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.OpenPgpPacket +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. + * Unfortunately we cannot simply override [BCPGInputStream.read] to tee the data out though, since + * [BCPGInputStream.readPacket] inconsistently calls a mix of [BCPGInputStream.read] and + * [InputStream.read] of the underlying stream. This would cause the second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an [DelayedTeeInputStream] which wraps the underlying + * stream. Since calling [BCPGInputStream.nextPacketTag] reads up to and including the next packets tag, + * we need to delay teeing out that byte to signature verifiers. + * Hence, the reading methods of the [TeeBCPGInputStream] handle pushing this byte to the output stream using + * [DelayedTeeInputStream.squeeze]. + */ +class TeeBCPGInputStream( + inputStream: BCPGInputStream, + outputStream: OutputStream) { + + private val delayedTee: DelayedTeeInputStream + private val packetInputStream: BCPGInputStream + + init { + delayedTee = DelayedTeeInputStream(inputStream, outputStream) + packetInputStream = BCPGInputStream(delayedTee) + } + + fun nextPacketTag(): OpenPgpPacket? { + return packetInputStream.nextPacketTag().let { + if (it == -1) null + else OpenPgpPacket.requireFromTag(it) + } + } + + fun readPacket(): Packet = packetInputStream.readPacket() + + fun readCompressedData(): PGPCompressedData { + delayedTee.squeeze() + return PGPCompressedData(packetInputStream) + } + + fun readLiteralData(): PGPLiteralData { + delayedTee.squeeze() + return PGPLiteralData(packetInputStream) + } + + fun readEncryptedDataList(): PGPEncryptedDataList { + delayedTee.squeeze() + return PGPEncryptedDataList(packetInputStream) + } + + fun readOnePassSignature(): PGPOnePassSignature { + return PGPOnePassSignature(packetInputStream).also { delayedTee.squeeze() } + } + + fun readSignature(): PGPSignature { + return PGPSignature(packetInputStream).also { delayedTee.squeeze() } + } + + fun readMarker(): MarkerPacket { + return (readPacket() as MarkerPacket).also { delayedTee.squeeze() } + } + + fun close() { + packetInputStream.close() + } + + class DelayedTeeInputStream( + private val inputStream: InputStream, + private val outputStream: OutputStream + ) : InputStream() { + private var last: Int = -1 + + override fun read(): Int { + if (last != -1) { + outputStream.write(last) + } + return try { + last = inputStream.read() + last + } catch (e : IOException) { + if (e.message?.contains("crc check failed in armored message") == true) { + throw e + } + -1 + } + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (last != -1) { + outputStream.write(last) + } + + inputStream.read(b, off, len).let { r -> + last = if (r > 0) { + outputStream.write(b, off, r - 1) + b[off + r - 1].toInt() + } else { + -1 + } + return r + } + } + + /** + * Squeeze the last byte out and update the output stream. + */ + fun squeeze() { + if (last != -1) { + outputStream.write(last) + } + last = -1 + } + + override fun close() { + inputStream.close() + outputStream.close() + } + } +} \ No newline at end of file From 068aa0ec278d89e90c6c4153de0fa3f4462f6c73 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Sep 2023 13:50:50 +0200 Subject: [PATCH 163/351] Kotlin conversion: SignatureGenerationStream --- .../SignatureGenerationStream.java | 65 ------------------- .../SignatureGenerationStream.kt | 39 +++++++++++ 2 files changed, 39 insertions(+), 65 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java deleted file mode 100644 index 7a96f2e1..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.key.SubkeyIdentifier; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.OutputStream; - -/** - * OutputStream which has the task of updating signature generators for written data. - */ -class SignatureGenerationStream extends OutputStream { - - private final OutputStream wrapped; - private final SigningOptions options; - - SignatureGenerationStream(@Nonnull OutputStream wrapped, @Nullable SigningOptions signingOptions) { - this.wrapped = wrapped; - this.options = signingOptions; - } - - @Override - public void write(int b) throws IOException { - wrapped.write(b); - if (options == null || options.getSigningMethods().isEmpty()) { - return; - } - - for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - byte asByte = (byte) (b & 0xff); - signatureGenerator.update(asByte); - } - } - - @Override - public void write(@Nonnull byte[] buffer) throws IOException { - write(buffer, 0, buffer.length); - } - - @Override - public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { - wrapped.write(buffer, 0, len); - if (options == null || options.getSigningMethods().isEmpty()) { - return; - } - for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - signatureGenerator.update(buffer, 0, len); - } - } - - @Override - public void close() throws IOException { - wrapped.close(); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt new file mode 100644 index 00000000..d0f04851 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import java.io.OutputStream + +/** + * OutputStream which has the task of updating signature generators for written data. + */ +class SignatureGenerationStream( + private val wrapped: OutputStream, + private val options: SigningOptions? +) : OutputStream() { + + override fun close() = wrapped.close() + override fun flush() = wrapped.flush() + + override fun write(b: Int) { + wrapped.write(b) + options?.run { + signingMethods.values.forEach { + it.signatureGenerator.update((b and 0xff).toByte()) + } + } + } + + override fun write(b: ByteArray) = write(b, 0, b.size) + + override fun write(b: ByteArray, off: Int, len: Int) { + wrapped.write(b, off, len) + options?.run { + signingMethods.values.forEach { + it.signatureGenerator.update(b, off, len) + } + } + } +} \ No newline at end of file From a50be47fa4f9d9294747c8af7e5542e877f6beba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 20 Sep 2023 13:57:16 +0200 Subject: [PATCH 164/351] Kotlin conversion: CRLFGeneratorStream --- .../CRLFGeneratorStream.java | 55 ------------------- .../encryption_signing/CRLFGeneratorStream.kt | 52 ++++++++++++++++++ 2 files changed, 52 insertions(+), 55 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java deleted file mode 100644 index 4cab8be3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/CRLFGeneratorStream.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 David Hook -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import org.pgpainless.algorithm.StreamEncoding; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} which applies CR-LF encoding of its input data, based on the desired {@link StreamEncoding}. - * This implementation originates from the Bouncy Castle library. - */ -public class CRLFGeneratorStream extends OutputStream { - - protected final OutputStream crlfOut; - private final boolean isBinary; - private int lastB = 0; - - public CRLFGeneratorStream(OutputStream crlfOut, StreamEncoding encoding) { - this.crlfOut = crlfOut; - this.isBinary = encoding == StreamEncoding.BINARY; - } - - public void write(int b) throws IOException { - if (!isBinary) { - if (b == '\n' && lastB != '\r') { // Unix - crlfOut.write('\r'); - } else if (lastB == '\r') { // MAC - if (b != '\n') { - crlfOut.write('\n'); - } - } - lastB = b; - } - - crlfOut.write(b); - } - - public void close() throws IOException { - if (!isBinary && lastB == '\r') { // MAC - crlfOut.write('\n'); - } - crlfOut.close(); - } - - @Override - public void flush() throws IOException { - super.flush(); - crlfOut.flush(); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt new file mode 100644 index 00000000..8735d7b1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2021 David Hook +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.pgpainless.algorithm.StreamEncoding +import java.io.OutputStream + +/** + * [OutputStream] which applies CR-LF encoding of its input data, based on the desired [StreamEncoding]. + * This implementation originates from the Bouncy Castle library. + */ +class CRLFGeneratorStream( + private val crlfOut: OutputStream, + encoding: StreamEncoding +) : OutputStream() { + + private val isBinary: Boolean + private var lastB = 0 + + init { + isBinary = encoding == StreamEncoding.BINARY + } + + override fun write(b: Int) { + if (!isBinary) { + if (b == '\n'.code && lastB != '\r'.code) { // Unix + crlfOut.write('\r'.code) + } else if (lastB == '\r'.code) { // MAC + if (b != '\n'.code) { + crlfOut.write('\n'.code) + } + } + lastB = b + } + crlfOut.write(b) + } + + override fun close() { + if (!isBinary && lastB == 'r'.code) { + crlfOut.write('\n'.code) + } + crlfOut.close() + } + + override fun flush() { + super.flush() + crlfOut.flush() + } +} \ No newline at end of file From 53b1e3ff71e78273e38b93eea040159e99ef2be3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Sep 2023 14:47:05 +0200 Subject: [PATCH 165/351] Kotlin conversion: HashContextSigning --- .../BcHashContextSigner.java | 66 ------- .../BcPGPHashContextContentSignerBuilder.java | 174 ------------------ .../PGPHashContextContentSignerBuilder.java | 83 --------- .../encryption_signing/package-info.java | 8 - .../encryption_signing/BcHashContextSigner.kt | 49 +++++ .../BcPGPHashContextContentSignerBuilder.kt | 131 +++++++++++++ .../PGPHashContextContentSignerBuilder.kt | 42 +++++ 7 files changed, 222 insertions(+), 331 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java deleted file mode 100644 index 54c6df9e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcHashContextSigner.java +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.security.MessageDigest; -import java.util.List; - -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.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; - -import javax.annotation.Nonnull; - -public class BcHashContextSigner { - - public static PGPSignature signHashContext(@Nonnull MessageDigest hashContext, - @Nonnull SignatureType signatureType, - @Nonnull PGPSecretKeyRing secretKeys, - @Nonnull SecretKeyRingProtector protector) - throws PGPException { - KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); - List signingSubkeyCandidates = info.getSigningSubkeys(); - PGPSecretKey signingKey = null; - for (PGPPublicKey signingKeyCandidate : signingSubkeyCandidates) { - signingKey = secretKeys.getSecretKey(signingKeyCandidate.getKeyID()); - if (signingKey != null) { - break; - } - } - if (signingKey == null) { - throw new PGPException("Key does not contain suitable signing subkey."); - } - - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); - return signHashContext(hashContext, signatureType, privateKey); - } - - /** - * Create an OpenPGP Signature over the given {@link MessageDigest} hash context. - * - * @param hashContext hash context - * @param privateKey signing-capable key - * @return signature - * @throws PGPException in case of an OpenPGP error - */ - static PGPSignature signHashContext(MessageDigest hashContext, SignatureType signatureType, PGPPrivateKey privateKey) - throws PGPException { - PGPSignatureGenerator sigGen = new PGPSignatureGenerator( - new BcPGPHashContextContentSignerBuilder(hashContext) - ); - - sigGen.init(signatureType.getCode(), privateKey); - return sigGen.generate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java deleted file mode 100644 index 5cdf9e36..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.OutputStream; -import java.security.MessageDigest; - -import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.CryptoException; -import org.bouncycastle.crypto.DataLengthException; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; -import org.bouncycastle.crypto.signers.DSADigestSigner; -import org.bouncycastle.crypto.signers.DSASigner; -import org.bouncycastle.crypto.signers.ECDSASigner; -import org.bouncycastle.crypto.signers.Ed25519Signer; -import org.bouncycastle.crypto.signers.Ed448Signer; -import org.bouncycastle.crypto.signers.RSADigestSigner; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.operator.PGPContentSigner; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; -import org.bouncycastle.util.Arrays; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; - -/** - * Implementation of {@link PGPContentSignerBuilder} using the BC API, which can be used to sign hash contexts. - * This can come in handy to sign data, which was already processed to calculate the hash context, without the - * need to process it again to calculate the OpenPGP signature. - */ -class BcPGPHashContextContentSignerBuilder extends PGPHashContextContentSignerBuilder { - - private final BcPGPKeyConverter keyConverter = new BcPGPKeyConverter(); - private final MessageDigest messageDigest; - private final HashAlgorithm hashAlgorithm; - - BcPGPHashContextContentSignerBuilder(MessageDigest messageDigest) { - this.messageDigest = messageDigest; - this.hashAlgorithm = requireFromName(messageDigest.getAlgorithm()); - } - - private static HashAlgorithm requireFromName(String digestName) { - HashAlgorithm hashAlgorithm = HashAlgorithm.fromName(digestName); - if (hashAlgorithm == null) { - throw new IllegalArgumentException("Cannot recognize OpenPGP Hash Algorithm: " + digestName); - } - return hashAlgorithm; - } - - @Override - public PGPContentSigner build(int signatureType, PGPPrivateKey privateKey) throws PGPException { - PublicKeyAlgorithm keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.getPublicKeyPacket().getAlgorithm()); - AsymmetricKeyParameter privKeyParam = keyConverter.getPrivateKey(privateKey); - final Signer signer = createSigner(keyAlgorithm, messageDigest, privKeyParam); - signer.init(true, privKeyParam); - - return new PGPContentSigner() { - public int getType() { - return signatureType; - } - - public int getHashAlgorithm() { - return hashAlgorithm.getAlgorithmId(); - } - - public int getKeyAlgorithm() { - return keyAlgorithm.getAlgorithmId(); - } - - public long getKeyID() { - return privateKey.getKeyID(); - } - - public OutputStream getOutputStream() { - return new PGPHashContextContentSignerBuilder.SignerOutputStream(signer); - } - - public byte[] getSignature() { - try { - return signer.generateSignature(); - } catch (CryptoException e) { - throw new IllegalStateException("unable to create signature"); - } - } - - public byte[] getDigest() { - return messageDigest.digest(); - } - }; - } - - static Signer createSigner( - PublicKeyAlgorithm keyAlgorithm, - MessageDigest messageDigest, - CipherParameters keyParam) - throws PGPException { - ExistingMessageDigest staticDigest = new ExistingMessageDigest(messageDigest); - switch (keyAlgorithm.getAlgorithmId()) { - case PublicKeyAlgorithmTags.RSA_GENERAL: - case PublicKeyAlgorithmTags.RSA_SIGN: - return new RSADigestSigner(staticDigest); - case PublicKeyAlgorithmTags.DSA: - return new DSADigestSigner(new DSASigner(), staticDigest); - case PublicKeyAlgorithmTags.ECDSA: - return new DSADigestSigner(new ECDSASigner(), staticDigest); - case PublicKeyAlgorithmTags.EDDSA: - if (keyParam instanceof Ed25519PrivateKeyParameters || keyParam instanceof Ed25519PublicKeyParameters) { - return new EdDsaSigner(new Ed25519Signer(), staticDigest); - } - return new EdDsaSigner(new Ed448Signer(new byte[0]), staticDigest); - default: - throw new PGPException("cannot recognise keyAlgorithm: " + keyAlgorithm); - } - } - - // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ - private static class EdDsaSigner - implements Signer { - private final Signer signer; - private final Digest digest; - private final byte[] digBuf; - - EdDsaSigner(Signer signer, Digest digest) { - this.signer = signer; - this.digest = digest; - this.digBuf = new byte[digest.getDigestSize()]; - } - - public void init(boolean forSigning, CipherParameters param) { - this.signer.init(forSigning, param); - this.digest.reset(); - } - - public void update(byte b) { - this.digest.update(b); - } - - public void update(byte[] in, int off, int len) { - this.digest.update(in, off, len); - } - - public byte[] generateSignature() - throws CryptoException, DataLengthException { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.generateSignature(); - } - - public boolean verifySignature(byte[] signature) { - digest.doFinal(digBuf, 0); - - signer.update(digBuf, 0, digBuf.length); - - return signer.verifySignature(signature); - } - - public void reset() { - Arrays.clear(digBuf); - signer.reset(); - digest.reset(); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java deleted file mode 100644 index 7b8529fe..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.java +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.io.OutputStream; -import java.security.MessageDigest; -import javax.annotation.Nonnull; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; - -abstract class PGPHashContextContentSignerBuilder implements PGPContentSignerBuilder { - - // Copied from BC, required since BCs class is package visible only - static class SignerOutputStream - extends OutputStream { - private Signer sig; - - SignerOutputStream(Signer sig) { - this.sig = sig; - } - - public void write(@Nonnull byte[] bytes, int off, int len) { - sig.update(bytes, off, len); - } - - public void write(@Nonnull byte[] bytes) { - sig.update(bytes, 0, bytes.length); - } - - public void write(int b) { - sig.update((byte) b); - } - } - - - static class ExistingMessageDigest implements Digest { - - private final MessageDigest digest; - - ExistingMessageDigest(MessageDigest messageDigest) { - this.digest = messageDigest; - } - - @Override - public void update(byte in) { - digest.update(in); - } - - @Override - public void update(byte[] in, int inOff, int len) { - digest.update(in, inOff, len); - } - - @Override - public int doFinal(byte[] out, int outOff) { - byte[] hash = digest.digest(); - System.arraycopy(hash, 0, out, outOff, hash.length); - return getDigestSize(); - } - - @Override - public void reset() { - // Nope! - // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset - // the messageDigest, losing its state. This would shatter our intention. - } - - @Override - public String getAlgorithmName() { - return digest.getAlgorithm(); - } - - @Override - public int getDigestSize() { - return digest.getDigestLength(); - } - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java deleted file mode 100644 index 4a2f1f39..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes used to encrypt or sign data using OpenPGP. - */ -package org.pgpainless.encryption_signing; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt new file mode 100644 index 00000000..90f275a4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.extensions.unlock +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import java.security.MessageDigest + +class BcHashContextSigner { + + companion object { + @JvmStatic + fun signHashContext(hashContext: MessageDigest, + signatureType: SignatureType, + secretKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector): PGPSignature { + val info = PGPainless.inspectKeyRing(secretKey) + return info.signingSubkeys.mapNotNull { info.getSecretKey(it.keyID) }.firstOrNull() + ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } + ?: throw PGPException("Key does not contain suitable signing subkey.") + } + + /** + * Create an OpenPGP Signature over the given [MessageDigest] hash context. + * + * @param hashContext hash context + * @param privateKey signing-capable key + * @return signature + * @throws PGPException in case of an OpenPGP error + */ + @JvmStatic + internal fun signHashContext(hashContext: MessageDigest, + signatureType: SignatureType, + privateKey: PGPPrivateKey): PGPSignature { + return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) + .apply { init(signatureType.code, privateKey) } + .generate() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..381c4716 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.CryptoException +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import org.bouncycastle.crypto.signers.DSADigestSigner +import org.bouncycastle.crypto.signers.DSASigner +import org.bouncycastle.crypto.signers.ECDSASigner +import org.bouncycastle.crypto.signers.Ed25519Signer +import org.bouncycastle.crypto.signers.Ed448Signer +import org.bouncycastle.crypto.signers.RSADigestSigner +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.operator.PGPContentSigner +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import java.io.OutputStream +import java.security.MessageDigest + +/** + * Implementation of [PGPContentSignerBuilder] using the BC API, which can be used to sign hash contexts. + * This can come in handy to sign data, which was already processed to calculate the hash context, without the + * need to process it again to calculate the OpenPGP signature. + */ +class BcPGPHashContextContentSignerBuilder( + private val messageDigest: MessageDigest +) : PGPHashContextContentSignerBuilder() { + + private val keyConverter = BcPGPKeyConverter() + private val _hashAlgorithm: HashAlgorithm + + init { + _hashAlgorithm = requireFromName(messageDigest.algorithm) + } + + override fun build(signatureType: Int, privateKey: PGPPrivateKey): PGPContentSigner { + val keyAlgorithm = PublicKeyAlgorithm.requireFromId(privateKey.publicKeyPacket.algorithm) + val privKeyParam = keyConverter.getPrivateKey(privateKey) + val signer = createSigner(keyAlgorithm, messageDigest, privKeyParam) + signer.init(true, privKeyParam) + + return object : PGPContentSigner { + override fun getOutputStream(): OutputStream = SignerOutputStream(signer) + override fun getSignature(): ByteArray = try { + signer.generateSignature() + } catch (e : CryptoException) { + throw IllegalStateException("unable to create signature.", e) + } + override fun getDigest(): ByteArray = messageDigest.digest() + override fun getType(): Int = signatureType + override fun getHashAlgorithm(): Int = _hashAlgorithm.algorithmId + override fun getKeyAlgorithm(): Int = keyAlgorithm.algorithmId + override fun getKeyID(): Long = privateKey.keyID + } + } + + companion object { + @JvmStatic + private fun requireFromName(digestName: String): HashAlgorithm { + val algorithm = HashAlgorithm.fromName(digestName) + require(algorithm != null) { "Cannot recognize OpenPGP Hash Algorithm: $digestName"} + return algorithm + } + + @JvmStatic + private fun createSigner(keyAlgorithm: PublicKeyAlgorithm, + messageDigest: MessageDigest, + keyParam: CipherParameters): Signer { + val staticDigest = ExistingMessageDigest(messageDigest) + return when (keyAlgorithm.algorithmId) { + PublicKeyAlgorithmTags.RSA_GENERAL, PublicKeyAlgorithmTags.RSA_SIGN -> RSADigestSigner(staticDigest) + PublicKeyAlgorithmTags.DSA -> DSADigestSigner(DSASigner(), staticDigest) + PublicKeyAlgorithmTags.ECDSA -> DSADigestSigner(ECDSASigner(), staticDigest) + PublicKeyAlgorithmTags.EDDSA_LEGACY -> { + if (keyParam is Ed25519PrivateKeyParameters || keyParam is Ed25519PublicKeyParameters) + EdDsaSigner(Ed25519Signer(), staticDigest) + else EdDsaSigner(Ed448Signer(byteArrayOf()), staticDigest) + } + else -> throw PGPException("cannot recognize keyAlgorithm: $keyAlgorithm") + } + } + } + + // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ + internal class EdDsaSigner( + private val signer: Signer, + private val digest: Digest + ) : Signer { + private val digBuf: ByteArray = ByteArray(digest.digestSize) + + override fun init(forSigning: Boolean, param: CipherParameters) { + signer.init(forSigning, param) + digest.reset() + } + + override fun update(b: Byte) { + digest.update(b) + } + + override fun update(b: ByteArray, off: Int, len: Int) { + digest.update(b, off, len) + } + + override fun generateSignature(): ByteArray { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.generateSignature() + } + + override fun verifySignature(signature: ByteArray): Boolean { + digest.doFinal(digBuf, 0) + signer.update(digBuf, 0, digBuf.size) + return signer.verifySignature(signature) + } + + override fun reset() { + digBuf.fill(0) + signer.reset() + digest.reset() + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt new file mode 100644 index 00000000..aa3dd58c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing + +import org.bouncycastle.crypto.Digest +import org.bouncycastle.crypto.Signer +import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder +import java.io.OutputStream +import java.security.MessageDigest + +abstract class PGPHashContextContentSignerBuilder : PGPContentSignerBuilder { + + // Copied from BC, required since BCs class is package visible only + internal class SignerOutputStream( + private val signer: Signer + ) : OutputStream() { + override fun write(p0: Int) = signer.update(p0.toByte()) + override fun write(b: ByteArray) = signer.update(b, 0, b.size) + override fun write(b: ByteArray, off: Int, len: Int) = signer.update(b, off, len) + } + + internal class ExistingMessageDigest( + private val digest: MessageDigest + ) : Digest { + + override fun getAlgorithmName(): String = digest.algorithm + override fun getDigestSize(): Int = digest.digestLength + override fun update(b: Byte) = digest.update(b) + override fun update(buf: ByteArray, inOff: Int, len: Int) = digest.update(buf) + override fun doFinal(out: ByteArray, outOff: Int): Int { + digest.digest().copyInto(out, outOff) + return digestSize + } + override fun reset() { + // Nope! + // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // the messageDigest, losing its state. This would shatter our intention. + } + } +} \ No newline at end of file From 33037b9743515e426c50ac5ad82c6e8e6465f633 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Sep 2023 15:19:32 +0200 Subject: [PATCH 166/351] Kotlin conversion: Passphrase --- .../java/org/pgpainless/util/Passphrase.java | 168 ------------------ .../kotlin/org/pgpainless/util/Passphrase.kt | 105 +++++++++++ 2 files changed, 105 insertions(+), 168 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java b/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java deleted file mode 100644 index 9576fb3e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Passphrase.java +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Arrays; - -public class Passphrase { - - public final Object lock = new Object(); - - private final char[] chars; - private boolean valid = true; - - /** - * Passphrase for keys etc. - * - * @param chars may be null for empty passwords. - */ - public Passphrase(@Nullable char[] chars) { - if (chars == null) { - this.chars = null; - } else { - char[] trimmed = removeTrailingAndLeadingWhitespace(chars); - if (trimmed.length == 0) { - this.chars = null; - } else { - this.chars = trimmed; - } - } - } - - /** - * Return a copy of the passed in char array, with leading and trailing whitespace characters removed. - * - * @param chars char array - * @return copy of char array with leading and trailing whitespace characters removed - */ - private static char[] removeTrailingAndLeadingWhitespace(char[] chars) { - int i = 0; - while (i < chars.length && isWhitespace(chars[i])) { - i++; - } - int j = chars.length - 1; - while (j >= i && isWhitespace(chars[j])) { - j--; - } - - char[] trimmed = new char[chars.length - i - (chars.length - 1 - j)]; - System.arraycopy(chars, i, trimmed, 0, trimmed.length); - - return trimmed; - } - - /** - * Return true, if the passed in char is a whitespace symbol (space, newline, tab). - * - * @param xar char - * @return true if whitespace - */ - private static boolean isWhitespace(char xar) { - return xar == ' ' || xar == '\n' || xar == '\t'; - } - - /** - * Create a {@link Passphrase} from a {@link String}. - * - * @param password password - * @return passphrase - */ - public static Passphrase fromPassword(@Nonnull String password) { - return new Passphrase(password.toCharArray()); - } - - /** - * Overwrite the char array with spaces and mark the {@link Passphrase} as invalidated. - */ - public void clear() { - synchronized (lock) { - if (chars != null) { - Arrays.fill(chars, ' '); - } - valid = false; - } - } - - /** - * Return a copy of the underlying char array. - * A return value of {@code null} represents no password. - * - * @return passphrase chars. - * - * @throws IllegalStateException in case the password has been cleared at this point. - */ - public @Nullable char[] getChars() { - synchronized (lock) { - if (!valid) { - throw new IllegalStateException("Passphrase has been cleared."); - } - - if (chars == null) { - return null; - } - - char[] copy = new char[chars.length]; - System.arraycopy(chars, 0, copy, 0, chars.length); - return copy; - } - } - - /** - * Return true if the passphrase has not yet been cleared. - * - * @return valid - */ - public boolean isValid() { - synchronized (lock) { - return valid; - } - } - - /** - * Return true if the passphrase represents no password. - * - * @return empty - */ - public boolean isEmpty() { - synchronized (lock) { - return valid && chars == null; - } - } - - /** - * Represents a {@link Passphrase} instance that represents no password. - * - * @return empty passphrase - */ - public static Passphrase emptyPassphrase() { - return new Passphrase(null); - } - - @Override - public int hashCode() { - if (getChars() == null) { - return 0; - } - return new String(getChars()).hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof Passphrase)) { - return false; - } - Passphrase other = (Passphrase) obj; - return (getChars() == null && other.getChars() == null) || - org.bouncycastle.util.Arrays.constantTimeAreEqual(getChars(), other.getChars()); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt new file mode 100644 index 00000000..a64fda53 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.util.Arrays + +/** + * Passphrase for keys or messages. + * + * @param chars may be null for empty passwords. + */ +class Passphrase( + chars: CharArray? +) { + private val lock = Any() + private var valid = true + private val chars: CharArray? + + init { + this.chars = trimWhitespace(chars) + } + + /** + * Return a copy of the underlying char array. + * A return value of null represents an empty password. + * + * @return passphrase chars. + * + * @throws IllegalStateException in case the password has been cleared at this point. + */ + fun getChars(): CharArray? = synchronized(lock) { + check(valid) { "Passphrase has been cleared." } + chars?.copyOf() + } + + /** + * Return true if the passphrase has not yet been cleared. + * + * @return valid + */ + val isValid: Boolean + get() = synchronized(lock) { valid } + + /** + * Return true if the passphrase represents no password. + * + * @return empty + */ + val isEmpty: Boolean + get() = synchronized(lock) { valid && chars == null } + + /** + * Overwrite the char array with spaces and mark the [Passphrase] as invalidated. + */ + fun clear() = synchronized(lock) { + chars?.fill(' ') + valid = false + } + + override fun equals(other: Any?): Boolean { + return if (other == null) + false + else if (this === other) + true + else if (other !is Passphrase) + false + else + getChars() == null && other.getChars() == null || Arrays.constantTimeAreEqual(getChars(), other.getChars()) + } + + override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() + + companion object { + + /** + * Create a [Passphrase] from a [CharSequence]. + * + * @param password password + * @return passphrase + */ + @JvmStatic + fun fromPassword(password: CharSequence) = Passphrase(password.toString().toCharArray()) + + @JvmStatic + fun emptyPassphrase() = Passphrase(null) + + /** + * Return a copy of the passed in char array, with leading and trailing whitespace characters removed. + * If the passed in char array is null, return null. + * If the resulting char array is empty, return null as well. + * + * @param chars char array + * @return copy of char array with leading and trailing whitespace characters removed + */ + @JvmStatic + private fun trimWhitespace(chars: CharArray?): CharArray? { + return chars?.dropWhile { it.isWhitespace() } + ?.dropLastWhile { it.isWhitespace() } + ?.toCharArray() + ?.let { if (it.isEmpty()) null else it } + } + } +} \ No newline at end of file From c9f988b2d18caf7e0609fa73813e986ac3b25674 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 26 Sep 2023 17:38:07 +0200 Subject: [PATCH 167/351] Kotlin conversion: SelectUserId --- .../util/selection/userid/SelectUserId.java | 231 ------------------ .../util/selection/userid/package-info.java | 8 - .../secretkeyring/SecretKeyRingEditor.kt | 37 ++- .../SecretKeyRingEditorInterface.kt | 78 +++++- .../util/selection/userid/SelectUserId.kt | 104 ++++++++ .../selection/userid/SelectUserIdTest.java | 78 +++--- 6 files changed, 240 insertions(+), 296 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java deleted file mode 100644 index 5c1611af..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/SelectUserId.java +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util.selection.userid; - -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.key.info.KeyRingInfo; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Filter for selecting user-ids from keys and from lists. - */ -public abstract class SelectUserId { - - /** - * Return true, if the given user-id is accepted by this particular filter, false otherwise. - * - * @param userId user-id - * @return acceptance of the filter - */ - protected abstract boolean accept(String userId); - - /** - * Select all currently valid user-ids of the given key ring. - * - * @param keyRing public or secret key ring - * @return valid user-ids - */ - @Nonnull - public List selectUserIds(@Nonnull PGPKeyRing keyRing) { - List userIds = PGPainless.inspectKeyRing(keyRing).getValidUserIds(); - return selectUserIds(userIds); - } - - /** - * Select all acceptable (see {@link #accept(String)}) from the given list of user-ids. - * - * @param userIds list of user-ids - * @return sub-list of acceptable user-ids - */ - @Nonnull - public List selectUserIds(@Nonnull List userIds) { - List selected = new ArrayList<>(); - for (String userId : userIds) { - if (accept(userId)) { - selected.add(userId); - } - } - return selected; - } - - /** - * Return the first valid, acceptable user-id from the given public or secret key ring. - * - * @param keyRing public or secret key ring - * @return first matching valid user-id or null - */ - @Nullable - public String firstMatch(PGPKeyRing keyRing) { - return firstMatch(selectUserIds(keyRing)); - } - - /** - * Return the first valid, acceptable user-id from the list of user-ids. - * - * @param userIds list of user-ids - * @return first matching valid user-id or null - */ - @Nullable - public String firstMatch(@Nonnull List userIds) { - for (String userId : userIds) { - if (accept(userId)) { - return userId; - } - } - return null; - } - - /** - * Filter that filters for user-ids which contain the given
query
as a substring. - * - * @param query query - * @return filter - */ - public static SelectUserId containsSubstring(@Nonnull CharSequence query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.contains(query.toString()); - } - }; - } - - /** - * Filter that filters for user-ids which match the given
query
exactly. - * - * @param query query - * @return filter - */ - public static SelectUserId exactMatch(@Nonnull CharSequence query) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.equals(query.toString()); - } - }; - } - - /** - * Filter that filters for user-ids which start with the given
substring
. - * - * @param substring substring - * @return filter - */ - public static SelectUserId startsWith(@Nonnull CharSequence substring) { - String string = substring.toString(); - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return userId.startsWith(string); - } - }; - } - - /** - * Filter that filters for user-ids which contain the given
email
address. - * Note: This only accepts user-ids which properly have the email address surrounded by angle brackets. - * - * The argument
email
can both be a plain email address (
"foo@bar.baz"
), - * or surrounded by angle brackets ({@code
""
}), the result of the filter will be the same. - * - * @param email email address - * @return filter - */ - public static SelectUserId containsEmailAddress(@Nonnull CharSequence email) { - String string = email.toString(); - return containsSubstring(string.matches("^<.+>$") ? string : '<' + string + '>'); - } - - /** - * Filter that filters for valid user-ids on the given
keyRing
only. - * - * @param keyRing public / secret keys - * @return filter - */ - public static SelectUserId validUserId(PGPKeyRing keyRing) { - final KeyRingInfo info = PGPainless.inspectKeyRing(keyRing); - - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return info.isUserIdValid(userId); - } - }; - } - - /** - * Filter that filters for user-ids which pass all the given
filters
. - * - * @param filters filters - * @return filter - */ - public static SelectUserId and(SelectUserId... filters) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = true; - for (SelectUserId filter : filters) { - accept &= filter.accept(userId); - } - return accept; - } - }; - } - - /** - * Filter that filters for user-ids which pass at least one of the given
filters
. - * - * @param filters filters - * @return filter - */ - public static SelectUserId or(SelectUserId... filters) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - boolean accept = false; - for (SelectUserId filter : filters) { - accept |= filter.accept(userId); - } - return accept; - } - }; - } - - /** - * Filter that inverts the result of the given
filter
. - * - * @param filter filter - * @return inverting filter - */ - public static SelectUserId not(SelectUserId filter) { - return new SelectUserId() { - @Override - protected boolean accept(String userId) { - return !filter.accept(userId); - } - }; - } - - /** - * Filter that selects user-ids by the given
email
address. - * It returns user-ids which either contain the given
email
address as angle-bracketed string, - * or which equal the given
email
string exactly. - * - * @param email email - * @return filter - */ - public static SelectUserId byEmail(CharSequence email) { - return SelectUserId.or( - SelectUserId.exactMatch(email), - SelectUserId.containsEmailAddress(email) - ); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java deleted file mode 100644 index 5b70f412..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/userid/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * UserID selection strategies. - */ -package org.pgpainless.util.selection.userid; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 2fde469c..b219a739 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -31,7 +31,9 @@ import org.pgpainless.signature.subpackets.* import org.pgpainless.util.Passphrase import org.pgpainless.util.selection.userid.SelectUserId import java.util.* +import java.util.function.Predicate import javax.annotation.Nonnull +import kotlin.NoSuchElementException class SecretKeyRingEditor( var secretKeyRing: PGPSecretKeyRing, @@ -109,14 +111,22 @@ class SecretKeyRingEditor( return this } + @Deprecated("Use of SelectUserId class is deprecated.", replaceWith = ReplaceWith("removeUserId(protector, predicate)")) override fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { return revokeUserIds(selector, protector, RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) .withoutDescription()) } + override fun removeUserId(protector: SecretKeyRingProtector, predicate: (String) -> Boolean): SecretKeyRingEditorInterface { + return revokeUserIds(protector, RevocationAttributes.createCertificateRevocation() + .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) + .withoutDescription(), + predicate) + } + override fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { - return removeUserId(SelectUserId.exactMatch(userId), protector) + return removeUserId(protector) { uid -> userId == uid } } override fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { @@ -246,21 +256,23 @@ class SecretKeyRingEditor( } override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { - return revokeUserIds(SelectUserId.exactMatch(sanitizeUserId(userId)), protector, callback) + return revokeUserIds(protector, callback, SelectUserId.exactMatch(sanitizeUserId(userId))) } - override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { - return revokeUserIds(selector, protector, object : RevocationSignatureSubpackets.Callback { + override fun revokeUserIds(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean): SecretKeyRingEditorInterface { + return revokeUserIds(protector, object : RevocationSignatureSubpackets.Callback { override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { - if (revocationAttributes != null) { - hashedSubpackets.setRevocationReason(revocationAttributes) - } + if (revocationAttributes != null) hashedSubpackets.setRevocationReason(revocationAttributes) } - }) + }, predicate) } - override fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { - selector.selectUserIds(secretKeyRing).also { + override fun revokeUserIds(protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean): SecretKeyRingEditorInterface { + selectUserIds(predicate).also { if (it.isEmpty()) throw NoSuchElementException("No matching user-ids found on the key.") }.forEach { userId -> doRevokeUserId(userId, protector, callback) } return this @@ -348,7 +360,7 @@ class SecretKeyRingEditor( private fun sanitizeUserId(userId: CharSequence): CharSequence = // TODO: Further research how to sanitize user IDs. - // eg. what about newlines? + // e.g. what about newlines? userId.toString().trim() private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = @@ -454,6 +466,9 @@ class SecretKeyRingEditor( }.build(secretKeyRing.publicKey) } + private fun selectUserIds(predicate: Predicate): List = + inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } + private class WithKeyRingEncryptionSettingsImpl( private val editor: SecretKeyRingEditor, private val keyId: Long?, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt index 45a09fb2..8a26161b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -4,11 +4,7 @@ package org.pgpainless.key.modification.secretkeyring -import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPKeyPair -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.* import org.pgpainless.algorithm.KeyFlag import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.KeySpec @@ -23,6 +19,7 @@ import java.io.IOException import java.security.InvalidAlgorithmParameterException import java.security.NoSuchAlgorithmException import java.util.* +import java.util.function.Predicate interface SecretKeyRingEditorInterface { @@ -82,8 +79,25 @@ interface SecretKeyRingEditorInterface { * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ + @Deprecated("Use of SelectUserId class is deprecated.", + ReplaceWith("removeUserId(protector, predicate)")) @Throws(PGPException::class) - fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector) = + removeUserId(protector, selector) + + /** + * Convenience method to revoke selected user-ids using soft revocation signatures. + * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the user-id + * can be re-certified at a later point. + * + * @param protector protector to unlock the primary key + * @param predicate predicate to select user-ids for revocation + * @return the builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun removeUserId(protector: SecretKeyRingProtector, predicate: (String) -> Boolean): SecretKeyRingEditorInterface /** * Convenience method to revoke a single user-id using a soft revocation signature. @@ -342,9 +356,32 @@ interface SecretKeyRingEditorInterface { * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) + @Deprecated("Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, revocationAttributes, predicate)")) fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface + revocationAttributes: RevocationAttributes?) = + revokeUserIds(protector, revocationAttributes, selector) + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationAttributes] will be set as reason for revocation in each + * revocation signature. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose + * a soft revocation reason. See [RevocationAttributes.Reason] for more information. + * + * @param protector protector to unlock the primary secret key + * @param revocationAttributes revocation attributes + * @param predicate to select user-ids for revocation + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean): SecretKeyRingEditorInterface /** * Revoke all user-ids that match the provided [SelectUserId] filter. @@ -364,9 +401,34 @@ interface SecretKeyRingEditorInterface { * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) + @Deprecated("Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, callback, predicate)")) fun revokeUserIds(selector: SelectUserId, protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + callback: RevocationSignatureSubpackets.Callback?) = + revokeUserIds(protector, callback, selector) + + /** + * Revoke all user-ids that match the provided [SelectUserId] filter. + * The provided [RevocationSignatureSubpackets.Callback] will be used to modify the + * revocation signatures subpackets. + * + * Note: If you intend to re-certify these user-ids at a later point, make sure to set + * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. + * + * See [RevocationAttributes.Reason] for more information. + * + * @param protector protector to unlock the primary secret key + * @param callback callback to modify the revocations subpackets + * @param predicate to select user-ids for revocation + * @return builder + * + * @throws PGPException in case we cannot generate a revocation signature for the user-id + */ + @Throws(PGPException::class) + fun revokeUserIds(protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean): SecretKeyRingEditorInterface /** * Set the expiration date for the primary key of the key ring. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt new file mode 100644 index 00000000..3c215d80 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util.selection.userid + +import org.bouncycastle.openpgp.PGPKeyRing +import org.pgpainless.PGPainless +import java.util.function.Predicate + +abstract class SelectUserId : Predicate, (String) -> Boolean { + + /** + * Legacy glue code to forward accept() calls to invoke() instead. + */ + @Deprecated("Use invoke() instead.", ReplaceWith("invoke(userId)")) + protected fun accept(userId: String): Boolean = invoke(userId) + + override fun test(userId: String): Boolean = invoke(userId) + + companion object { + + /** + * Filter for user-ids which match the given [query] exactly. + * + * @param query query + * @return filter + */ + @JvmStatic + fun exactMatch(query: CharSequence) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + userId == query + } + + /** + * Filter for user-ids which start with the given [substring]. + * + * @param substring substring + * @return filter + */ + @JvmStatic + fun startsWith(substring: CharSequence) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + userId.startsWith(substring) + } + + /** + * Filter for user-ids which contain the given [substring]. + * + * @param substring query + * @return filter + */ + @JvmStatic + fun containsSubstring(substring: CharSequence) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + userId.contains(substring) + } + + /** + * Filter for user-ids which contain the given [email] address. + * Note: This only accepts user-ids which properly have the email address surrounded by angle brackets. + * + * The argument [email] can both be a plain email address (`foo@bar.baz`), + * or surrounded by angle brackets (``), the result of the filter will be the same. + * + * @param email email address + * @return filter + */ + @JvmStatic + fun containsEmailAddress(email: CharSequence) = + if (email.startsWith('<') && email.endsWith('>')) + containsSubstring(email) + else + containsSubstring("<$email>") + + @JvmStatic + fun byEmail(email: CharSequence) = or(exactMatch(email), containsEmailAddress(email)) + + @JvmStatic + fun validUserId(keyRing: PGPKeyRing) = object : SelectUserId() { + private val info = PGPainless.inspectKeyRing(keyRing) + override fun invoke(userId: String): Boolean = + info.isUserIdValid(userId) + } + + @JvmStatic + fun and(vararg filters: SelectUserId) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + filters.all { it.invoke(userId) } + } + + @JvmStatic + fun or(vararg filters: SelectUserId) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + filters.any { it.invoke(userId) } + } + + @JvmStatic + fun not(filter: SelectUserId) = object : SelectUserId() { + override fun invoke(userId: String): Boolean = + !filter.invoke(userId) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java index 3a49247c..99a0c87c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/userid/SelectUserIdTest.java @@ -4,6 +4,15 @@ package org.pgpainless.util.selection.userid; +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.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; @@ -11,20 +20,10 @@ import org.pgpainless.PGPainless; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.UserId; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class SelectUserIdTest { @Test - public void testSelectUserIds() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testSelectUserIds() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .simpleEcKeyRing(""); secretKeys = PGPainless.modifyKeyRing(secretKeys) @@ -34,48 +33,53 @@ public class SelectUserIdTest { SecretKeyRingProtector.unprotectedKeys()) .done(); - List validEmail = SelectUserId.and( + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + List validEmail = userIds.stream().filter(SelectUserId.and( SelectUserId.validUserId(secretKeys), SelectUserId.containsEmailAddress("alice@wonderland.lit") - ).selectUserIds(secretKeys); + )).collect(Collectors.toList()); assertEquals(Collections.singletonList(""), validEmail); - List startsWithAlice = SelectUserId.startsWith("Alice").selectUserIds(secretKeys); + List startsWithAlice = userIds.stream().filter(SelectUserId.startsWith("Alice")).collect(Collectors.toList()); assertEquals(Collections.singletonList("Alice Liddell "), startsWithAlice); - List exactMatch = SelectUserId.or( + List exactMatch = userIds.stream().filter(SelectUserId.or( SelectUserId.exactMatch(""), SelectUserId.startsWith("Not Found") - ).selectUserIds(secretKeys); + )).collect(Collectors.toList()); assertEquals(Collections.singletonList(""), exactMatch); } @Test - public void testContainsSubstring() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testContainsSubstring() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("wine drinker"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("this is not a quine", SecretKeyRingProtector.unprotectedKeys()) .addUserId("this is not a crime", SecretKeyRingProtector.unprotectedKeys()) .done(); - List containSubstring = SelectUserId.containsSubstring("ine") - .selectUserIds(secretKeys); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + + List containSubstring = userIds.stream().filter(SelectUserId.containsSubstring("ine")).collect(Collectors.toList()); assertEquals(Arrays.asList("wine drinker", "this is not a quine"), containSubstring); } @Test - public void testContainsEmailAddress() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testContainsEmailAddress() { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); - assertEquals("Alice ", SelectUserId.containsEmailAddress("alice@wonderland.lit").firstMatch(secretKeys)); - assertEquals("Alice ", SelectUserId.containsEmailAddress("").firstMatch(secretKeys)); + assertEquals("Alice ", userIds.stream().filter( + SelectUserId.containsEmailAddress("alice@wonderland.lit")).findFirst().get()); + assertEquals("Alice ", userIds.stream().filter( + SelectUserId.containsEmailAddress("")).findFirst().get()); - assertNull(SelectUserId.containsEmailAddress("mad@hatter.lit").firstMatch(secretKeys)); + assertFalse(userIds.stream().anyMatch(SelectUserId.containsEmailAddress("mad@hatter.lit"))); } @Test - public void testAndOrNot() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testAndOrNot() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("Alice "); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Alice ", SecretKeyRingProtector.unprotectedKeys()) @@ -83,34 +87,32 @@ public class SelectUserIdTest { .addUserId("Crazy Girl ", SecretKeyRingProtector.unprotectedKeys()) .done(); - List or = SelectUserId.or( + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + + List or = userIds.stream().filter(SelectUserId.or( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Arrays.asList("Alice ", "Alice ", "Crazy Girl "), or); - List and = SelectUserId.and( + List and = userIds.stream().filter(SelectUserId.and( SelectUserId.containsEmailAddress("alice@wonderland.lit"), - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Collections.singletonList("Alice "), and); - List not = SelectUserId.not( - SelectUserId.startsWith("Alice")) - .selectUserIds(secretKeys); + List not = userIds.stream().filter(SelectUserId.not( + SelectUserId.startsWith("Alice"))).collect(Collectors.toList()); assertEquals(Arrays.asList("", "Crazy Girl "), not); } @Test - public void testFirstMatch() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + public void testFirstMatch() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().simpleEcKeyRing("First UserID"); secretKeys = PGPainless.modifyKeyRing(secretKeys) .addUserId("Second UserID", SecretKeyRingProtector.unprotectedKeys()) .done(); - assertEquals("First UserID", SelectUserId.validUserId(secretKeys).firstMatch(secretKeys)); - assertEquals("Second UserID", SelectUserId.containsSubstring("Second").firstMatch( - PGPainless.inspectKeyRing(secretKeys).getUserIds() - )); + List userIds = PGPainless.inspectKeyRing(secretKeys).getValidUserIds(); + assertEquals("First UserID", userIds.stream().filter(SelectUserId.validUserId(secretKeys)).findFirst().get()); + assertEquals("Second UserID", userIds.stream().filter(SelectUserId.containsSubstring("Second")).findFirst().get()); } @Test From 4c237d55ed8743bb8c5767df6baff6290d30055c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 11:29:54 +0200 Subject: [PATCH 168/351] Add note about deprecation to BaseSignatureSubpackets --- .../signature/subpackets/BaseSignatureSubpackets.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java index 28e99b9e..09fe1a99 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -35,6 +35,15 @@ public interface BaseSignatureSubpackets { } + /** + * Add both an {@link IssuerKeyID} and {@link IssuerFingerprint} subpacket pointing to the given key. + * + * @param key key + * @return this + * + * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain any + * {@link IssuerKeyID} packets. + */ BaseSignatureSubpackets setIssuerFingerprintAndKeyId(PGPPublicKey key); BaseSignatureSubpackets setIssuerKeyId(long keyId); From 9a917f7fdba603224ee9bbae2b4369224e612ee2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 11:53:30 +0200 Subject: [PATCH 169/351] Kotlin conversion: DateUtil --- .../java/org/pgpainless/util/DateUtil.java | 75 ------------------- .../src/main/kotlin/openpgp/DateExtensions.kt | 61 +++++++++++---- .../kotlin/org/pgpainless/util/DateUtil.kt | 50 +++++++++++++ 3 files changed, 97 insertions(+), 89 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java deleted file mode 100644 index f56020d4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/DateUtil.java +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -public final class DateUtil { - - private DateUtil() { - - } - - // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. - @Nonnull - public static SimpleDateFormat getParser() { - SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); - parser.setTimeZone(TimeZone.getTimeZone("UTC")); - return parser; - } - - /** - * Parse a UTC timestamp into a date. - * - * @param dateString timestamp - * @return date - */ - @Nonnull - public static Date parseUTCDate(@Nonnull String dateString) { - try { - return getParser().parse(dateString); - } catch (ParseException e) { - throw new IllegalArgumentException("Malformed UTC timestamp: " + dateString, e); - } - } - - /** - * Format a date as UTC timestamp. - * - * @param date date - * @return timestamp - */ - @Nonnull - public static String formatUTCDate(Date date) { - return getParser().format(date); - } - - /** - * Floor a date down to seconds precision. - * @param date date - * @return floored date - */ - @Nonnull - public static Date toSecondsPrecision(@Nonnull Date date) { - long millis = date.getTime(); - long seconds = millis / 1000; - long floored = seconds * 1000; - return new Date(floored); - } - - /** - * Return the current date "floored" to UTC precision. - * - * @return now - */ - @Nonnull - public static Date now() { - return toSecondsPrecision(new Date()); - } -} diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt index db98fb44..5972cd3a 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -4,20 +4,53 @@ package openpgp +import java.text.ParseException +import java.text.SimpleDateFormat import java.util.* - /** - * Return a new date which represents this date plus the given amount of seconds added. - * - * Since '0' is a special date value in the OpenPGP specification - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. - * - * @param date date - * @param seconds number of seconds to be added - * @return date plus seconds or null if seconds is '0' - */ - fun Date.plusSeconds(seconds: Long): Date? { - require(Long.MAX_VALUE - time > seconds) { "Adding $seconds seconds to this date would cause time to overflow." } - return if (seconds == 0L) null - else Date(this.time + 1000 * seconds) +/** + * Return a new date which represents this date plus the given amount of seconds added. + * + * Since '0' is a special date value in the OpenPGP specification + * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * + * @param date date + * @param seconds number of seconds to be added + * @return date plus seconds or null if seconds is '0' + */ +fun Date.plusSeconds(seconds: Long): Date? { + require(Long.MAX_VALUE - time > seconds) { "Adding $seconds seconds to this date would cause time to overflow." } + return if (seconds == 0L) null + else Date(this.time + 1000 * seconds) +} + +/** + * Return a new [Date] instance with this instance's time floored down to seconds precision. + */ +fun Date.toSecondsPrecision(): Date { + return Date((time / 1000) * 1000) +} + +internal val parser: SimpleDateFormat + // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. + get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z") + .apply { timeZone = TimeZone.getTimeZone("UTC") } + +/** + * Format a date as UTC timestamp. + * + * @return timestamp + */ +fun Date.formatUTC(): String = parser.format(this) + +/** + * Parse a UTC timestamp into a date. + * @return date + */ +fun String.parseUTC(): Date { + return try { + parser.parse(this) + } catch (e : ParseException) { + throw IllegalArgumentException("Malformed UTC timestamp: $this", e) } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt new file mode 100644 index 00000000..de2052d6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import openpgp.formatUTC +import openpgp.parseUTC +import openpgp.toSecondsPrecision +import java.util.* + +class DateUtil { + + companion object { + + /** + * Parse a UTC timestamp into a date. + * + * @param dateString timestamp + * @return date + */ + @JvmStatic + fun parseUTCDate(dateString: String): Date = dateString.parseUTC() + + /** + * Format a date as UTC timestamp. + * + * @param date date + * @return timestamp + */ + @JvmStatic + fun formatUTCDate(date: Date): String = date.formatUTC() + + /** + * Floor a date down to seconds precision. + * @param date date + * @return floored date + */ + @JvmStatic + fun toSecondsPrecision(date: Date): Date = date.toSecondsPrecision() + + /** + * Return the current date "floored" to UTC precision. + * + * @return now + */ + @JvmStatic + fun now() = toSecondsPrecision(Date()) + } +} \ No newline at end of file From b324742a623cbaa819b91af4de846ffb56dff1c1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 13:30:39 +0200 Subject: [PATCH 170/351] Kotlin conversion: ArmorUtils --- .../java/org/pgpainless/util/ArmorUtils.java | 603 ------------------ .../main/java/org/pgpainless/util/Tuple.java | 8 + .../kotlin/org/pgpainless/util/ArmorUtils.kt | 422 ++++++++++++ 3 files changed, 430 insertions(+), 603 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java deleted file mode 100644 index 976f6ab2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ /dev/null @@ -1,603 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Pattern; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPKeyRing; -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.PGPSignature; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.util.KeyRingUtils; - -/** - * Utility class for dealing with ASCII armored OpenPGP data. - */ -public final class ArmorUtils { - - // MessageIDs are 32 printable characters - private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$"); - - /** - * Constant armor key for comments. - */ - public static final String HEADER_COMMENT = "Comment"; - /** - * Constant armor key for program versions. - */ - public static final String HEADER_VERSION = "Version"; - /** - * Constant armor key for message IDs. Useful for split messages. - */ - public static final String HEADER_MESSAGEID = "MessageID"; - /** - * Constant armor key for used hash algorithms in clearsigned messages. - */ - public static final String HEADER_HASH = "Hash"; - /** - * Constant armor key for message character sets. - */ - public static final String HEADER_CHARSET = "Charset"; - - private ArmorUtils() { - - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKey}. - * - * @param secretKey secret key - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSecretKey secretKey) - throws IOException { - MultiMap header = keyToHeader(secretKey.getPublicKey()); - return toAsciiArmoredString(secretKey.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKey}. - * - * @param publicKey public key - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPPublicKey publicKey) - throws IOException { - MultiMap header = keyToHeader(publicKey); - return toAsciiArmoredString(publicKey.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRing secretKeys) - throws IOException { - MultiMap header = keysToHeader(secretKeys); - return toAsciiArmoredString(secretKeys.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKeyRing}. - * - * @param publicKeys public key ring - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRing publicKeys) - throws IOException { - MultiMap header = keysToHeader(publicKeys); - return toAsciiArmoredString(publicKeys.getEncoded(), header); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPSecretKeyRingCollection}. - * The encoding will use per-key ASCII armors protecting each {@link PGPSecretKeyRing} individually. - * Those armors are then concatenated with newlines in between. - * - * @param secretKeyRings secret key ring collection - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRingCollection secretKeyRings) - throws IOException { - StringBuilder sb = new StringBuilder(); - for (Iterator iterator = secretKeyRings.iterator(); iterator.hasNext(); ) { - PGPSecretKeyRing secretKeyRing = iterator.next(); - sb.append(toAsciiArmoredString(secretKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - /** - * Return the ASCII armored encoding of the given {@link PGPPublicKeyRingCollection}. - * The encoding will use per-key ASCII armors protecting each {@link PGPPublicKeyRing} individually. - * Those armors are then concatenated with newlines in between. - * - * @param publicKeyRings public key ring collection - * @return ascii armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRingCollection publicKeyRings) - throws IOException { - StringBuilder sb = new StringBuilder(); - for (Iterator iterator = publicKeyRings.iterator(); iterator.hasNext(); ) { - PGPPublicKeyRing publicKeyRing = iterator.next(); - sb.append(toAsciiArmoredString(publicKeyRing)); - if (iterator.hasNext()) { - sb.append('\n'); - } - } - return sb.toString(); - } - - /** - * Return the ASCII armored representation of the given detached signature. - * The signature will not be stripped of non-exportable subpackets or trust-packets. - * If you need to strip those (e.g. because the signature is intended to be sent to a third party), use - * {@link #toAsciiArmoredString(PGPSignature, boolean)} and provide
true
as boolean value. - * - * @param signature signature - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature) throws IOException { - return toAsciiArmoredString(signature, false); - } - - /** - * Return the ASCII armored representation of the given detached signature. - * If
export
is true, the signature will be stripped of non-exportable subpackets or trust-packets. - * If it is
false
, the signature will be encoded as-is. - * - * @param signature signature - * @param export whether to exclude non-exportable subpackets or trust-packets. - * @return ascii armored string - * - * @throws IOException in case of an error in the {@link ArmoredOutputStream} - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull PGPSignature signature, boolean export) - throws IOException { - return toAsciiArmoredString(signature.getEncoded(export)); - } - - /** - * Return the ASCII armored encoding of the given OpenPGP data bytes. - * - * @param bytes openpgp data - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull byte[] bytes) - throws IOException { - return toAsciiArmoredString(bytes, null); - } - - /** - * Return the ASCII armored encoding of the given OpenPGP data bytes. - * The ASCII armor will include headers from the header map. - * - * @param bytes OpenPGP data - * @param additionalHeaderValues header map - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull byte[] bytes, - @Nullable MultiMap additionalHeaderValues) - throws IOException { - return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues); - } - - /** - * Return the ASCII armored encoding of the {@link InputStream} containing OpenPGP data. - * - * @param inputStream input stream of OpenPGP data - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull InputStream inputStream) - throws IOException { - return toAsciiArmoredString(inputStream, null); - } - - /** - * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. - * The ASCII armor will include armor headers from the given header map. - * - * @param inputStream input stream of OpenPGP data - * @param additionalHeaderValues ASCII armor header map - * @return ASCII armored encoding - * - * @throws IOException in case of an io error - */ - @Nonnull - public static String toAsciiArmoredString(@Nonnull InputStream inputStream, - @Nullable MultiMap additionalHeaderValues) - throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues); - Streams.pipeAll(inputStream, armor); - armor.close(); - - return out.toString(); - } - - /** - * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given - * {@link OutputStream}. - * - * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)} - * with the armored output stream as an argument. - * - * @param keyRing key ring - * @param outputStream wrapped output stream - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull PGPKeyRing keyRing, - @Nonnull OutputStream outputStream) { - MultiMap header = keysToHeader(keyRing); - return toAsciiArmoredStream(outputStream, header); - } - - /** - * Create an {@link ArmoredOutputStream} wrapping the given {@link OutputStream}. - * The armored output stream will be prepared with armor headers given by header. - * - * Note: Since the armored output stream is retrieved from {@link ArmoredOutputStreamFactory#get(OutputStream)}, - * it may already come with custom headers. Hence, the header entries given by header are appended below those - * already populated headers. - * - * @param outputStream output stream to wrap - * @param header map of header entries - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull OutputStream outputStream, - @Nullable MultiMap header) { - ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream); - if (header != null) { - for (String headerKey : header.keySet()) { - for (String headerValue : header.get(headerKey)) { - armoredOutputStream.addHeader(headerKey, headerValue); - } - } - } - return armoredOutputStream; - } - - /** - * Generate a header map for ASCII armor from the given {@link PGPKeyRing}. - * - * @param keyRing key ring - * @return header map - */ - @Nonnull - private static MultiMap keysToHeader(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey publicKey = keyRing.getPublicKey(); - return keyToHeader(publicKey); - } - - /** - * Generate a header map for ASCII armor from the given {@link PGPPublicKey}. - * The header map consists of a comment field of the keys pretty-printed fingerprint, - * as well as some optional user-id information (see {@link #setUserIdInfoOnHeader(MultiMap, PGPPublicKey)}. - * - * @param publicKey public key - * @return header map - */ - @Nonnull - private static MultiMap keyToHeader(@Nonnull PGPPublicKey publicKey) { - MultiMap header = new MultiMap<>(); - OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey); - - header.put(HEADER_COMMENT, fingerprint.prettyPrint()); - setUserIdInfoOnHeader(header, publicKey); - return header; - } - - /** - * Add user-id information to the header map. - * If the key is carrying at least one user-id, we add a comment for the probable primary user-id. - * If the key carries more than one user-id, we further add a comment stating how many further identities - * the key has. - * - * @param header header map - * @param publicKey public key - */ - private static void setUserIdInfoOnHeader(@Nonnull MultiMap header, - @Nonnull PGPPublicKey publicKey) { - Tuple idCount = getPrimaryUserIdAndUserIdCount(publicKey); - String primary = idCount.getA(); - int totalCount = idCount.getB(); - if (primary != null) { - header.put(HEADER_COMMENT, primary); - } - if (totalCount == 2) { - header.put(HEADER_COMMENT, "1 further identity"); - } else if (totalCount > 2) { - header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1)); - } - } - - /** - * Determine a probable primary user-id, as well as the total number of user-ids on the given {@link PGPPublicKey}. - * This method is trimmed for efficiency and does not do any cryptographic validation of signatures. - * - * The key might not have any user-id at all, in which case {@link Tuple#getA()} will return null. - * The key might have some user-ids, but none of it marked as primary, in which case {@link Tuple#getA()} - * will return the first user-id of the key. - * - * @param publicKey public key - * @return tuple consisting of a primary user-id candidate, and the total number of user-ids on the key. - */ - @Nonnull - private static Tuple getPrimaryUserIdAndUserIdCount(@Nonnull PGPPublicKey publicKey) { - // Quickly determine the primary user-id + number of total user-ids - // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES - // DO NOT RELY ON IT! - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey); - int countIdentities = 0; - String first = null; - String primary = null; - for (String userId : userIds) { - countIdentities++; - // remember the first user-id - if (first == null) { - first = userId; - } - - if (primary == null) { - Iterator signatures = publicKey.getSignaturesForID(userId); - while (signatures.hasNext()) { - PGPSignature signature = signatures.next(); - if (signature.getHashedSubPackets().isPrimaryUserID()) { - primary = userId; - break; - } - } - } - } - // It may happen that no user-id is marked as primary - // in that case print the first one - String printed = primary != null ? primary : first; - return new Tuple<>(printed, countIdentities); - } - - /** - * Set the version header entry in the ASCII armor. - * If the version info is null or only contains whitespace characters, then the version header will be removed. - * - * @param armor armored output stream - * @param version version header. - */ - public static void setVersionHeader(@Nonnull ArmoredOutputStream armor, - @Nullable String version) { - if (version == null || version.trim().isEmpty()) { - armor.setHeader(HEADER_VERSION, null); - } else { - armor.setHeader(HEADER_VERSION, version); - } - } - - /** - * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param hashAlgorithm hash algorithm - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addHashAlgorithmHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull HashAlgorithm hashAlgorithm) { - armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName()); - } - - /** - * Add an ASCII armor comment header entry into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param comment free-text comment - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addCommentHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull String comment) { - armor.addHeader(HEADER_COMMENT, comment); - } - - /** - * Add an ASCII armor message-id header entry into the {@link ArmoredOutputStream}. - * - * @param armor armored output stream - * @param messageId message id - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor - */ - public static void addMessageIdHeader(@Nonnull ArmoredOutputStream armor, - @Nonnull String messageId) { - if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) { - throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters."); - } - armor.addHeader(HEADER_MESSAGEID, messageId); - } - - /** - * Extract all ASCII armor header values of type comment from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of comment headers - */ - @Nonnull - public static List getCommentHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_COMMENT); - } - - /** - * Extract all ASCII armor header values of type message id from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of message-id headers - */ - @Nonnull - public static List getMessageIdHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_MESSAGEID); - } - - /** - * Return all ASCII armor header values of type hash-algorithm from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of hash headers - */ - @Nonnull - public static List getHashHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_HASH); - } - - /** - * Return a list of {@link HashAlgorithm} enums extracted from the hash header entries of the given - * {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of hash algorithms from the ASCII header - */ - @Nonnull - public static List getHashAlgorithms(@Nonnull ArmoredInputStream armor) { - List algorithmNames = getHashHeaderValues(armor); - List algorithms = new ArrayList<>(); - for (String name : algorithmNames) { - HashAlgorithm algorithm = HashAlgorithm.fromName(name); - if (algorithm != null) { - algorithms.add(algorithm); - } - } - return algorithms; - } - - /** - * Return all ASCII armor header values of type version from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of version headers - */ - @Nonnull - public static List getVersionHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_VERSION); - } - - /** - * Return all ASCII armor header values of type charset from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @return list of charset headers - */ - @Nonnull - public static List getCharsetHeaderValues(@Nonnull ArmoredInputStream armor) { - return getArmorHeaderValues(armor, HEADER_CHARSET); - } - - /** - * Return all ASCII armor header values of the given headerKey from the given {@link ArmoredInputStream}. - * - * @param armor armored input stream - * @param headerKey ASCII armor header key - * @return list of values for the header key - */ - @Nonnull - public static List getArmorHeaderValues(@Nonnull ArmoredInputStream armor, - @Nonnull String headerKey) { - String[] header = armor.getArmorHeaders(); - String key = headerKey + ": "; - List values = new ArrayList<>(); - for (String line : header) { - if (line.startsWith(key)) { - values.add(line.substring(key.length())); - } - } - return values; - } - - /** - * Hacky workaround for #96. - * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)} - * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given - * as constructor argument is a PGPUtil.BufferedInputStreamExt. - * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream} - * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the - * end-result is a PGPUtil.BufferedInputStreamExt. - * - * @param inputStream input stream - * @return BufferedInputStreamExt - * - * @throws IOException in case of an IO error - */ - @Nonnull - public static InputStream getDecoderStream(@Nonnull InputStream inputStream) - throws IOException { - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); - return PGPUtil.getDecoderStream(armorIn); - } - - return openPgpIn; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java index 27ad6a12..84d7a370 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java @@ -4,6 +4,14 @@ package org.pgpainless.util; +/** + * Helper class pairing together two values. + * @param type of the first value + * @param type of the second value + * @deprecated Scheduled for removal. + * TODO: Remove + */ +@Deprecated public class Tuple { private final A a; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt new file mode 100644 index 00000000..c8ff4da8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -0,0 +1,422 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0package org.pgpainless.util + +package org.pgpainless.util + +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.openpgp.PGPKeyRing +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.PGPSignature +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.util.io.Streams +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.util.KeyRingUtils +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class ArmorUtils { + + companion object { + // MessageIDs are 32 printable characters + private val PATTER_MESSAGE_ID = "^\\S{32}$".toRegex() + /** + * Constant armor key for comments. + */ + const val HEADER_COMMENT = "Comment" + /** + * Constant armor key for program versions. + */ + const val HEADER_VERSION = "Version" + /** + * Constant armor key for message IDs. Useful for split messages. + */ + const val HEADER_MESSAGEID = "MessageID" + /** + * Constant armor key for used hash algorithms in clearsigned messages. + */ + const val HEADER_HASH = "Hash" + /** + * Constant armor key for message character sets. + */ + const val HEADER_CHARSET = "Charset" + + /** + * Return the ASCII armored encoding of the given [PGPSecretKey]. + * + * @param secretKey secret key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKey: PGPSecretKey): String = + toAsciiArmoredString(secretKey.encoded, keyToHeader(secretKey.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKey]. + * + * @param publicKey public key + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(publicKey: PGPPublicKey): String = + toAsciiArmoredString(publicKey.encoded, keyToHeader(publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRing]. + * + * @param secretKeys secret key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeys: PGPSecretKeyRing): String = + toAsciiArmoredString(secretKeys.encoded, keyToHeader(secretKeys.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRing]. + * + * @param certificate public key ring + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificate: PGPPublicKeyRing): String = + toAsciiArmoredString(certificate.encoded, keyToHeader(certificate.publicKey)) + + /** + * Return the ASCII armored encoding of the given [PGPSecretKeyRingCollection]. + * The encoding will use per-key ASCII armors protecting each [PGPSecretKeyRing] individually. + * Those armors are then concatenated with newlines in between. + * + * @param secretKeysCollection secret key ring collection + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(secretKeysCollection: PGPSecretKeyRingCollection): String = + secretKeysCollection.keyRings.asSequence() + .joinToString("\n") { toAsciiArmoredString(it) } + + /** + * Return the ASCII armored encoding of the given [PGPPublicKeyRingCollection]. + * The encoding will use per-key ASCII armors protecting each [PGPPublicKeyRing] individually. + * Those armors are then concatenated with newlines in between. + * + * @param certificates public key ring collection + * @return ascii armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredString(certificates: PGPPublicKeyRingCollection): String = + certificates.joinToString("\n") { toAsciiArmoredString(it) } + + /** + * Return the ASCII armored representation of the given detached signature. + * If [export] is true, the signature will be stripped of non-exportable subpackets or trust-packets. + * If it is false, the signature will be encoded as-is. + * + * @param signature signature + * @param export whether to exclude non-exportable subpackets or trust-packets. + * @return ascii armored string + * + * @throws IOException in case of an error in the [ArmoredOutputStream] + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(signature: PGPSignature, export: Boolean = false): String = + toAsciiArmoredString(signature.getEncoded(export)) + + /** + * Return the ASCII armored encoding of the given OpenPGP data bytes. + * The ASCII armor will include headers from the header map. + * + * @param bytes OpenPGP data + * @param header header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(bytes: ByteArray, header: Map>? = null): String = + toAsciiArmoredString(bytes.inputStream(), header) + + /** + * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. + * The ASCII armor will include armor headers from the given header map. + * + * @param inputStream input stream of OpenPGP data + * @param header ASCII armor header map + * @return ASCII armored encoding + * + * @throws IOException in case of an io error + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredString(inputStream: InputStream, header: Map>? = null): String = + ByteArrayOutputStream().apply { + toAsciiArmoredStream(this, header).run { + Streams.pipeAll(inputStream, this) + this.close() + } + }.toString() + + /** + * Return an [ArmoredOutputStream] prepared with headers for the given key ring, which wraps the given + * {@link OutputStream}. + * + * The armored output stream can be used to encode the key ring by calling [PGPKeyRing.encode] + * with the armored output stream as an argument. + * + * @param keys OpenPGP key or certificate + * @param outputStream wrapped output stream + * @return armored output stream + */ + @JvmStatic + @Throws(IOException::class) + fun toAsciiArmoredStream(keys: PGPKeyRing, outputStream: OutputStream): ArmoredOutputStream = + toAsciiArmoredStream(outputStream, keyToHeader(keys.publicKey)) + + /** + * Create an [ArmoredOutputStream] wrapping the given [OutputStream]. + * The armored output stream will be prepared with armor headers given by header. + * + * Note: Since the armored output stream is retrieved from [ArmoredOutputStreamFactory.get], + * it may already come with custom headers. Hence, the header entries given by header are appended below those + * already populated headers. + * + * @param outputStream output stream to wrap + * @param header map of header entries + * @return armored output stream + */ + @JvmStatic + @JvmOverloads + @Throws(IOException::class) + fun toAsciiArmoredStream(outputStream: OutputStream, header: Map>? = null): ArmoredOutputStream = + ArmoredOutputStreamFactory.get(outputStream).apply { + header?.forEach { entry -> + entry.value.forEach { value -> + addHeader(entry.key, value) + } + } + } + + /** + * Generate a header map for ASCII armor from the given [PGPPublicKey]. + * The header map consists of a comment field of the keys pretty-printed fingerprint, + * as well as the primary or first user-id plus the count of remaining user-ids. + * + * @param publicKey public key + * @return header map + */ + @JvmStatic + private fun keyToHeader(publicKey: PGPPublicKey): Map> { + val headerMap = mutableMapOf>() + val userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) + val first: String? = userIds.firstOrNull() + val primary: String? = userIds.firstOrNull { + publicKey.getSignaturesForID(it)?.asSequence()?.any { sig -> + sig.hashedSubPackets.isPrimaryUserID + } ?: false + } + + // Fingerprint + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(OpenPgpFingerprint.of(publicKey).prettyPrint()) + // Primary / First User ID + (primary ?: first)?.let { headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) } + // X-1 further identities + when (userIds.size) { + 0, 1 -> {} + 2 -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("1 further identity") + else -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("${userIds.size - 1} further identities") + } + return headerMap + } + + /** + * Set the version header entry in the ASCII armor. + * If the version info is null or only contains whitespace characters, then the version header will be removed. + * + * @param armor armored output stream + * @param version version header. + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun setVersionHeader(armor: ArmoredOutputStream, version: String?) = + armor.setHeader(HEADER_VERSION, version?.let { it.ifBlank { null } }) + + /** + * Add an ASCII armor header entry about the used hash algorithm into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param hashAlgorithm hash algorithm + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addHashAlgorithmHeader(armor: ArmoredOutputStream, hashAlgorithm: HashAlgorithm) = + armor.addHeader(HEADER_HASH, hashAlgorithm.algorithmName) + + /** + * Add an ASCII armor comment header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param comment free-text comment + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addCommentHeader(armor: ArmoredOutputStream, comment: String) = + armor.addHeader(HEADER_COMMENT, comment) + + /** + * Add an ASCII armor message-id header entry into the [ArmoredOutputStream]. + * + * @param armor armored output stream + * @param messageId message id + * + * @see + * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + */ + @JvmStatic + @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + "Use ArmoredOutputStream builder instead.") + fun addMessageIdHeader(armor: ArmoredOutputStream, messageId: String) { + require(PATTER_MESSAGE_ID.matches(messageId)) { "MessageIDs MUST consist of 32 printable characters." } + armor.addHeader(HEADER_MESSAGEID, messageId) + } + + /** + * Extract all ASCII armor header values of type comment from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of comment headers + */ + @JvmStatic + fun getCommentHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_COMMENT) + + /** + * Extract all ASCII armor header values of type message id from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of message-id headers + */ + @JvmStatic + fun getMessageIdHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_MESSAGEID) + + /** + * Return all ASCII armor header values of type hash-algorithm from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash headers + */ + @JvmStatic + fun getHashHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_HASH) + + /** + * Return a list of [HashAlgorithm] enums extracted from the hash header entries of the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of hash algorithms from the ASCII header + */ + @JvmStatic + fun getHashAlgorithms(armor: ArmoredInputStream): List = + getHashHeaderValues(armor).mapNotNull { HashAlgorithm.fromName(it) } + + /** + * Return all ASCII armor header values of type version from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of version headers + */ + @JvmStatic + fun getVersionHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_VERSION) + + /** + * Return all ASCII armor header values of type charset from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @return list of charset headers + */ + @JvmStatic + fun getCharsetHeaderValues(armor: ArmoredInputStream): List = + getArmorHeaderValues(armor, HEADER_CHARSET) + + /** + * Return all ASCII armor header values of the given headerKey from the given [ArmoredInputStream]. + * + * @param armor armored input stream + * @param key ASCII armor header key + * @return list of values for the header key + */ + @JvmStatic + fun getArmorHeaderValues(armor: ArmoredInputStream, key: String): List = + armor.armorHeaders + .filter { it.startsWith("$key: ") } + .map { it.substring(key.length + 2) } // key.len + ": ".len + + /** + * Hacky workaround for #96. + * For `PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)` + * or `PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)` + * to read all PGPKeyRings properly, we apparently have to make sure that the [InputStream] that is given + * as constructor argument is a [PGPUtil.BufferedInputStreamExt]. + * Since [PGPUtil.getDecoderStream] will return an [org.bouncycastle.bcpg.ArmoredInputStream] + * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the + * end-result is a [PGPUtil.BufferedInputStreamExt]. + * + * @param inputStream input stream + * @return BufferedInputStreamExt + * + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun getDecoderStream(inputStream: InputStream): InputStream = + OpenPgpInputStream(inputStream).let { + if (it.isAsciiArmored) { + PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) + } else { + it + } + } + } +} \ No newline at end of file From 841b386226f278fdde17deb219d022cd7f9bfee7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 14:36:04 +0200 Subject: [PATCH 171/351] Kotlin conversion: MultiMap Warning: This commit changes the semantics of MultiMap.put() put() now replaces values, while plus() adds them. --- .../java/org/pgpainless/util/MultiMap.java | 137 ------------------ .../PublicKeyRingSelectionStrategy.java | 7 +- .../SecretKeyRingSelectionStrategy.java | 7 +- .../kotlin/org/pgpainless/util/MultiMap.kt | 75 ++++++++++ .../org/pgpainless/util/MultiMapTest.java | 84 +++++++---- .../keyring/KeyRingsFromCollectionTest.java | 14 +- 6 files changed, 147 insertions(+), 177 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java b/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java deleted file mode 100644 index 3b342778..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/MultiMap.java +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -public class MultiMap { - - private final Map> map; - - public MultiMap() { - map = new HashMap<>(); - } - - public MultiMap(@Nonnull MultiMap other) { - this.map = new HashMap<>(); - for (K k : other.map.keySet()) { - map.put(k, new LinkedHashSet<>(other.map.get(k))); - } - } - - public MultiMap(@Nonnull Map> content) { - this.map = new HashMap<>(content); - } - - public int size() { - return map.size(); - } - - public boolean isEmpty() { - return map.isEmpty(); - } - - public boolean containsKey(K o) { - return map.containsKey(o); - } - - public boolean containsValue(V o) { - for (Set values : map.values()) { - if (values.contains(o)) return true; - } - return false; - } - - public Set get(K o) { - return map.get(o); - } - - public void put(K k, V v) { - Set values = map.get(k); - if (values == null) { - values = new LinkedHashSet<>(); - map.put(k, values); - } - values.add(v); - } - - public void put(K k, Set vs) { - for (V v : vs) { - put(k, v); - } - } - - public void removeAll(K o) { - map.remove(o); - } - - public void remove(K o, V v) { - Set vs = map.get(o); - if (vs == null) return; - vs.remove(v); - } - - public void putAll(MultiMap other) { - for (K key : other.keySet()) { - put(key, other.get(key)); - } - } - - public void clear() { - map.clear(); - } - - public Set keySet() { - return map.keySet(); - } - - public Collection> values() { - return map.values(); - } - - public Set>> entrySet() { - return map.entrySet(); - } - - /** - * Return all values of the {@link MultiMap} in a single {@link LinkedHashSet}. - * - * @return set of all values - */ - public Set flatten() { - LinkedHashSet flattened = new LinkedHashSet<>(); - for (Set items : map.values()) { - flattened.addAll(items); - } - return flattened; - } - - @Override - public boolean equals(Object o) { - if (o == null) { - return false; - } - - if (!(o instanceof MultiMap)) { - return false; - } - - if (this == o) { - return true; - } - - return map.equals(((MultiMap) o).map); - } - - @Override - public int hashCode() { - return map.hashCode(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java index 68c9d946..7dbf7c93 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/PublicKeyRingSelectionStrategy.java @@ -7,6 +7,7 @@ package org.pgpainless.util.selection.keyring; import javax.annotation.Nonnull; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import org.bouncycastle.openpgp.PGPPublicKeyRing; @@ -33,9 +34,9 @@ public abstract class PublicKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (O identifier : keyRingCollections.keySet()) { - for (PGPPublicKeyRingCollection collection : keyRingCollections.get(identifier)) { - keyRings.put(identifier, selectKeyRingsFromCollection(identifier, collection)); + for (Map.Entry> entry : keyRingCollections.entrySet()) { + for (PGPPublicKeyRingCollection collection : entry.getValue()) { + keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); } } return keyRings; diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java index ac5e8065..9e57b575 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/selection/keyring/SecretKeyRingSelectionStrategy.java @@ -6,6 +6,7 @@ package org.pgpainless.util.selection.keyring; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; @@ -32,9 +33,9 @@ public abstract class SecretKeyRingSelectionStrategy implements KeyRingSelect @Override public MultiMap selectKeyRingsFromCollections(@Nonnull MultiMap keyRingCollections) { MultiMap keyRings = new MultiMap<>(); - for (O identifier : keyRingCollections.keySet()) { - for (PGPSecretKeyRingCollection collection : keyRingCollections.get(identifier)) { - keyRings.put(identifier, selectKeyRingsFromCollection(identifier, collection)); + for (Map.Entry> entry : keyRingCollections.entrySet()) { + for (PGPSecretKeyRingCollection collection : entry.getValue()) { + keyRings.plus(entry.getKey(), selectKeyRingsFromCollection(entry.getKey(), collection)); } } return keyRings; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt new file mode 100644 index 00000000..9ca193a6 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +class MultiMap : Iterable>> { + + private val map: Map> + + constructor(): this(mutableMapOf()) + constructor(other: MultiMap): this(other.map) + constructor(content: Map>) { + map = mutableMapOf() + content.forEach { + map[it.key] = it.value.toMutableSet() + } + } + + override fun iterator(): Iterator>> { + return map.iterator() + } + + val size: Int + get() = map.size + fun size() = size + val keys: Set + get() = map.keys + fun keySet() = keys + val values: Collection> + get() = map.values + fun values() = values + val entries: Set>> + get() = map.entries + fun entrySet() = entries + fun isEmpty(): Boolean = map.isEmpty() + fun containsKey(key: K): Boolean = map.containsKey(key) + fun containsValue(value: V): Boolean = map.values.any { it.contains(value) } + fun contains(key: K, value: V): Boolean = map[key]?.contains(value) ?: false + operator fun get(key: K): Set? = map[key] + fun put(key: K, value: V) = + (map as MutableMap).put(key, mutableSetOf(value)) + fun plus(key: K, value: V) = + (map as MutableMap).getOrPut(key) { mutableSetOf() }.add(value) + fun put(key: K, values: Set) = + (map as MutableMap).put(key, values.toMutableSet()) + fun plus(key: K, values: Set) = + (map as MutableMap).getOrPut(key) { mutableSetOf() }.addAll(values) + fun putAll(other: MultiMap) = other.map.entries.forEach { + put(it.key, it.value) + } + fun plusAll(other: MultiMap) = other.map.entries.forEach { + plus(it.key, it.value) + } + fun removeAll(key: K) = (map as MutableMap).remove(key) + fun remove(key: K, value: V) = (map as MutableMap)[key]?.remove(value) + fun clear() = (map as MutableMap).clear() + fun flatten() = map.flatMap { it.value }.toSet() + + override fun equals(other: Any?): Boolean { + return if (other == null) + false + else if (other !is MultiMap<*, *>) + false + else if (this === other) { + true + } else { + map == other.map + } + } + + override fun hashCode(): Int { + return map.hashCode() + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java index 98688a94..164befb1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/MultiMapTest.java @@ -41,7 +41,37 @@ public class MultiMapTest { assertTrue(multiMap.containsKey("alice")); assertTrue(multiMap.containsValue("wonderland")); assertNotNull(multiMap.get("alice")); - assertTrue(multiMap.get("alice").contains("wonderland")); + assertTrue(multiMap.contains("alice", "wonderland")); + } + + @Test + public void putOverwritesExistingElements() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.put("alice", "whothefrickisalice"); + assertFalse(map.containsValue("wonderland")); + } + + @Test + public void plusDoesNotOverwriteButAdd() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.plus("alice", "whothefrickisalice"); + assertTrue(map.containsValue("wonderland")); + assertTrue(map.containsValue("whothefrickisalice")); + } + + @Test + public void containsWorks() { + MultiMap map = new MultiMap<>(); + map.put("alice", "wonderland"); + map.plus("alice", "bar"); + map.put("bob", "builder"); + + assertTrue(map.contains("alice", "wonderland")); + assertTrue(map.contains("alice", "bar")); + assertTrue(map.contains("bob", "builder")); + assertFalse(map.contains("bob", "bar")); } @Test @@ -104,7 +134,7 @@ public class MultiMapTest { @Test public void emptyAfterClear() { MultiMap map = new MultiMap<>(); - map.put("test", "foo"); + map.plus("test", "foo"); assertFalse(map.isEmpty()); map.clear(); assertTrue(map.isEmpty()); @@ -113,8 +143,8 @@ public class MultiMapTest { @Test public void addTwoRemoveOneWorks() { MultiMap map = new MultiMap<>(); - map.put("alice", "wonderland"); - map.put("bob", "builder"); + map.plus("alice", "wonderland"); + map.plus("bob", "builder"); map.removeAll("alice"); assertFalse(map.containsKey("alice")); @@ -125,11 +155,11 @@ public class MultiMapTest { @Test public void addMultiValue() { MultiMap addOneByOne = new MultiMap<>(); - addOneByOne.put("foo", "bar"); - addOneByOne.put("foo", "baz"); + addOneByOne.plus("foo", "bar"); + addOneByOne.plus("foo", "baz"); MultiMap addOnce = new MultiMap<>(); - addOnce.put("foo", new HashSet<>(Arrays.asList("baz", "bar"))); + addOnce.plus("foo", new HashSet<>(Arrays.asList("baz", "bar"))); assertEquals(addOneByOne, addOnce); } @@ -138,7 +168,7 @@ public class MultiMapTest { public void addMultiValueRemoveSingle() { MultiMap map = new MultiMap<>(); map.put("foo", "bar"); - map.put("foo", "baz"); + map.plus("foo", "baz"); map.remove("foo", "bar"); assertFalse(map.isEmpty()); @@ -149,9 +179,9 @@ public class MultiMapTest { @Test public void addMultiValueRemoveAll() { MultiMap map = new MultiMap<>(); - map.put("foo", "bar"); - map.put("foo", "baz"); - map.put("bingo", "bango"); + map.plus("foo", "bar"); + map.plus("foo", "baz"); + map.plus("bingo", "bango"); map.removeAll("foo"); assertFalse(map.isEmpty()); @@ -160,23 +190,23 @@ public class MultiMapTest { } @Test - public void putAll() { + public void plusAll() { MultiMap map = new MultiMap<>(); - map.put("A", "1"); - map.put("A", "2"); - map.put("B", "1"); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); MultiMap other = new MultiMap<>(); - other.put("A", "1"); - other.put("B", "2"); - other.put("C", "3"); + other.plus("A", "1"); + other.plus("B", "2"); + other.plus("C", "3"); - map.putAll(other); - assertTrue(map.get("A").contains("1")); - assertTrue(map.get("A").contains("2")); - assertTrue(map.get("B").contains("1")); - assertTrue(map.get("B").contains("2")); - assertTrue(map.get("C").contains("3")); + map.plusAll(other); + assertTrue(map.contains("A", "1")); + assertTrue(map.contains("A", "2")); + assertTrue(map.contains("B", "1")); + assertTrue(map.contains("B", "2")); + assertTrue(map.contains("C", "3")); } @Test @@ -188,9 +218,9 @@ public class MultiMapTest { @Test public void flattenMap() { MultiMap map = new MultiMap<>(); - map.put("A", "1"); - map.put("A", "2"); - map.put("B", "1"); + map.plus("A", "1"); + map.plus("A", "2"); + map.plus("B", "1"); Set expected = new LinkedHashSet<>(); expected.add("1"); diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java index bf4955cd..c7f6d722 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/selection/keyring/KeyRingsFromCollectionTest.java @@ -5,7 +5,7 @@ package org.pgpainless.util.selection.keyring; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.util.Arrays; @@ -19,8 +19,8 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.junit.jupiter.api.Test; import org.pgpainless.key.TestKeys; -import org.pgpainless.util.selection.keyring.impl.ExactUserId; import org.pgpainless.util.MultiMap; +import org.pgpainless.util.selection.keyring.impl.ExactUserId; public class KeyRingsFromCollectionTest { @@ -52,7 +52,7 @@ public class KeyRingsFromCollectionTest { MultiMap selected = strategy.selectKeyRingsFromCollections(map); assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertNull(selected.get("invalidId")); + assertTrue(selected.get("invalidId").isEmpty()); } @Test @@ -73,16 +73,16 @@ public class KeyRingsFromCollectionTest { PGPPublicKeyRing juliet = TestKeys.getJulietPublicKeyRing(); MultiMap map = new MultiMap<>(); PGPPublicKeyRingCollection julietCollection = new PGPPublicKeyRingCollection(Arrays.asList(emil, juliet)); - map.put(TestKeys.JULIET_UID, julietCollection); + map.plus(TestKeys.JULIET_UID, julietCollection); PGPPublicKeyRingCollection emilCollection = new PGPPublicKeyRingCollection(Collections.singletonList(emil)); - map.put(TestKeys.EMIL_UID, emilCollection); + map.plus(TestKeys.EMIL_UID, emilCollection); assertEquals(2, julietCollection.size()); - map.put("invalidId", emilCollection); + map.plus("invalidId", emilCollection); PublicKeyRingSelectionStrategy strategy = new ExactUserId.PubRingSelectionStrategy(); MultiMap selected = strategy.selectKeyRingsFromCollections(map); assertEquals(1, selected.get(TestKeys.JULIET_UID).size()); assertEquals(1, selected.get(TestKeys.EMIL_UID).size()); - assertNull(selected.get("invalidId")); + assertTrue(selected.get("invalidId").isEmpty()); } } From 6b397a0d568488a5935c7516bb676c6b7b01747b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 28 Sep 2023 15:38:30 +0200 Subject: [PATCH 172/351] Kotlin conversion: SignaturePicker --- .../signature/consumer/SignaturePicker.java | 395 ------------------ .../extensions/PGPSignatureExtensions.kt | 4 + .../signature/consumer/SignaturePicker.kt | 310 ++++++++++++++ 3 files changed, 314 insertions(+), 395 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java deleted file mode 100644 index e6f2f755..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignaturePicker.java +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.CollectionUtils; - -/** - * Pick signatures from keys. - * - * The format of a V4 OpenPGP key is: - * - * Primary-Key - * [Revocation Self Signature] - * [Direct Key Signature...] - * User ID [Signature ...] - * [User ID [Signature ...] ...] - * [User Attribute [Signature ...] ...] - * [[Subkey [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] - */ -public final class SignaturePicker { - - private SignaturePicker() { - - } - - /** - * Pick the at validation date most recent valid key revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing key ring - * @param policy policy - * @param validationDate date of signature validation - * @return most recent, valid key revocation signature - */ - public static PGPSignature pickCurrentRevocationSelfSignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - List signatures = getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION); - PGPSignature mostCurrentValidSig = null; - - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifyKeyRevocationSignature(signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // Signature is not valid - continue; - } - mostCurrentValidSig = signature; - } - - return mostCurrentValidSig; - } - - /** - * Pick the at validationDate most recent, valid direct key signature. - * This method might return null, if there is no direct key self-signature which is valid at validationDate. - * - * @param keyRing key ring - * @param policy policy - * @param validationDate validation date - * @return direct-key self-signature - */ - public static PGPSignature pickCurrentDirectKeySelfSignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, validationDate); - } - - /** - * Pick the at validationDate, latest, valid direct key signature made by signingKey on signedKey. - * This method might return null, if there is no direct key self signature which is valid at validationDate. - * - * @param signingKey key that created the signature - * @param signedKey key that carries the signature - * @param policy policy - * @param validationDate validation date - * @return direct key sig - */ - public static PGPSignature pickCurrentDirectKeySignature(PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) { - List directKeySignatures = getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY); - - PGPSignature mostRecentDirectKeySigBySigningKey = null; - for (PGPSignature signature : directKeySignatures) { - try { - SignatureVerifier.verifyDirectKeySignature(signature, signingKey, signedKey, policy, validationDate); - } catch (SignatureValidationException e) { - // Direct key sig is not valid - continue; - } - mostRecentDirectKeySigBySigningKey = signature; - } - - return mostRecentDirectKeySigBySigningKey; - } - - /** - * Pick the at validationDate latest direct key signature. - * This method might return an expired signature. - * If there are more than one direct-key signature, and some of those are not expired, the latest non-expired - * yet already effective direct-key signature will be returned. - * - * @param keyRing key ring - * @param policy policy - * @param validationDate validation date - * @return latest direct key signature - */ - public static PGPSignature pickLatestDirectKeySignature(PGPKeyRing keyRing, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - return pickLatestDirectKeySignature(primaryKey, primaryKey, policy, validationDate); - } - - /** - * Pick the at validationDate latest direct key signature made by signingKey on signedKey. - * This method might return an expired signature. - * If a non-expired direct-key signature exists, the latest non-expired yet already effective direct-key - * signature will be returned. - * - * @param signingKey signing key (key that made the sig) - * @param signedKey signed key (key that carries the sig) - * @param policy policy - * @param validationDate date of validation - * @return latest direct key sig - */ - public static PGPSignature pickLatestDirectKeySignature(PGPPublicKey signingKey, PGPPublicKey signedKey, Policy policy, Date validationDate) { - List signatures = getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY); - - PGPSignature latestDirectKeySignature = null; - for (PGPSignature signature : signatures) { - try { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired - if (latestDirectKeySignature != null && !SignatureUtils.isSignatureExpired(latestDirectKeySignature, validationDate)) { - SignatureValidator.signatureIsNotYetExpired(validationDate).verify(signature); - } - SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); - } catch (SignatureValidationException e) { - // Direct key signature is not valid - continue; - } - latestDirectKeySignature = signature; - } - - return latestDirectKeySignature; - } - - /** - * Pick the at validationDate most recent, valid user-id revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing key ring - * @param userId user-Id that gets revoked - * @param policy policy - * @param validationDate validation date - * @return revocation signature - */ - public static PGPSignature pickCurrentUserIdRevocationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - List signatures = getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION); - - PGPSignature latestUserIdRevocation = null; - for (PGPSignature signature : signatures) { - PGPPublicKey signer = keyRing.getPublicKey(signature.getKeyID()); - if (signer == null) { - // Signature made by external key. Skip. - continue; - } - try { - SignatureVerifier.verifyUserIdRevocation(userId, signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // User-id revocation is not valid - continue; - } - latestUserIdRevocation = signature; - } - - return latestUserIdRevocation; - } - - /** - * Pick the at validationDate latest, valid certification self-signature for the given user-id. - * This method might return null, if there is no certification self signature for that user-id which is valid - * at validationDate. - * - * @param keyRing keyring - * @param userId userid - * @param policy policy - * @param validationDate validation date - * @return user-id certification - */ - public static PGPSignature pickCurrentUserIdCertificationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - Iterator userIdSigIterator = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigIterator); - - Collections.sort(signatures, new SignatureCreationDateComparator()); - - PGPSignature mostRecentUserIdCertification = null; - for (PGPSignature signature : signatures) { - if (primaryKey.getKeyID() != signature.getKeyID()) { - // Signature not made by primary key - continue; - } - try { - SignatureVerifier.verifyUserIdCertification(userId, signature, primaryKey, policy, validationDate); - } catch (SignatureValidationException e) { - // User-id certification is not valid - continue; - } - mostRecentUserIdCertification = signature; - } - - return mostRecentUserIdCertification; - } - - /** - * Pick the at validationDate latest certification self-signature for the given user-id. - * This method might return an expired signature. - * If a non-expired user-id certification signature exists, the latest non-expired yet already effective - * user-id certification signature for the given user-id will be returned. - * - * @param keyRing keyring - * @param userId userid - * @param policy policy - * @param validationDate validation date - * @return user-id certification - */ - public static PGPSignature pickLatestUserIdCertificationSignature(PGPKeyRing keyRing, String userId, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - - Iterator userIdSigIterator = primaryKey.getSignaturesForID(userId); - List signatures = CollectionUtils.iteratorToList(userIdSigIterator); - Collections.sort(signatures, new SignatureCreationDateComparator()); - - PGPSignature latestUserIdCert = null; - for (PGPSignature signature : signatures) { - try { - SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - SignatureValidator.correctSignatureOverUserId(userId, primaryKey, primaryKey).verify(signature); - } catch (SignatureValidationException e) { - // User-id certification is not valid - continue; - } - - latestUserIdCert = signature; - } - - return latestUserIdCert; - } - - /** - * Pick the at validationDate most recent, valid subkey revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. - * - * @param keyRing keyring - * @param subkey subkey - * @param policy policy - * @param validationDate validation date - * @return subkey revocation signature - */ - public static PGPSignature pickCurrentSubkeyBindingRevocationSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding revocations."); - } - - List signatures = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION); - PGPSignature latestSubkeyRevocation = null; - - for (PGPSignature signature : signatures) { - try { - SignatureVerifier.verifySubkeyBindingRevocation(signature, primaryKey, subkey, policy, validationDate); - } catch (SignatureValidationException e) { - // subkey binding revocation is not valid - continue; - } - latestSubkeyRevocation = signature; - } - - return latestSubkeyRevocation; - } - - /** - * Pick the at validationDate latest, valid subkey binding signature for the given subkey. - * This method might return null, if there is no subkey binding signature which is valid - * at validationDate. - * - * @param keyRing key ring - * @param subkey subkey - * @param policy policy - * @param validationDate date of validation - * @return most recent valid subkey binding signature - */ - public static PGPSignature pickCurrentSubkeyBindingSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding signature."); - } - - List subkeyBindingSigs = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING); - PGPSignature mostCurrentValidSig = null; - - for (PGPSignature signature : subkeyBindingSigs) { - try { - SignatureVerifier.verifySubkeyBindingSignature(signature, primaryKey, subkey, policy, validationDate); - } catch (SignatureValidationException validationException) { - // Subkey binding sig is not valid - continue; - } - mostCurrentValidSig = signature; - } - - return mostCurrentValidSig; - } - - /** - * Pick the at validationDate latest subkey binding signature for the given subkey. - * This method might return an expired signature. - * If a non-expired subkey binding signature exists, the latest non-expired yet already effective - * subkey binding signature for the given subkey will be returned. - * - * @param keyRing key ring - * @param subkey subkey - * @param policy policy - * @param validationDate validationDate - * @return subkey binding signature - */ - public static PGPSignature pickLatestSubkeyBindingSignature(PGPKeyRing keyRing, PGPPublicKey subkey, Policy policy, Date validationDate) { - PGPPublicKey primaryKey = keyRing.getPublicKey(); - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new IllegalArgumentException("Primary key cannot have subkey binding signature."); - } - - List signatures = getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING); - PGPSignature latestSubkeyBinding = null; - - for (PGPSignature signature : signatures) { - try { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsAlreadyEffective(validationDate).verify(signature); - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired - if (latestSubkeyBinding != null && !SignatureUtils.isSignatureExpired(latestSubkeyBinding, validationDate)) { - SignatureValidator.signatureIsNotYetExpired(validationDate).verify(signature); - } - SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); - } catch (SignatureValidationException e) { - // Subkey binding sig is not valid - continue; - } - latestSubkeyBinding = signature; - } - - return latestSubkeyBinding; - } - - /** - * Return a list of all signatures of the given {@link SignatureType} on the given key, sorted using a - * {@link SignatureCreationDateComparator}. - * - * The returned list will be sorted first by ascending signature creation time. - * - * @param key key - * @param type type of signatures which shall be collected and sorted - * @return sorted list of signatures - */ - private static List getSortedSignaturesOfType(PGPPublicKey key, SignatureType type) { - Iterator signaturesOfType = key.getSignaturesOfType(type.getCode()); - List signatureList = CollectionUtils.iteratorToList(signaturesOfType); - Collections.sort(signatureList, new SignatureCreationDateComparator()); - return signatureList; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index a27e68e0..4fe97bc7 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -5,6 +5,7 @@ package org.bouncycastle.extensions import openpgp.plusSeconds +import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.RevocationState import org.pgpainless.algorithm.SignatureType @@ -69,6 +70,9 @@ fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = false } +fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = + wasIssuedBy(OpenPgpFingerprint.of(key)) + /** * Return true, if this signature is a hard revocation. */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt new file mode 100644 index 00000000..9c5b9a8d --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt @@ -0,0 +1,310 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.extensions.getPublicKeyFor +import org.bouncycastle.extensions.hasPublicKey +import org.bouncycastle.extensions.isExpired +import org.bouncycastle.extensions.wasIssuedBy +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.policy.Policy +import java.util.Date +import kotlin.math.sign + +/** + * Pick signatures from keys. + * + * The format of a V4 OpenPGP key is: + * + * Primary-Key + * [Revocation Self Signature] + * [Direct Key Signature...] + * User ID [Signature ...] + * [User ID [Signature ...] ...] + * [User Attribute [Signature ...] ...] + * [[Subkey [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] + */ +class SignaturePicker { + + companion object { + + /** + * Pick the at validation date most recent valid key revocation signature. + * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after + * validationDate or if it is already expired. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime date of signature validation + * @return most recent, valid key revocation signature + */ + @JvmStatic + fun pickCurrentRevocationSelfSignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + return getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifyKeyRevocationSignature(it, primaryKey, policy, referenceTime) + true // valid + } catch (e : SignatureValidationException) { + false // not valid + } + } + } + + /** + * Pick the at validationDate most recent, valid direct key signature. + * This method might return null, if there is no direct key self-signature which is valid at validationDate. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime validation date + * @return direct-key self-signature + */ + @JvmStatic + fun pickCurrentDirectKeySelfSignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, referenceTime) + } + + @JvmStatic + fun pickCurrentDirectKeySignature(signingKey: PGPPublicKey, signedKey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifyDirectKeySignature(it, signingKey, signedKey, policy, referenceTime) + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest direct key signature. + * This method might return an expired signature. + * If there are more than one direct-key signature, and some of those are not expired, the latest non-expired + * yet already effective direct-key signature will be returned. + * + * @param keyRing key ring + * @param policy policy + * @param referenceTime validation date + * @return latest direct key signature + */ + @JvmStatic + fun pickLatestDirectKeySignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { + return pickLatestDirectKeySignature(keyRing.publicKey, keyRing.publicKey, policy, referenceTime) + } + + /** + * Pick the at validationDate latest direct key signature made by signingKey on signedKey. + * This method might return an expired signature. + * If a non-expired direct-key signature exists, the latest non-expired yet already effective direct-key + * signature will be returned. + * + * @param signingKey signing key (key that made the sig) + * @param signedKey signed key (key that carries the sig) + * @param policy policy + * @param referenceTime date of validation + * @return latest direct key sig + */ + @JvmStatic + fun pickLatestDirectKeySignature(signingKey: PGPPublicKey, signedKey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + var latest: PGPSignature? = null + return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { + try { + SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(it) + SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + if (latest != null && !latest!!.isExpired(referenceTime)) { + SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) + } + SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(it) + latest = it + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate most recent, valid user-id revocation signature. + * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after + * validationDate or if it is already expired. + * + * @param keyRing key ring + * @param userId user-Id that gets revoked + * @param policy policy + * @param referenceTime validation date + * @return revocation signature + */ + @JvmStatic + fun pickCurrentUserIdRevocationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + return getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION).lastOrNull { + keyRing.getPublicKeyFor(it) ?: return@lastOrNull false // signature made by external key. skip. + return@lastOrNull try { + SignatureVerifier.verifyUserIdRevocation(userId.toString(), it, primaryKey, policy, referenceTime) + true + } catch (e : SignatureValidationException) { + false // signature not valid + } + } + } + + /** + * Pick the at validationDate latest, valid certification self-signature for the given user-id. + * This method might return null, if there is no certification self signature for that user-id which is valid + * at validationDate. + * + * @param keyRing keyring + * @param userId userid + * @param policy policy + * @param referenceTime validation date + * @return user-id certification + */ + @JvmStatic + fun pickCurrentUserIdCertificationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + return primaryKey.getSignaturesForID(userId.toString()).asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull it.wasIssuedBy(primaryKey) && try { + SignatureVerifier.verifyUserIdCertification(userId.toString(), it, primaryKey, policy, referenceTime) + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest certification self-signature for the given user-id. + * This method might return an expired signature. + * If a non-expired user-id certification signature exists, the latest non-expired yet already effective + * user-id certification signature for the given user-id will be returned. + * + * @param keyRing keyring + * @param userId userid + * @param policy policy + * @param referenceTime validation date + * @return user-id certification + */ + @JvmStatic + fun pickLatestUserIdCertificationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + return primaryKey.getSignaturesForID(userId.toString()).asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull try { + SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(it) + SignatureValidator.signatureIsCertification().verify(it) + SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + SignatureValidator.correctSignatureOverUserId(userId.toString(), primaryKey, primaryKey).verify(it) + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate most recent, valid subkey revocation signature. + * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after + * validationDate or if it is already expired. + * + * @param keyRing keyring + * @param subkey subkey + * @param policy policy + * @param referenceTime validation date + * @return subkey revocation signature + */ + @JvmStatic + fun pickCurrentSubkeyBindingRevocationSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding revocations." } + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifySubkeyBindingRevocation(it, primaryKey, subkey, policy, referenceTime) + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest, valid subkey binding signature for the given subkey. + * This method might return null, if there is no subkey binding signature which is valid + * at validationDate. + * + * @param keyRing key ring + * @param subkey subkey + * @param policy policy + * @param referenceTime date of validation + * @return most recent valid subkey binding signature + */ + @JvmStatic + fun pickCurrentSubkeyBindingSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding signatures." } + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { + return@lastOrNull try { + SignatureVerifier.verifySubkeyBindingSignature(it, primaryKey, subkey, policy, referenceTime) + true + } catch (e : SignatureValidationException) { + false + } + } + } + + /** + * Pick the at validationDate latest subkey binding signature for the given subkey. + * This method might return an expired signature. + * If a non-expired subkey binding signature exists, the latest non-expired yet already effective + * subkey binding signature for the given subkey will be returned. + * + * @param keyRing key ring + * @param subkey subkey + * @param policy policy + * @param referenceTime validationDate + * @return subkey binding signature + */ + @JvmStatic + fun pickLatestSubkeyBindingSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + val primaryKey = keyRing.publicKey + require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding signatures." } + var latest: PGPSignature? = null + return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { + return@lastOrNull try { + SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(it) + SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) + SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + // if the currently latest signature is not yet expired, check if the next candidate is not yet expired + if (latest != null && !latest!!.isExpired(referenceTime)) { + SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) + } + SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(it) + latest = it + true + } catch (e : SignatureValidationException) { + false + } + } + } + + @JvmStatic + private fun getSortedSignaturesOfType(key: PGPPublicKey, type: SignatureType): List = + key.getSignaturesOfType(type.code).asSequence() + .sortedWith(SignatureCreationDateComparator()) + .toList() + } + +} \ No newline at end of file From e16376ca68fdfcb641903f7b7a414c0ab97ccb53 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Sep 2023 14:05:21 +0200 Subject: [PATCH 173/351] Kotlin conversion: ArmoredInputStreamFactory --- .../util/ArmoredInputStreamFactory.java | 43 ------------------- .../util/ArmoredInputStreamFactory.kt | 36 ++++++++++++++++ 2 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java deleted file mode 100644 index 77e9c236..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredInputStreamFactory.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; - -import javax.annotation.Nonnull; - -/** - * Factory class for instantiating preconfigured {@link ArmoredInputStream ArmoredInputStreams}. - * {@link #get(InputStream)} will return an {@link ArmoredInputStream} that is set up to properly detect CRC errors. - */ -public final class ArmoredInputStreamFactory { - - private ArmoredInputStreamFactory() { - - } - - /** - * Return an instance of {@link ArmoredInputStream} which will detect CRC errors. - * - * @param inputStream input stream - * @return armored input stream - * @throws IOException in case of an IO error - */ - @Nonnull - public static ArmoredInputStream get(@Nonnull InputStream inputStream) throws IOException { - if (inputStream instanceof CRCingArmoredInputStreamWrapper) { - return (ArmoredInputStream) inputStream; - } - if (inputStream instanceof ArmoredInputStream) { - return new CRCingArmoredInputStreamWrapper((ArmoredInputStream) inputStream); - } - - ArmoredInputStream armorIn = new ArmoredInputStream(inputStream); - return new CRCingArmoredInputStreamWrapper(armorIn); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt new file mode 100644 index 00000000..82d8f248 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.bcpg.ArmoredInputStream +import java.io.IOException +import java.io.InputStream + +/** + * Factory class for instantiating preconfigured [ArmoredInputStream] instances. + * [get] will return an [ArmoredInputStream] that is set up to properly detect CRC errors v4 style. + */ +class ArmoredInputStreamFactory { + + companion object { + + /** + * Return an instance of [ArmoredInputStream] which will detect CRC errors. + * + * @param inputStream input stream + * @return armored input stream + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun get(inputStream: InputStream): ArmoredInputStream { + return when (inputStream) { + is CRCingArmoredInputStreamWrapper -> inputStream + is ArmoredInputStream -> CRCingArmoredInputStreamWrapper(inputStream) + else -> CRCingArmoredInputStreamWrapper(ArmoredInputStream(inputStream)) + } + } + } +} \ No newline at end of file From aca884e9366b0e60654db4185db280fe262ac500 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Sep 2023 14:57:48 +0200 Subject: [PATCH 174/351] Kotlin conversion: ArmoredOutputStreamFactory Also allow configuration of CRC calculation for both input and output streams --- .../util/ArmoredOutputStreamFactory.java | 132 ------------------ .../ConsumerOptions.kt | 1 + .../encryption_signing/ProducerOptions.kt | 1 + .../util/ArmoredInputStreamFactory.kt | 12 +- .../util/ArmoredOutputStreamFactory.kt | 102 ++++++++++++++ 5 files changed, 114 insertions(+), 134 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java deleted file mode 100644 index 269f8674..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmoredOutputStreamFactory.java +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.pgpainless.encryption_signing.ProducerOptions; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Factory to create configured {@link ArmoredOutputStream ArmoredOutputStreams}. - * The configuration entails setting custom version and comment headers. - */ -public final class ArmoredOutputStreamFactory { - - /** - * Name of the program. - */ - public static final String PGPAINLESS = "PGPainless"; - private static String version = PGPAINLESS; - private static String[] comment = new String[0]; - - private ArmoredOutputStreamFactory() { - - } - - private static ArmoredOutputStream.Builder getBuilder() { - ArmoredOutputStream.Builder builder = ArmoredOutputStream.builder(); - builder.clearHeaders(); - if (version != null && !version.isEmpty()) { - builder.setVersion(version); - } - for (String comment : comment) { - builder.addComment(comment); - } - return builder; - } - - /** - * Wrap an {@link OutputStream} inside a preconfigured {@link ArmoredOutputStream}. - * - * @param outputStream inner stream - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream get(@Nonnull OutputStream outputStream) { - return getBuilder().build(outputStream); - } - - /** - * Return an instance of the {@link ArmoredOutputStream} which might have pre-populated armor headers. - * - * @param outputStream output stream - * @param options options - * @return armored output stream - */ - @Nonnull - public static ArmoredOutputStream get(@Nonnull OutputStream outputStream, @Nonnull ProducerOptions options) { - ArmoredOutputStream.Builder builder = getBuilder(); - if (options.isHideArmorHeaders()) { - builder.clearHeaders(); - } - if (options.hasVersion()) { - builder.setVersion(options.getVersion()); - } - if (options.hasComment()) { - builder.setComment(options.getComment()); - } - return builder.build(outputStream); - } - - /** - * Overwrite the version header of ASCII armors with a custom value. - * Newlines in the version info string result in multiple version header entries. - * If this is set to
null
, then the version header is omitted altogether. - * - * @param versionString version string - */ - public static void setVersionInfo(@Nullable String versionString) { - if (versionString == null) { - version = null; - return; - } - String trimmed = versionString.trim(); - if (trimmed.isEmpty()) { - version = null; - } else { - version = trimmed; - } - } - - /** - * Reset the version header to its default value of {@link #PGPAINLESS}. - */ - public static void resetVersionInfo() { - version = PGPAINLESS; - } - - /** - * Set a comment header value in the ASCII armor header. - * If the comment contains newlines, it will be split into multiple header entries. - * - * @see org.pgpainless.encryption_signing.ProducerOptions#setComment(String) for how to set comments for - * individual messages. - * - * @param commentString comment - */ - public static void setComment(@Nullable String commentString) { - if (commentString == null) { - throw new IllegalArgumentException("Comment cannot be null."); - } - String trimmed = commentString.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Comment cannot be empty."); - } - - String[] lines = commentString.split("\n"); - comment = lines; - } - - /** - * Reset to the default of no comment headers. - */ - public static void resetComment() { - comment = new String[0]; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index dbff0551..e0ec1fd5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -24,6 +24,7 @@ import java.util.* class ConsumerOptions { private var ignoreMDCErrors = false + var isDisableAsciiArmorCRC = false private var forceNonOpenPgpData = false private var verifyNotBefore: Date? = null private var verifyNotAfter: Date? = Date() diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt index bdc153e9..88345fd9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -20,6 +20,7 @@ class ProducerOptions private constructor( private var applyCRLFEncoding = false private var cleartextSigned = false private var _hideArmorHeaders = false + var isDisableAsciiArmorCRC = false private var _compressionAlgorithmOverride: CompressionAlgorithm = PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm private var asciiArmor = true diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt index 82d8f248..254b5c88 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt @@ -5,6 +5,7 @@ package org.pgpainless.util import org.bouncycastle.bcpg.ArmoredInputStream +import org.pgpainless.decryption_verification.ConsumerOptions import java.io.IOException import java.io.InputStream @@ -24,12 +25,19 @@ class ArmoredInputStreamFactory { * @throws IOException in case of an IO error */ @JvmStatic + @JvmOverloads @Throws(IOException::class) - fun get(inputStream: InputStream): ArmoredInputStream { + fun get(inputStream: InputStream, options: ConsumerOptions? = null): ArmoredInputStream { return when (inputStream) { is CRCingArmoredInputStreamWrapper -> inputStream is ArmoredInputStream -> CRCingArmoredInputStreamWrapper(inputStream) - else -> CRCingArmoredInputStreamWrapper(ArmoredInputStream(inputStream)) + else -> CRCingArmoredInputStreamWrapper( + ArmoredInputStream.builder().apply { + setParseForHeaders(true) + options?.let { + setIgnoreCRC(it.isDisableAsciiArmorCRC) + } + }.build(inputStream)) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt new file mode 100644 index 00000000..69a5520f --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.util + +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.pgpainless.encryption_signing.ProducerOptions +import java.io.OutputStream + +/** + * Factory to create configured [ArmoredOutputStream] instances. + * The configuration entails setting custom version and comment headers. + */ +class ArmoredOutputStreamFactory { + + companion object { + private const val PGPAINLESS = "PGPainless" + + @JvmStatic + private var version: String? = PGPAINLESS + private var comment: String? = null + + /** + * Return an instance of the [ArmoredOutputStream] which might have pre-populated armor headers. + * + * @param outputStream output stream + * @param options options + * @return armored output stream + */ + @JvmStatic + @JvmOverloads + fun get(outputStream: OutputStream, options: ProducerOptions? = null): ArmoredOutputStream { + val builder = ArmoredOutputStream.builder().apply { + // set fields defined in ArmoredOutputStreamFactory + if (!version.isNullOrBlank()) setVersion(version) + if (!comment.isNullOrBlank()) setComment(comment) + + // set (and potentially overwrite with) values from ProducerOptions + options?.let { + enableCRC(!it.isDisableAsciiArmorCRC) + if (it.isHideArmorHeaders) clearHeaders() + if (it.hasVersion()) setVersion(it.version) + if (it.hasComment()) addComment(it.comment) + // TODO: configure CRC + } + } + return get(outputStream, builder) + } + + /** + * Build an [ArmoredOutputStream] around the given [outputStream], configured according to the passed in + * [ArmoredOutputStream.Builder] instance. + * + * @param outputStream output stream + * @param builder builder instance + */ + @JvmStatic + fun get(outputStream: OutputStream, builder: ArmoredOutputStream.Builder): ArmoredOutputStream { + return builder.build(outputStream) + } + + /** + * Overwrite the version header of ASCII armors with a custom value. + * Newlines in the version info string result in multiple version header entries. + * If this is set to
null
, then the version header is omitted altogether. + * + * @param versionString version string + */ + @JvmStatic + fun setVersionInfo(versionString: String?) { + version = if (versionString.isNullOrBlank()) null else versionString.trim() + } + + /** + * Reset the version header to its default value of [PGPAINLESS]. + */ + @JvmStatic + fun resetVersionInfo() { + version = PGPAINLESS + } + + /** + * Set a comment header value in the ASCII armor header. + * If the comment contains newlines, it will be split into multiple header entries. + * + * @see [ProducerOptions.setComment] for how to set comments for individual messages. + * + * @param commentString comment + */ + @JvmStatic + fun setComment(commentString: String) { + require(commentString.isNotBlank()) { "Comment cannot be empty. See resetComment() to clear the comment." } + comment = commentString.trim() + } + + @JvmStatic + fun resetComment() { + comment = null + } + } +} \ No newline at end of file From 8382da923d931bfc904a6eea6dda46d15cc84260 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Sep 2023 14:58:25 +0200 Subject: [PATCH 175/351] Add TODO to CRCinArmoredInputStreamWrapper --- .../org/pgpainless/util/CRCingArmoredInputStreamWrapper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java index 2c43339b..d2393be3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java @@ -17,6 +17,8 @@ import javax.annotation.Nonnull; * * Furthermore, this class swallows exceptions from BC's ArmoredInputStream that are caused * by missing CRC checksums. + * + * TODO: Validate whether this class is still needed. */ public class CRCingArmoredInputStreamWrapper extends ArmoredInputStream { From d707dcf74a8aa78b8c5e524f5481220aa5ba96a8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Oct 2023 13:16:06 +0200 Subject: [PATCH 176/351] Move now unused utility classes to test directory --- .../{main => test}/java/org/pgpainless/util/CollectionUtils.java | 0 .../src/{main => test}/java/org/pgpainless/util/Tuple.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename pgpainless-core/src/{main => test}/java/org/pgpainless/util/CollectionUtils.java (100%) rename pgpainless-core/src/{main => test}/java/org/pgpainless/util/Tuple.java (100%) diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java b/pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/util/CollectionUtils.java rename to pgpainless-core/src/test/java/org/pgpainless/util/CollectionUtils.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java rename to pgpainless-core/src/test/java/org/pgpainless/util/Tuple.java From 1cdce5c93aca3d259079d1c8e4c428c24914a520 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Oct 2023 14:18:26 +0200 Subject: [PATCH 177/351] Kotlin conversion: ImplementationFactory classes --- .../BcImplementationFactory.java | 154 ------------------ .../implementation/ImplementationFactory.java | 119 -------------- .../JceImplementationFactory.java | 141 ---------------- .../implementation/package-info.java | 8 - .../PublicKeyParameterValidationUtil.java | 2 +- .../consumer/SignatureValidator.java | 10 +- .../signature/consumer/SignatureVerifier.java | 2 +- .../implementation/BcImplementationFactory.kt | 96 +++++++++++ .../implementation/ImplementationFactory.kt | 93 +++++++++++ .../JceImplementationFactory.kt | 102 ++++++++++++ ...artyCertificationSignatureBuilderTest.java | 2 +- 11 files changed, 299 insertions(+), 430 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java deleted file mode 100644 index cbc320e6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/BcImplementationFactory.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter; -import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.Passphrase; - -public class BcImplementationFactory extends ImplementationFactory { - - @Override - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase) { - return new BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) - .build(passphrase.getChars()); - } - - @Override - public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) { - return new BcPBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) - .build(passphrase.getChars()); - } - - @Override - public BcPGPDigestCalculatorProvider getPGPDigestCalculatorProvider() { - return new BcPGPDigestCalculatorProvider(); - } - - @Override - public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { - return new BcPGPContentVerifierBuilderProvider(); - } - - @Override - public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { - return new BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm); - } - - @Override - public KeyFingerPrintCalculator getKeyFingerprintCalculator() { - return new BcKeyFingerprintCalculator(); - } - - @Override - public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) { - return new BcPBEDataDecryptorFactory(passphrase.getChars(), getPGPDigestCalculatorProvider()); - } - - @Override - public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { - return new BcPublicKeyDataDecryptorFactory(privateKey); - } - - @Override - public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new BcSessionKeyDataDecryptorFactory(sessionKey); - } - - @Override - public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { - return new BcPublicKeyKeyEncryptionMethodGenerator(key); - } - - @Override - public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { - return new BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()); - } - - @Override - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { - return new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm); - } - - @Override - public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) - throws PGPException { - return new BcPGPKeyPair(algorithm.getAlgorithmId(), jceToBcKeyPair(algorithm, keyPair, creationDate), creationDate); - } - - @Override - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { - return new BcPBESecretKeyEncryptorBuilder( - encryptionAlgorithm.getAlgorithmId(), - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .build(passphrase.getChars()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { - return new BcPGPObjectFactory(bytes); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(InputStream inputStream) { - return new BcPGPObjectFactory(inputStream); - } - - private AsymmetricCipherKeyPair jceToBcKeyPair(PublicKeyAlgorithm algorithm, - KeyPair keyPair, - Date creationDate) throws PGPException { - BcPGPKeyConverter converter = new BcPGPKeyConverter(); - - PGPKeyPair pair = new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); - AsymmetricKeyParameter publicKey = converter.getPublicKey(pair.getPublicKey()); - AsymmetricKeyParameter privateKey = converter.getPrivateKey(pair.getPrivateKey()); - - return new AsymmetricCipherKeyPair(publicKey, privateKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java deleted file mode 100644 index 90d1330f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/ImplementationFactory.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; - -public abstract class ImplementationFactory { - - private static ImplementationFactory FACTORY_IMPLEMENTATION; - - public static void setFactoryImplementation(ImplementationFactory implementation) { - FACTORY_IMPLEMENTATION = implementation; - } - - public static ImplementationFactory getInstance() { - if (FACTORY_IMPLEMENTATION == null) { - FACTORY_IMPLEMENTATION = new BcImplementationFactory(); - } - return FACTORY_IMPLEMENTATION; - } - - public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, - PGPDigestCalculator digestCalculator, - Passphrase passphrase); - - public abstract PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException; - - public PGPDigestCalculator getV4FingerprintCalculator() throws PGPException { - return getPGPDigestCalculator(HashAlgorithm.SHA1); - } - - public PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException { - return getPGPDigestCalculator(algorithm.getAlgorithmId()); - } - - public PGPDigestCalculator getPGPDigestCalculator(int algorithm) throws PGPException { - return getPGPDigestCalculatorProvider().get(algorithm); - } - - public abstract PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() throws PGPException; - - public abstract PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider(); - - public PGPContentSignerBuilder getPGPContentSignerBuilder(PublicKeyAlgorithm keyAlgorithm, HashAlgorithm hashAlgorithm) { - return getPGPContentSignerBuilder(keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId()); - } - - public abstract PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm); - - public abstract KeyFingerPrintCalculator getKeyFingerprintCalculator(); - - public abstract PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) throws PGPException; - - public abstract PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey); - - public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(SessionKey sessionKey) { - PGPSessionKey pgpSessionKey = new PGPSessionKey( - sessionKey.getAlgorithm().getAlgorithmId(), - sessionKey.getKey() - ); - return getSessionKeyDataDecryptorFactory(pgpSessionKey); - } - - public abstract SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey); - - public abstract PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key); - - public abstract PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase); - - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm symmetricKeyAlgorithm) { - return getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); - } - - public abstract PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm); - - public abstract PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException; - - public abstract PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, - HashAlgorithm hashAlgorithm, int s2kCount, - Passphrase passphrase) throws PGPException; - - public abstract PGPObjectFactory getPGPObjectFactory(InputStream inputStream); - - public abstract PGPObjectFactory getPGPObjectFactory(byte[] bytes); - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java deleted file mode 100644 index 10760de2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/JceImplementationFactory.java +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.implementation; - -import java.io.InputStream; -import java.security.KeyPair; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyPair; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSessionKey; -import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.PGPDigestCalculator; -import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.provider.ProviderFactory; -import org.pgpainless.util.Passphrase; - -public class JceImplementationFactory extends ImplementationFactory { - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm symmetricKeyAlgorithm, PGPDigestCalculator digestCalculator, Passphrase passphrase) { - return new JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId(), digestCalculator) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException { - return new JcePBESecretKeyDecryptorBuilder(getPGPDigestCalculatorProvider()) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PGPDigestCalculatorProvider getPGPDigestCalculatorProvider() - throws PGPException { - return new JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.getProvider()) - .build(); - } - - public PGPContentVerifierBuilderProvider getPGPContentVerifierBuilderProvider() { - return new JcaPGPContentVerifierBuilderProvider() - .setProvider(ProviderFactory.getProvider()); - } - - public PGPContentSignerBuilder getPGPContentSignerBuilder(int keyAlgorithm, int hashAlgorithm) { - return new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) - .setProvider(ProviderFactory.getProvider()); - } - - public KeyFingerPrintCalculator getKeyFingerprintCalculator() { - return new JcaKeyFingerprintCalculator() - .setProvider(ProviderFactory.getProvider()); - } - - public PBEDataDecryptorFactory getPBEDataDecryptorFactory(Passphrase passphrase) - throws PGPException { - return new JcePBEDataDecryptorFactoryBuilder(getPGPDigestCalculatorProvider()) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - public PublicKeyDataDecryptorFactory getPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey) { - return new JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.getProvider()) - .build(privateKey); - } - - @Override - public SessionKeyDataDecryptorFactory getSessionKeyDataDecryptorFactory(PGPSessionKey sessionKey) { - return new JceSessionKeyDataDecryptorFactoryBuilder() - .build(sessionKey); - } - - public PublicKeyKeyEncryptionMethodGenerator getPublicKeyKeyEncryptionMethodGenerator(PGPPublicKey key) { - return new JcePublicKeyKeyEncryptionMethodGenerator(key) - .setProvider(ProviderFactory.getProvider()); - } - - public PBEKeyEncryptionMethodGenerator getPBEKeyEncryptionMethodGenerator(Passphrase passphrase) { - return new JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) - .setProvider(ProviderFactory.getProvider()); - } - - public PGPDataEncryptorBuilder getPGPDataEncryptorBuilder(int symmetricKeyAlgorithm) { - return new JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) - .setProvider(ProviderFactory.getProvider()); - } - - public PGPKeyPair getPGPKeyPair(PublicKeyAlgorithm algorithm, KeyPair keyPair, Date creationDate) throws PGPException { - return new JcaPGPKeyPair(algorithm.getAlgorithmId(), keyPair, creationDate); - } - - public PBESecretKeyEncryptor getPBESecretKeyEncryptor(SymmetricKeyAlgorithm encryptionAlgorithm, HashAlgorithm hashAlgorithm, int s2kCount, Passphrase passphrase) throws PGPException { - return new JcePBESecretKeyEncryptorBuilder( - encryptionAlgorithm.getAlgorithmId(), - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .setProvider(ProviderFactory.getProvider()) - .build(passphrase.getChars()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(InputStream inputStream) { - return new PGPObjectFactory(inputStream, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - @Override - public PGPObjectFactory getPGPObjectFactory(byte[] bytes) { - return new PGPObjectFactory(bytes, ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java deleted file mode 100644 index 3ce87531..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/implementation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation factory classes to be able to switch out the underlying crypto engine implementation. - */ -package org.pgpainless.implementation; diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index 344f063b..1649f578 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -120,7 +120,7 @@ public class PublicKeyParameterValidationUtil { signatureGenerator.update(data); PGPSignature sig = signatureGenerator.generate(); - sig.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), publicKey); + sig.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), publicKey); sig.update(data); return sig.verify(); } catch (PGPException e) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 8ab31b2f..18bf5883 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -495,7 +495,7 @@ public abstract class SignatureValidator { } try { signature.init(ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(), primaryKey); + .getPgpContentVerifierBuilderProvider(), primaryKey); boolean valid = signature.verifyCertification(primaryKey, subkey); if (!valid) { throw new SignatureValidationException("Signature is not correct."); @@ -519,7 +519,7 @@ public abstract class SignatureValidator { @Override public void verify(PGPSignature signature) throws SignatureValidationException { try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), subkey); + signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), subkey); boolean valid = signature.verifyCertification(primaryKey, subkey); if (!valid) { throw new SignatureValidationException("Primary Key Binding Signature is not correct."); @@ -544,7 +544,7 @@ public abstract class SignatureValidator { @Override public void verify(PGPSignature signature) throws SignatureValidationException { try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signer); + signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), signer); boolean valid; if (signer.getKeyID() == signee.getKeyID() || signature.getSignatureType() == PGPSignature.DIRECT_KEY) { valid = signature.verifyCertification(signee); @@ -615,7 +615,7 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { try { signature.init(ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(), certifyingKey); + .getPgpContentVerifierBuilderProvider(), certifyingKey); boolean valid = signature.verifyCertification(userId, certifiedKey); if (!valid) { throw new SignatureValidationException("Signature over user-id '" + userId + @@ -645,7 +645,7 @@ public abstract class SignatureValidator { public void verify(PGPSignature signature) throws SignatureValidationException { try { signature.init(ImplementationFactory.getInstance() - .getPGPContentVerifierBuilderProvider(), certifyingKey); + .getPgpContentVerifierBuilderProvider(), certifyingKey); boolean valid = signature.verifyCertification(userAttributes, certifiedKey); if (!valid) { throw new SignatureValidationException("Signature over user-attribute vector is not correct."); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java index c4565197..a55037e5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java @@ -384,7 +384,7 @@ public final class SignatureVerifier { PGPPublicKey signingKey) throws SignatureValidationException { try { - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); + signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), signingKey); int read; byte[] buf = new byte[8192]; byte lastByte = -1; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt new file mode 100644 index 00000000..fec3a550 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory +import org.bouncycastle.openpgp.operator.* +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyEncryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter +import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.bc.BcSessionKeyDataDecryptorFactory +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.util.Passphrase +import java.io.InputStream +import java.security.KeyPair +import java.util.* + +class BcImplementationFactory : ImplementationFactory() { + override val pgpDigestCalculatorProvider: BcPGPDigestCalculatorProvider = BcPGPDigestCalculatorProvider() + override val pgpContentVerifierBuilderProvider: BcPGPContentVerifierBuilderProvider = BcPGPContentVerifierBuilderProvider() + override val keyFingerprintCalculator: BcKeyFingerprintCalculator = BcKeyFingerprintCalculator() + + override fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .build(passphrase.getChars()) + + override fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .build(passphrase.getChars()) + + override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = + BcPBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) + .build(passphrase.getChars()) + + override fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder = + BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + + override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = + BcPBEDataDecryptorFactory(passphrase.getChars(), pgpDigestCalculatorProvider) + + override fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory = + BcPublicKeyDataDecryptorFactory(privateKey) + + override fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory = + BcSessionKeyDataDecryptorFactory(sessionKey) + + override fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator = + BcPublicKeyKeyEncryptionMethodGenerator(key) + + override fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator = + BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()) + + override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = + BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm) + + override fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair = + BcPGPKeyPair( + publicKeyAlgorithm.algorithmId, + jceToBcKeyPair(publicKeyAlgorithm, keyPair, creationDate), + creationDate) + + override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = BcPGPObjectFactory(inputStream) + + private fun jceToBcKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date): AsymmetricCipherKeyPair = + BcPGPKeyConverter().let { converter -> + JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate).let { pair -> + AsymmetricCipherKeyPair(converter.getPublicKey(pair.publicKey), converter.getPrivateKey(pair.privateKey)) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt new file mode 100644 index 00000000..5ae653b4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.* +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.util.Passphrase +import org.pgpainless.util.SessionKey +import java.io.InputStream +import java.security.KeyPair +import java.util.* + +abstract class ImplementationFactory { + + companion object { + @JvmStatic + private var instance: ImplementationFactory = BcImplementationFactory() + + @JvmStatic + fun getInstance() = instance + + @JvmStatic + fun setFactoryImplementation(implementation: ImplementationFactory) = apply { + instance = implementation + } + } + + abstract val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider + abstract val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider + abstract val keyFingerprintCalculator: KeyFingerPrintCalculator + + val v4FingerprintCalculator: PGPDigestCalculator + get() = getPGPDigestCalculator(HashAlgorithm.SHA1) + + @Throws(PGPException::class) + abstract fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase): PBESecretKeyEncryptor + + @Throws(PGPException::class) + abstract fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor + + @Throws(PGPException::class) + abstract fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, hashAlgorithm: HashAlgorithm, + s2kCount: Int, passphrase: Passphrase): PBESecretKeyEncryptor + + fun getPGPDigestCalculator(hashAlgorithm: HashAlgorithm): PGPDigestCalculator = + getPGPDigestCalculator(hashAlgorithm.algorithmId) + + fun getPGPDigestCalculator(hashAlgorithm: Int): PGPDigestCalculator = + pgpDigestCalculatorProvider.get(hashAlgorithm) + + fun getPGPContentSignerBuilder(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm): PGPContentSignerBuilder = + getPGPContentSignerBuilder(keyAlgorithm.algorithmId, hashAlgorithm.algorithmId) + + abstract fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder + + @Throws(PGPException::class) + abstract fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory + + abstract fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory + + fun getSessionKeyDataDecryptorFactory(sessionKey: SessionKey): SessionKeyDataDecryptorFactory = + getSessionKeyDataDecryptorFactory(PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)) + + abstract fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory + + abstract fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator + + abstract fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator + + fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: SymmetricKeyAlgorithm): PGPDataEncryptorBuilder = + getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.algorithmId) + + abstract fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder + + @Throws(PGPException::class) + abstract fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair + + fun getPGPObjectFactory(bytes: ByteArray): PGPObjectFactory = + getPGPObjectFactory(bytes.inputStream()) + + abstract fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory + + override fun toString(): String { + return javaClass.simpleName + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt new file mode 100644 index 00000000..0684fa24 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.implementation + +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.* +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair +import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator +import org.bouncycastle.openpgp.operator.jcajce.JceSessionKeyDataDecryptorFactoryBuilder +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.provider.ProviderFactory +import org.pgpainless.util.Passphrase +import java.io.InputStream +import java.security.KeyPair +import java.util.* + +class JceImplementationFactory : ImplementationFactory() { + override val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider = + JcaPGPDigestCalculatorProviderBuilder() + .setProvider(ProviderFactory.getProvider()) + .build() + override val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider = + JcaPGPContentVerifierBuilderProvider() + .setProvider(ProviderFactory.getProvider()) + override val keyFingerprintCalculator: KeyFingerPrintCalculator = + JcaKeyFingerprintCalculator() + .setProvider(ProviderFactory.getProvider()) + + override fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()) + + override fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, + getPGPDigestCalculator(hashAlgorithm), + s2kCount) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()) + + override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = + JcePBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()) + + override fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder = + JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + .setProvider(ProviderFactory.getProvider()) + + override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = + JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.getProvider()) + .build(passphrase.getChars()) + + override fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory = + JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(privateKey) + + override fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory = + JceSessionKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.getProvider()) + .build(sessionKey) + + override fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator = + JcePublicKeyKeyEncryptionMethodGenerator(key) + .setProvider(ProviderFactory.getProvider()) + + override fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator = + JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) + .setProvider(ProviderFactory.getProvider()) + + override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = + JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) + .setProvider(ProviderFactory.getProvider()) + + override fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair = + JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate) + + override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = + PGPObjectFactory(inputStream, keyFingerprintCalculator) +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java index bf1cb694..2b0f4d35 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilderTest.java @@ -68,7 +68,7 @@ public class ThirdPartyCertificationSignatureBuilderTest { assertFalse(exportable.isExportable()); // test sig correctness - certification.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), secretKeys.getPublicKey()); + certification.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), secretKeys.getPublicKey()); assertTrue(certification.verifyCertification("Bob", bobsPublicKeys.getPublicKey())); } } From 83512236148db656e8afaf9298e28e17a0d11596 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 4 Oct 2023 14:45:45 +0200 Subject: [PATCH 178/351] Kotlin conversion: PublicKeyParameterValidationUtil --- .../PublicKeyParameterValidationUtil.java | 291 ------------------ .../util/PublicKeyParameterValidationUtil.kt | 246 +++++++++++++++ 2 files changed, 246 insertions(+), 291 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java deleted file mode 100644 index 1649f578..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.security.SecureRandom; - -import org.bouncycastle.bcpg.BCPGKey; -import org.bouncycastle.bcpg.DSAPublicBCPGKey; -import org.bouncycastle.bcpg.DSASecretBCPGKey; -import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; -import org.bouncycastle.bcpg.EdSecretBCPGKey; -import org.bouncycastle.bcpg.ElGamalPublicBCPGKey; -import org.bouncycastle.bcpg.ElGamalSecretBCPGKey; -import org.bouncycastle.bcpg.RSAPublicBCPGKey; -import org.bouncycastle.bcpg.RSASecretBCPGKey; -import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.util.Arrays; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.exception.KeyIntegrityException; -import org.pgpainless.implementation.ImplementationFactory; - -/** - * Utility class to verify keys against Key Overwriting (KO) attacks. - * This class of attacks is only possible if the attacker has access to the (encrypted) secret key material. - * To execute the attack, they would modify the unauthenticated parameters of the users public key. - * Using the modified public key in combination with the unmodified secret key material can then lead to the - * extraction of secret key parameters via weakly crafted messages. - * - * @see Key Overwriting (KO) Attacks against OpenPGP - */ -public class PublicKeyParameterValidationUtil { - - public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey) - throws KeyIntegrityException { - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); - boolean valid = true; - - // Algorithm specific validations - BCPGKey key = privateKey.getPrivateKeyDataPacket(); - if (key instanceof RSASecretBCPGKey) { - valid = verifyRSAKeyIntegrity( - (RSASecretBCPGKey) key, - (RSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof EdSecretBCPGKey) { - valid = verifyEdDsaKeyIntegrity( - (EdSecretBCPGKey) key, - (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof DSASecretBCPGKey) { - valid = verifyDsaKeyIntegrity( - (DSASecretBCPGKey) key, - (DSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } else if (key instanceof ElGamalSecretBCPGKey) { - valid = verifyElGamalKeyIntegrity( - (ElGamalSecretBCPGKey) key, - (ElGamalPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) - && valid; - } - - if (!valid) { - throw new KeyIntegrityException(); - } - - // Additional to the algorithm-specific tests further above, we also perform - // generic functionality tests with the key, such as whether it is able to decrypt encrypted data - // or verify signatures. - // These tests should be more or less constant time. - if (publicKeyAlgorithm.isSigningCapable()) { - valid = verifyCanSign(privateKey, publicKey); - } - if (publicKeyAlgorithm.isEncryptionCapable()) { - valid = verifyCanDecrypt(privateKey, publicKey) && valid; - } - - if (!valid) { - throw new KeyIntegrityException(); - } - } - - /** - * Verify that the public key can be used to successfully verify a signature made by the private key. - * @param privateKey private key - * @param publicKey public key - * @return false if signature verification fails - */ - private static boolean verifyCanSign(PGPPrivateKey privateKey, PGPPublicKey publicKey) { - SecureRandom random = new SecureRandom(); - PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.getAlgorithm()); - PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKeyAlgorithm, HashAlgorithm.SHA256) - ); - - try { - signatureGenerator.init(SignatureType.TIMESTAMP.getCode(), privateKey); - - byte[] data = new byte[512]; - random.nextBytes(data); - - signatureGenerator.update(data); - PGPSignature sig = signatureGenerator.generate(); - - sig.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), publicKey); - sig.update(data); - return sig.verify(); - } catch (PGPException e) { - return false; - } - } - - /** - * Verify that the public key can be used to encrypt a message which can successfully be - * decrypted using the private key. - * @param privateKey private key - * @param publicKey public key - * @return false if decryption of a message encrypted with the public key fails - */ - private static boolean verifyCanDecrypt(PGPPrivateKey privateKey, PGPPublicKey publicKey) { - SecureRandom random = new SecureRandom(); - PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator( - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256) - ); - encryptedDataGenerator.addMethod( - ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey)); - - byte[] data = new byte[1024]; - random.nextBytes(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - OutputStream outputStream = encryptedDataGenerator.open(out, new byte[1024]); - outputStream.write(data); - encryptedDataGenerator.close(); - PGPEncryptedDataList encryptedDataList = new PGPEncryptedDataList(out.toByteArray()); - PublicKeyDataDecryptorFactory decryptorFactory = - ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); - PGPPublicKeyEncryptedData encryptedData = - (PGPPublicKeyEncryptedData) encryptedDataList.getEncryptedDataObjects().next(); - InputStream decrypted = encryptedData.getDataStream(decryptorFactory); - out = new ByteArrayOutputStream(); - Streams.pipeAll(decrypted, out); - decrypted.close(); - } catch (IOException | PGPException e) { - return false; - } - - return Arrays.constantTimeAreEqual(data, out.toByteArray()); - } - - private static boolean verifyEdDsaKeyIntegrity(EdSecretBCPGKey privateKey, EdDSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // TODO: Implement - return true; - } - - private static boolean verifyDsaKeyIntegrity(DSASecretBCPGKey privateKey, DSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // Not sure what value to put here in order to have a "robust" primality check - // I went with 40, since that's what SO recommends: - // https://stackoverflow.com/a/6330138 - final int certainty = 40; - BigInteger pG = publicKey.getG(); - BigInteger pP = publicKey.getP(); - BigInteger pQ = publicKey.getQ(); - BigInteger pY = publicKey.getY(); - BigInteger sX = privateKey.getX(); - - boolean pPrime = pP.isProbablePrime(certainty); - if (!pPrime) { - return false; - } - - boolean qPrime = pQ.isProbablePrime(certainty); - if (!qPrime) { - return false; - } - - // q > 160 bits - boolean qLarge = pQ.bitLength() > 160; - if (!qLarge) { - return false; - } - - // q divides p - 1 - boolean qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ).equals(BigInteger.ZERO); - if (!qDividesPminus1) { - return false; - } - - // 1 < g < p - boolean gInBounds = BigInteger.ONE.max(pG).equals(pG) && pG.max(pP).equals(pP); - if (!gInBounds) { - return false; - } - - // g^q = 1 mod p - boolean gPowXModPEquals1 = pG.modPow(pQ, pP).equals(BigInteger.ONE); - if (!gPowXModPEquals1) { - return false; - } - - // y = g^x mod p - boolean yEqualsGPowXModP = pY.equals(pG.modPow(sX, pP)); - if (!yEqualsGPowXModP) { - return false; - } - - return true; - } - - private static boolean verifyRSAKeyIntegrity(RSASecretBCPGKey secretKey, RSAPublicBCPGKey publicKey) - throws KeyIntegrityException { - // Verify that the public keys N is equal to private keys p*q - return publicKey.getModulus().equals(secretKey.getPrimeP().multiply(secretKey.getPrimeQ())); - } - - /** - * Validate ElGamal public key parameters. - * - * Original implementation by the openpgpjs authors: - * Key Overwriting (KO) Attacks against OpenPGP + */ +class PublicKeyParameterValidationUtil { + + companion object { + @JvmStatic + @Throws(KeyIntegrityException::class) + fun verifyPublicKeyParameterIntegrity(privateKey: PGPPrivateKey, publicKey: PGPPublicKey) { + val algorithm = publicKey.publicKeyAlgorithm + var valid = true + + val key = privateKey.privateKeyDataPacket + when (privateKey.privateKeyDataPacket) { + is RSASecretBCPGKey -> + valid = verifyRSAKeyIntegrity(key as RSASecretBCPGKey, publicKey.publicKeyPacket.key as RSAPublicBCPGKey) + is EdSecretBCPGKey -> + valid = verifyEdDsaKeyIntegrity(key as EdSecretBCPGKey, publicKey.publicKeyPacket.key as EdDSAPublicBCPGKey) + is DSASecretBCPGKey -> + valid = verifyDsaKeyIntegrity(key as DSASecretBCPGKey, publicKey.publicKeyPacket.key as DSAPublicBCPGKey) + is ElGamalSecretBCPGKey -> + valid = verifyElGamalKeyIntegrity(key as ElGamalSecretBCPGKey, publicKey.publicKeyPacket.key as ElGamalPublicBCPGKey) + } + + if (!valid) throw KeyIntegrityException() + + // Additional to the algorithm-specific tests further above, we also perform + // generic functionality tests with the key, such as whether it is able to decrypt encrypted data + // or verify signatures. + // These tests should be more or less constant time. + if (algorithm.isSigningCapable()) { + valid = verifyCanSign(privateKey, publicKey) + } + if (algorithm.isEncryptionCapable()) { + valid = valid and verifyCanDecrypt(privateKey, publicKey) + } + + if (!valid) throw KeyIntegrityException() + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyRSAKeyIntegrity(secretKey: RSASecretBCPGKey, publicKey: RSAPublicBCPGKey): Boolean { + // Verify that the public keys N is equal to private keys p*q + return publicKey.modulus.equals(secretKey.primeP.multiply(secretKey.primeQ)) + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyEdDsaKeyIntegrity(secretKey: EdSecretBCPGKey, publicKey: EdDSAPublicBCPGKey): Boolean { + // TODO: Implement + return true + } + + @JvmStatic + @Throws(KeyIntegrityException::class) + private fun verifyDsaKeyIntegrity(privateKey: DSASecretBCPGKey, publicKey: DSAPublicBCPGKey): Boolean { + // Not sure what value to put here in order to have a "robust" primality check + // I went with 40, since that's what SO recommends: + // https://stackoverflow.com/a/6330138 + val certainty = 40 + val pG = publicKey.g + val pP = publicKey.p + val pQ = publicKey.q + val pY = publicKey.y + val sX = privateKey.x + + val pPrime = pP.isProbablePrime(certainty) + if (!pPrime) { + return false + } + + val qPrime = pQ.isProbablePrime(certainty) + if (!qPrime) { + return false + } + + // q > 160 bits + val qLarge = pQ.bitLength() > 160 + if (!qLarge) { + return false + } + + // q divides p - 1 + val qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ) == BigInteger.ZERO + if (!qDividesPminus1) { + return false + } + + // 1 < g < p + val gInBounds = BigInteger.ONE.max(pG) == pG && pG.max(pP) == pP + if (!gInBounds) { + return false + } + + // g^q = 1 mod p + val gPowXModPEquals1 = pG.modPow(pQ, pP) == BigInteger.ONE + if (!gPowXModPEquals1) { + return false + } + + // y = g^x mod p + return pY == pG.modPow(sX, pP) + } + + /** + * Validate ElGamal public key parameters. + * + * Original implementation by the openpgpjs authors: + * Date: Tue, 10 Oct 2023 13:00:01 +0200 Subject: [PATCH 180/351] Clean up unused casts from EncryptionOptions --- .../encryption_signing/EncryptionOptions.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index 74d2b3ca..00b1359a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -10,6 +10,7 @@ import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator import org.pgpainless.algorithm.EncryptionPurpose import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.* import org.pgpainless.implementation.ImplementationFactory @@ -19,7 +20,6 @@ import org.pgpainless.key.info.KeyAccessor import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.util.Passphrase import java.util.* -import javax.annotation.Nonnull class EncryptionOptions( @@ -144,8 +144,8 @@ class EncryptionOptions( for (subkey in subkeys) { val keyId = SubkeyIdentifier(key, subkey.keyID) - (_keyRingInfo as MutableMap)[keyId] = info - (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) + _keyRingInfo[keyId] = info + _keyViews[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) addRecipientKey(key, subkey, false) } } @@ -188,8 +188,8 @@ class EncryptionOptions( for (subkey in encryptionSubkeys) { val keyId = SubkeyIdentifier(key, subkey.keyID) - (_keyRingInfo as MutableMap)[keyId] = info - (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaKeyId(info, keyId) + _keyRingInfo[keyId] = info + _keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId) addRecipientKey(key, subkey, wildcardKeyId) } } @@ -197,7 +197,7 @@ class EncryptionOptions( private fun addRecipientKey(certificate: PGPPublicKeyRing, key: PGPPublicKey, wildcardKeyId: Boolean) { - (_encryptionKeyIdentifiers as MutableSet).add(SubkeyIdentifier(certificate, key.keyID)) + _encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID)) addEncryptionMethod(ImplementationFactory.getInstance() .getPublicKeyKeyEncryptionMethodGenerator(key) .also { it.setUseWildcardKeyID(wildcardKeyId) }) @@ -228,7 +228,7 @@ class EncryptionOptions( * @return this */ fun addEncryptionMethod(encryptionMethod: PGPKeyEncryptionMethodGenerator) = apply { - (_encryptionMethods as MutableSet).add(encryptionMethod) + _encryptionMethods.add(encryptionMethod) } /** From efae652a662f95f57ad28d27c44cd40c38bb77bf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Oct 2023 13:38:45 +0200 Subject: [PATCH 181/351] Kotlin conversion: CertificateValidator --- .../consumer/CertificateValidator.java | 299 ------------------ .../consumer/CertificateValidator.kt | 249 +++++++++++++++ 2 files changed, 249 insertions(+), 299 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java deleted file mode 100644 index f22e057a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import static org.pgpainless.signature.consumer.SignatureVerifier.verifyOnePassSignature; - -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. - */ -public final class CertificateValidator { - - private CertificateValidator() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(CertificateValidator.class); - - /** - * Check if the signing key was eligible to create the provided signature. - * - * That entails: - * - Check, if the primary key is being revoked via key-revocation signatures. - * - Check, if the keys user-ids are revoked or not bound. - * - Check, if the signing subkey is revoked or expired. - * - Check, if the signing key is not capable of signing - * - * @param signature signature - * @param signingKeyRing signing key ring - * @param policy validation policy - * @return true if the signing key was eligible to create the signature - * @throws SignatureValidationException in case of a validation constraint violation - */ - public static boolean validateCertificate(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy) - throws SignatureValidationException { - - Map rejections = new ConcurrentHashMap<>(); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingSubkey = signingKeyRing.getPublicKey(keyId); - if (signingSubkey == null) { - throw new SignatureValidationException("Provided key ring does not contain a subkey with id " + Long.toHexString(keyId)); - } - - PGPPublicKey primaryKey = signingKeyRing.getPublicKey(); - - // Key-Revocation Signatures - List directKeySignatures = new ArrayList<>(); - Iterator primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode()); - while (primaryKeyRevocationIterator.hasNext()) { - PGPSignature revocation = primaryKeyRevocationIterator.next(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Revocation was not made by primary key, skip - continue; - // TODO: What about external revocation keys? - } - try { - if (SignatureVerifier.verifyKeyRevocationSignature(revocation, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting key revocation signature: {}", e.getMessage(), e); - } - } - - // Direct-Key Signatures - Iterator keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode()); - while (keySignatures.hasNext()) { - PGPSignature keySignature = keySignatures.next(); - if (keySignature.getKeyID() != primaryKey.getKeyID()) { - // Signature was not made by primary key, skip - continue; - } - try { - if (SignatureVerifier.verifyDirectKeySignature(keySignature, primaryKey, policy, signature.getCreationTime())) { - directKeySignatures.add(keySignature); - } - } catch (SignatureValidationException e) { - rejections.put(keySignature, e); - LOGGER.debug("Rejecting key signature: {}", e.getMessage(), e); - } - } - - Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (!directKeySignatures.isEmpty()) { - if (directKeySignatures.get(0).getSignatureType() == SignatureType.KEY_REVOCATION.getCode()) { - throw new SignatureValidationException("Primary key has been revoked."); - } - } - - // User-ID signatures (certifications, revocations) - List userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey); - Map> userIdSignatures = new ConcurrentHashMap<>(); - for (String userId : userIds) { - List signaturesOnUserId = new ArrayList<>(); - Iterator userIdSigs = primaryKey.getSignaturesForID(userId); - while (userIdSigs.hasNext()) { - PGPSignature userIdSig = userIdSigs.next(); - if (userIdSig.getKeyID() != primaryKey.getKeyID()) { - // Sig was made by external key, skip - continue; - } - try { - if (SignatureVerifier.verifySignatureOverUserId(userId, userIdSig, primaryKey, policy, signature.getCreationTime())) { - signaturesOnUserId.add(userIdSig); - } - } catch (SignatureValidationException e) { - rejections.put(userIdSig, e); - LOGGER.debug("Rejecting user-id signature: {}", e.getMessage(), e); - } - } - Collections.sort(signaturesOnUserId, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - userIdSignatures.put(userId, signaturesOnUserId); - } - - boolean anyUserIdValid = false; - boolean hasAnyUserIds = !userIdSignatures.keySet().isEmpty(); - for (String userId : userIdSignatures.keySet()) { - if (!userIdSignatures.get(userId).isEmpty()) { - PGPSignature current = userIdSignatures.get(userId).get(0); - if (current.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { - LOGGER.debug("User-ID '{}' is revoked.", userId); - } else { - anyUserIdValid = true; - } - } - } - - if (hasAnyUserIds && !anyUserIdValid) { - throw new SignatureValidationException("No valid user-id found.", rejections); - } - - // Specific signer user-id - SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature); - if (signerUserID != null && policy.getSignerUserIdValidationLevel() == Policy.SignerUserIdValidationLevel.STRICT) { - List signerUserIdSigs = userIdSignatures.get(signerUserID.getID()); - if (signerUserIdSigs == null || signerUserIdSigs.isEmpty()) { - throw new SignatureValidationException("Signature was allegedly made by user-id '" + signerUserID.getID() + - "' but we have no valid signatures for that on the certificate."); - } - - PGPSignature userIdSig = signerUserIdSigs.get(0); - if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) { - throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked."); - } - } - - if (signingSubkey == primaryKey) { - if (!directKeySignatures.isEmpty()) { - PGPSignature directKeySignature = directKeySignatures.get(0); - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySignature); - if (keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - return true; - } - } - } // Subkey Binding Signatures / Subkey Revocation Signatures - else { - List subkeySigs = new ArrayList<>(); - Iterator bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode()); - while (bindingRevocations.hasNext()) { - PGPSignature revocation = bindingRevocations.next(); - if (revocation.getKeyID() != primaryKey.getKeyID()) { - // Subkey Revocation was not made by primary key, skip - continue; - } - try { - if (SignatureVerifier.verifySubkeyBindingRevocation(revocation, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(revocation); - } - } catch (SignatureValidationException e) { - rejections.put(revocation, e); - LOGGER.debug("Rejecting subkey revocation signature: {}", e.getMessage(), e); - } - } - - Iterator bindingSigs = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - while (bindingSigs.hasNext()) { - PGPSignature bindingSig = bindingSigs.next(); - try { - if (SignatureVerifier.verifySubkeyBindingSignature(bindingSig, primaryKey, signingSubkey, policy, signature.getCreationTime())) { - subkeySigs.add(bindingSig); - } - } catch (SignatureValidationException e) { - rejections.put(bindingSig, e); - LOGGER.debug("Rejecting subkey binding signature: {}", e.getMessage(), e); - } - } - - Collections.sort(subkeySigs, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)); - if (subkeySigs.isEmpty()) { - throw new SignatureValidationException("Subkey is not bound.", rejections); - } - - PGPSignature currentSig = subkeySigs.get(0); - if (currentSig.getSignatureType() == SignatureType.SUBKEY_REVOCATION.getCode()) { - throw new SignatureValidationException("Subkey is revoked."); - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(currentSig); - if (keyFlags == null) { - if (directKeySignatures.isEmpty()) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no direct-key sig)."); - } - PGPSignature directKeySig = directKeySignatures.get(0); - KeyFlags directKeyFlags = SignatureSubpacketsUtil.getKeyFlags(directKeySig); - if (directKeyFlags == null || !KeyFlag.hasKeyFlag(directKeyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no keyflags on binding sig, no SIGN flag on direct-key sig)."); - } - } else if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA)) { - throw new SignatureValidationException("Signature was made by key which is not capable of signing (no SIGN flag on binding sig)."); - } - } - return true; - } - - /** - * Validate the given signing key and then verify the given signature while parsing out the signed data. - * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. - * - * @param signature uninitialized signature - * @param signedData input stream containing signed data - * @param signingKeyRing key ring containing signing key - * @param policy validation policy - * @param validationDate date of validation - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException for validation constraint violations - */ - public static boolean validateCertificateAndVerifyUninitializedSignature(PGPSignature signature, - InputStream signedData, - PGPPublicKeyRing signingKeyRing, - Policy policy, - Date validationDate) - throws SignatureValidationException { - validateCertificate(signature, signingKeyRing, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - return SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(keyId), policy, validationDate); - } - - /** - * Validate the signing key and the given initialized signature. - * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. - * - * @param signature initialized signature - * @param verificationKeys key ring containing the verification key - * @param policy validation policy - * @return true if the signature is valid, false otherwise - * @throws SignatureValidationException in case of a validation constraint violation - */ - public static boolean validateCertificateAndVerifyInitializedSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) - throws SignatureValidationException { - validateCertificate(signature, verificationKeys, policy); - long keyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKey signingKey = verificationKeys.getPublicKey(keyId); - SignatureVerifier.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime()); - return true; - } - - /** - * Validate the signing key certificate and the given {@link OnePassSignatureCheck}. - * - * @param onePassSignature corresponding one-pass-signature - * @param policy policy - * @return true if the certificate is valid and the signature is correct, false otherwise. - * @throws SignatureValidationException in case of a validation error - */ - public static boolean validateCertificateAndVerifyOnePassSignature(OnePassSignatureCheck onePassSignature, Policy policy) - throws SignatureValidationException { - PGPSignature signature = onePassSignature.getSignature(); - validateCertificate(signature, onePassSignature.getVerificationKeys(), policy); - PGPPublicKey signingKey = onePassSignature.getVerificationKeys().getPublicKey(signature.getKeyID()); - verifyOnePassSignature(signature, signingKey, onePassSignature, policy); - return true; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt new file mode 100644 index 00000000..126803d4 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import openpgp.openPgpKeyId +import org.bouncycastle.extensions.issuerKeyId +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.util.* + +/** + * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. + */ +class CertificateValidator { + + companion object { + + @JvmStatic + private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) + + /** + * Check if the signing key was eligible to create the provided signature. + * + * That entails: + * - Check, if the primary key is being revoked via key-revocation signatures. + * - Check, if the keys user-ids are revoked or not bound. + * - Check, if the signing subkey is revoked or expired. + * - Check, if the signing key is not capable of signing + * + * @param signature signature + * @param signingKeyRing signing key ring + * @param policy validation policy + * @return true if the signing key was eligible to create the signature + * @throws SignatureValidationException in case of a validation constraint violation + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificate(signature: PGPSignature, + signingKeyRing: PGPPublicKeyRing, + policy: Policy = PGPainless.getPolicy()): Boolean { + val signingSubkey: PGPPublicKey = signingKeyRing.getPublicKey(signature.issuerKeyId) + ?: throw SignatureValidationException("Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") + val primaryKey = signingKeyRing.publicKey!! + val directKeyAndRevSigs = mutableListOf() + val rejections = mutableMapOf() + // revocations + primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } // We do not support external rev keys + .forEach { + try { + if (SignatureVerifier.verifyKeyRevocationSignature(it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) + } + } + + // direct-key sigs + primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifyDirectKeySignature(it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) + } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key signature: ${e.message}, e") + } + } + + directKeyAndRevSigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (directKeyAndRevSigs.isNotEmpty()) { + if (directKeyAndRevSigs[0].signatureType == SignatureType.KEY_REVOCATION.code) { + throw SignatureValidationException("Primary key has been revoked.") + } + } + + // UserID signatures + val userIdSignatures = mutableMapOf>() + KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId -> + buildList { + primaryKey.getSignaturesForID(userId) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { uidSig -> + try { + if (SignatureVerifier.verifySignatureOverUserId(userId, uidSig, primaryKey, policy, signature.creationTime)) { + add(uidSig) + } + } catch (e: SignatureValidationException) { + rejections[uidSig] = e + LOGGER.debug("Rejecting user-id signature: ${e.message}", e) + } + } + }.sortedWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + .let { userIdSignatures[userId] = it } + } + + val hasAnyUserIds = userIdSignatures.isNotEmpty() + val isAnyUserIdValid = userIdSignatures.any { entry -> + entry.value.isNotEmpty() && entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code + } + + if (hasAnyUserIds && !isAnyUserIdValid) { + throw SignatureValidationException("No valid user-id found.", rejections) + } + + // Specific signer user-id + if (policy.signerUserIdValidationLevel == Policy.SignerUserIdValidationLevel.STRICT) { + SignatureSubpacketsUtil.getSignerUserID(signature)?.let { + if (userIdSignatures[it.id] == null || userIdSignatures[it.id]!!.isEmpty()) { + throw SignatureValidationException("Signature was allegedly made by user-id '${it.id}'," + + " but we have no valid signatures for that on the certificate.") + } + + if (userIdSignatures[it.id]!![0].signatureType == SignatureType.CERTIFICATION_REVOCATION.code) { + throw SignatureValidationException("Signature was made with user-id '${it.id}' which is revoked.") + } + } + } + + if (signingSubkey.keyID == primaryKey.keyID) { // signing key is primary key + if (directKeyAndRevSigs.isNotEmpty()) { + val directKeySig = directKeyAndRevSigs[0]!! + val flags = SignatureSubpacketsUtil.getKeyFlags(directKeySig) + if (flags != null && KeyFlag.hasKeyFlag(flags.flags, KeyFlag.SIGN_DATA)) { + return true + } + } + } else { // signing key is subkey + val subkeySigs = mutableListOf() + signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code).asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingRevocation(it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e : SignatureValidationException) { + rejections[it] = e + LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e) + } + } + + signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.code).asSequence() + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingSignature(it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) + } + } catch (e : SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) + } + } + + subkeySigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + if (subkeySigs.isEmpty()) { + throw SignatureValidationException("Subkey is not bound.", rejections) + } + + if (subkeySigs[0].signatureType == SignatureType.SUBKEY_REVOCATION.code) { + throw SignatureValidationException("Subkey is revoked.") + } + + val keyFlags = SignatureSubpacketsUtil.getKeyFlags(subkeySigs[0]) + if (keyFlags == null || !KeyFlag.hasKeyFlag(keyFlags.flags, KeyFlag.SIGN_DATA)) { + throw SignatureValidationException("Signature was made by key which is not capable of signing (no keyflag).") + } + } + return true + } + + /** + * Validate the given signing key and then verify the given signature while parsing out the signed data. + * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. + * + * @param signature uninitialized signature + * @param signedData input stream containing signed data + * @param signingKeyRing key ring containing signing key + * @param policy validation policy + * @param validationDate date of validation + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException for validation constraint violations + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyUninitializedSignature(signature: PGPSignature, + signedData: InputStream, + signingKeyRing: PGPPublicKeyRing, + policy: Policy, + referenceTime: Date = signature.creationTime): Boolean { + return validateCertificate(signature, signingKeyRing, policy) + && SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(signature.issuerKeyId)!!, policy, referenceTime) + } + + /** + * Validate the signing key and the given initialized signature. + * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. + * + * @param signature initialized signature + * @param verificationKeys key ring containing the verification key + * @param policy validation policy + * @return true if the signature is valid, false otherwise + * @throws SignatureValidationException in case of a validation constraint violation + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyInitializedSignature(signature: PGPSignature, + verificationKeys: PGPPublicKeyRing, + policy: Policy): Boolean { + return validateCertificate(signature, verificationKeys, policy) && + SignatureVerifier.verifyInitializedSignature(signature, verificationKeys.getPublicKey(signature.issuerKeyId), policy, signature.creationTime) + } + + /** + * Validate the signing key certificate and the given [OnePassSignatureCheck]. + * + * @param onePassSignature corresponding one-pass-signature + * @param policy policy + * @return true if the certificate is valid and the signature is correct, false otherwise. + * @throws SignatureValidationException in case of a validation error + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun validateCertificateAndVerifyOnePassSignature(onePassSignature: OnePassSignatureCheck, + policy: Policy): Boolean { + return validateCertificate(onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && + SignatureVerifier.verifyOnePassSignature(onePassSignature.signature!!, + onePassSignature.verificationKeys.getPublicKey(onePassSignature.signature!!.issuerKeyId), + onePassSignature, policy) + } + } +} \ No newline at end of file From 70e1b40cd20e8a3d5a5caff899b412fe6b9f2c1f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Oct 2023 13:41:50 +0200 Subject: [PATCH 182/351] Fix ArmorUtil header --- .../src/main/kotlin/org/pgpainless/util/ArmorUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index c8ff4da8..53bdca0b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2023 Paul Schaub // -// SPDX-License-Identifier: Apache-2.0package org.pgpainless.util +// SPDX-License-Identifier: Apache-2.0 package org.pgpainless.util From 4fc513fa25c9c5a332e868f8d6bcba32642e8dbe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Oct 2023 18:58:37 +0200 Subject: [PATCH 183/351] Kotlin conversion: SignatureCreationDateComparator, SignatureValidityComparator --- .../SignatureCreationDateComparator.java | 53 ------------------- .../consumer/SignatureValidityComparator.java | 53 ------------------- .../SignatureCreationDateComparator.kt | 34 ++++++++++++ .../consumer/SignatureValidityComparator.kt | 31 +++++++++++ 4 files changed, 65 insertions(+), 106 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java deleted file mode 100644 index 7996e33b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureCreationDateComparator.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Comparator; - -import org.bouncycastle.openpgp.PGPSignature; - -/** - * Comparator which can be used to sort signatures with regard to their creation time. - */ -public class SignatureCreationDateComparator implements Comparator { - - public static final Order DEFAULT_ORDER = Order.OLD_TO_NEW; - - public enum Order { - /** - * Oldest signatures first. - */ - OLD_TO_NEW, - - /** - * Newest signatures first. - */ - NEW_TO_OLD - } - - private final Order order; - - /** - * Create a new comparator which sorts signatures old to new. - */ - public SignatureCreationDateComparator() { - this(DEFAULT_ORDER); - } - - /** - * Create a new comparator which sorts signatures according to the passed ordering. - * @param order ordering - */ - public SignatureCreationDateComparator(Order order) { - this.order = order; - } - - @Override - public int compare(PGPSignature one, PGPSignature two) { - return order == Order.OLD_TO_NEW - ? one.getCreationTime().compareTo(two.getCreationTime()) - : two.getCreationTime().compareTo(one.getCreationTime()); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java deleted file mode 100644 index 94ebd5b2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidityComparator.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Comparator; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.signature.SignatureUtils; - -/** - * Comparator which sorts signatures based on an ordering and on revocation hardness. - * - * If a list of signatures gets ordered using this comparator, hard revocations will always - * come first. - * Further, signatures are ordered by date according to the {@link SignatureCreationDateComparator.Order}. - */ -public class SignatureValidityComparator implements Comparator { - - private final SignatureCreationDateComparator creationDateComparator; - - /** - * Create a new {@link SignatureValidityComparator} which orders signatures the oldest first. - * Still, hard revocations will come first. - */ - public SignatureValidityComparator() { - this(SignatureCreationDateComparator.DEFAULT_ORDER); - } - - /** - * Create a new {@link SignatureValidityComparator} which orders signatures following the passed ordering. - * Still, hard revocations will come first. - * - * @param order order of creation dates - */ - public SignatureValidityComparator(SignatureCreationDateComparator.Order order) { - this.creationDateComparator = new SignatureCreationDateComparator(order); - } - - @Override - public int compare(PGPSignature one, PGPSignature two) { - boolean oneIsHard = SignatureUtils.isHardRevocation(one); - boolean twoIsHard = SignatureUtils.isHardRevocation(two); - - // both have same "hardness", so compare creation time - if (oneIsHard == twoIsHard) { - return creationDateComparator.compare(one, two); - } - // favor the "harder" signature - return oneIsHard ? -1 : 1; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt new file mode 100644 index 00000000..75cd274c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.openpgp.PGPSignature + +/** + * Create a new comparator which sorts signatures according to the passed ordering. + * @param order ordering + */ +class SignatureCreationDateComparator( + private val order: Order = Order.OLD_TO_NEW +) : Comparator { + + enum class Order { + /** + * Oldest signatures first. + */ + OLD_TO_NEW, + /** + * Newest signatures first. + */ + NEW_TO_OLD + } + + override fun compare(one: PGPSignature, two: PGPSignature): Int { + return when(order) { + Order.OLD_TO_NEW -> one.creationTime.compareTo(two.creationTime) + Order.NEW_TO_OLD -> two.creationTime.compareTo(one.creationTime) + } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt new file mode 100644 index 00000000..f2b586ae --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import org.bouncycastle.extensions.isHardRevocation +import org.bouncycastle.openpgp.PGPSignature + +/** + * Comparator which sorts signatures based on an ordering and on revocation hardness. + * + * If a list of signatures gets ordered using this comparator, hard revocations will always + * come first. + * Further, signatures are ordered by date according to the [SignatureCreationDateComparator.Order]. + */ +class SignatureValidityComparator( + order: SignatureCreationDateComparator.Order = SignatureCreationDateComparator.Order.OLD_TO_NEW +) : Comparator { + + private val creationDateComparator: SignatureCreationDateComparator = SignatureCreationDateComparator(order) + override fun compare(one: PGPSignature, two: PGPSignature): Int { + return if (one.isHardRevocation == two.isHardRevocation) { + // Both have the same hardness, so compare creation time + creationDateComparator.compare(one, two) + } + // else favor the "harder" signature + else if (one.isHardRevocation) -1 else 1 + } + +} \ No newline at end of file From 0effc84fac73a99fdff183e83b89c6f4f47bcf4e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 20 Oct 2023 14:10:37 +0200 Subject: [PATCH 184/351] Kotlin conversion: SignatureSubpackets + subclasses --- .../subpackets/BaseSignatureSubpackets.java | 140 ---- .../subpackets/CertificationSubpackets.java | 12 - .../RevocationSignatureSubpackets.java | 26 - .../subpackets/SelfSignatureSubpackets.java | 98 --- .../SignatureSubpacketCallback.java | 26 - .../subpackets/SignatureSubpackets.java | 735 ------------------ .../subpackets/SignatureSubpacketsHelper.java | 4 +- .../src/main/kotlin/openpgp/DateExtensions.kt | 12 +- .../secretkeyring/SecretKeyRingEditor.kt | 6 +- .../subpackets/BaseSignatureSubpackets.kt | 128 +++ .../subpackets/CertificationSubpackets.kt | 11 + .../RevocationSignatureSubpackets.kt | 21 + .../subpackets/SelfSignatureSubpackets.kt | 84 ++ .../subpackets/SignatureSubpacketCallback.kt | 26 + .../subpackets/SignatureSubpackets.kt | 491 ++++++++++++ 15 files changed, 777 insertions(+), 1043 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java deleted file mode 100644 index 09fe1a99..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.io.IOException; -import java.net.URL; -import java.util.Date; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.IssuerFingerprint; -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PolicyURI; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.bcpg.sig.SignatureTarget; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.bcpg.sig.TrustSignature; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; - -public interface BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - /** - * Add both an {@link IssuerKeyID} and {@link IssuerFingerprint} subpacket pointing to the given key. - * - * @param key key - * @return this - * - * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain any - * {@link IssuerKeyID} packets. - */ - BaseSignatureSubpackets setIssuerFingerprintAndKeyId(PGPPublicKey key); - - BaseSignatureSubpackets setIssuerKeyId(long keyId); - - BaseSignatureSubpackets setIssuerKeyId(boolean isCritical, long keyId); - - BaseSignatureSubpackets setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID); - - BaseSignatureSubpackets setIssuerFingerprint(@Nonnull PGPPublicKey key); - - BaseSignatureSubpackets setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key); - - BaseSignatureSubpackets setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint); - - BaseSignatureSubpackets setSignatureCreationTime(@Nonnull Date creationTime); - - BaseSignatureSubpackets setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime); - - BaseSignatureSubpackets setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime); - - BaseSignatureSubpackets setSignatureExpirationTime(boolean isCritical, long seconds); - - BaseSignatureSubpackets setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime); - - BaseSignatureSubpackets setSignerUserId(@Nonnull String userId); - - BaseSignatureSubpackets setSignerUserId(boolean isCritical, @Nonnull String userId); - - BaseSignatureSubpackets setSignerUserId(@Nullable SignerUserID signerUserId); - - BaseSignatureSubpackets addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue); - - BaseSignatureSubpackets addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue); - - BaseSignatureSubpackets addNotationData(@Nonnull NotationData notationData); - - BaseSignatureSubpackets clearNotationData(); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient); - - BaseSignatureSubpackets addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint); - - BaseSignatureSubpackets clearIntendedRecipientFingerprints(); - - BaseSignatureSubpackets setExportable(boolean isExportable); - - BaseSignatureSubpackets setExportable(boolean isCritical, boolean isExportable); - - BaseSignatureSubpackets setExportable(@Nullable Exportable exportable); - - BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl); - - BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl); - - BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl); - - BaseSignatureSubpackets setRegularExpression(@Nonnull String regex); - - BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex); - - BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex); - - BaseSignatureSubpackets setRevocable(boolean revocable); - - BaseSignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable); - - BaseSignatureSubpackets setRevocable(@Nullable Revocable revocable); - - BaseSignatureSubpackets setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); - - BaseSignatureSubpackets setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); - - BaseSignatureSubpackets setSignatureTarget(@Nullable SignatureTarget signatureTarget); - - BaseSignatureSubpackets setTrust(int depth, int amount); - - BaseSignatureSubpackets setTrust(boolean isCritical, int depth, int amount); - - BaseSignatureSubpackets setTrust(@Nullable TrustSignature trust); - - BaseSignatureSubpackets addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException; - - BaseSignatureSubpackets addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException; - - BaseSignatureSubpackets addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature); - - BaseSignatureSubpackets clearEmbeddedSignatures(); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java deleted file mode 100644 index 24614882..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/CertificationSubpackets.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -public interface CertificationSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java deleted file mode 100644 index 358437dc..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.pgpainless.key.util.RevocationAttributes; - -public interface RevocationSignatureSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - RevocationSignatureSubpackets setRevocationReason(RevocationAttributes revocationAttributes); - - RevocationSignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes); - - RevocationSignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description); - - RevocationSignatureSubpackets setRevocationReason(@Nullable RevocationReason reason); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java deleted file mode 100644 index 02cc5e93..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.util.Date; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -public interface SelfSignatureSubpackets extends BaseSignatureSubpackets { - - interface Callback extends SignatureSubpacketCallback { - - } - - SelfSignatureSubpackets setKeyFlags(KeyFlag... keyFlags); - - default SelfSignatureSubpackets setKeyFlags(List keyFlags) { - KeyFlag[] flags = keyFlags.toArray(new KeyFlag[0]); - return setKeyFlags(flags); - } - - SelfSignatureSubpackets setKeyFlags(boolean isCritical, KeyFlag... keyFlags); - - SelfSignatureSubpackets setKeyFlags(@Nullable KeyFlags keyFlags); - - SelfSignatureSubpackets setPrimaryUserId(); - - SelfSignatureSubpackets setPrimaryUserId(boolean isCritical); - - SelfSignatureSubpackets setPrimaryUserId(@Nullable PrimaryUserID primaryUserId); - - SelfSignatureSubpackets setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); - - SelfSignatureSubpackets setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration); - - SelfSignatureSubpackets setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(HashAlgorithm... algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(Set algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(boolean isCritical, Set algorithms); - - SelfSignatureSubpackets setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms); - - SelfSignatureSubpackets addRevocationKey(@Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey); - - SelfSignatureSubpackets addRevocationKey(@Nonnull RevocationKey revocationKey); - - SelfSignatureSubpackets clearRevocationKeys(); - - SelfSignatureSubpackets setFeatures(Feature... features); - - SelfSignatureSubpackets setFeatures(boolean isCritical, Feature... features); - - SelfSignatureSubpackets setFeatures(@Nullable Features features); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java deleted file mode 100644 index 11650ea3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -public interface SignatureSubpacketCallback { - - /** - * Callback method that can be used to modify the hashed subpackets of a signature. - * - * @param hashedSubpackets hashed subpackets - */ - default void modifyHashedSubpackets(S hashedSubpackets) { - - } - - /** - * Callback method that can be used to modify the unhashed subpackets of a signature. - * - * @param unhashedSubpackets unhashed subpackets - */ - default void modifyUnhashedSubpackets(S unhashedSubpackets) { - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java deleted file mode 100644 index d0466b6a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpackets.java +++ /dev/null @@ -1,735 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.SignatureSubpacketTags; -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.IssuerFingerprint; -import org.bouncycastle.bcpg.sig.IssuerKeyID; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PolicyURI; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.bcpg.sig.SignatureTarget; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.bcpg.sig.TrustSignature; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.key.util.RevocationAttributes; - -public class SignatureSubpackets - implements BaseSignatureSubpackets, SelfSignatureSubpackets, CertificationSubpackets, RevocationSignatureSubpackets { - - private SignatureCreationTime signatureCreationTime; - private SignatureExpirationTime signatureExpirationTime; - private IssuerKeyID issuerKeyID; - private IssuerFingerprint issuerFingerprint; - private final List notationDataList = new ArrayList<>(); - private final List intendedRecipientFingerprintList = new ArrayList<>(); - private final List revocationKeyList = new ArrayList<>(); - private Exportable exportable; - private SignatureTarget signatureTarget; - private Features features; - private KeyFlags keyFlags; - private TrustSignature trust; - private PreferredAlgorithms preferredCompressionAlgorithms; - private PreferredAlgorithms preferredSymmetricKeyAlgorithms; - private PreferredAlgorithms preferredHashAlgorithms; - private final List embeddedSignatureList = new ArrayList<>(); - private SignerUserID signerUserId; - private KeyExpirationTime keyExpirationTime; - private PolicyURI policyURI; - private PrimaryUserID primaryUserId; - private RegularExpression regularExpression; - private Revocable revocable; - private RevocationReason revocationReason; - private final List residualSubpackets = new ArrayList<>(); - - public SignatureSubpackets() { - - } - - public interface Callback extends SignatureSubpacketCallback { - - } - - public static SignatureSubpackets refreshHashedSubpackets(PGPPublicKey issuer, PGPSignature oldSignature) { - return createHashedSubpacketsFrom(issuer, oldSignature.getHashedSubPackets()); - } - - public static SignatureSubpackets refreshUnhashedSubpackets(PGPSignature oldSignature) { - return createSubpacketsFrom(oldSignature.getUnhashedSubPackets()); - } - - public static SignatureSubpackets createHashedSubpacketsFrom(PGPPublicKey issuer, PGPSignatureSubpacketVector base) { - SignatureSubpackets wrapper = createSubpacketsFrom(base); - wrapper.setIssuerFingerprintAndKeyId(issuer); - return wrapper; - } - - public static SignatureSubpackets createSubpacketsFrom(PGPSignatureSubpacketVector base) { - SignatureSubpackets wrapper = new SignatureSubpackets(); - SignatureSubpacketsHelper.applyFrom(base, wrapper); - return wrapper; - } - - public static SignatureSubpackets createHashedSubpackets(PGPPublicKey issuer) { - SignatureSubpackets wrapper = new SignatureSubpackets(); - wrapper.setIssuerFingerprintAndKeyId(issuer); - return wrapper; - } - - public static SignatureSubpackets createEmptySubpackets() { - return new SignatureSubpackets(); - } - - @Override - public SignatureSubpackets setIssuerFingerprintAndKeyId(PGPPublicKey key) { - setIssuerKeyId(key.getKeyID()); - setIssuerFingerprint(key); - return this; - } - - @Override - public SignatureSubpackets setIssuerKeyId(long keyId) { - return setIssuerKeyId(false, keyId); - } - - @Override - public SignatureSubpackets setIssuerKeyId(boolean isCritical, long keyId) { - return setIssuerKeyId(new IssuerKeyID(isCritical, keyId)); - } - - @Override - public SignatureSubpackets setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID) { - this.issuerKeyID = issuerKeyID; - return this; - } - - public IssuerKeyID getIssuerKeyIdSubpacket() { - return issuerKeyID; - } - - @Override - public SignatureSubpackets setIssuerFingerprint(@Nonnull PGPPublicKey key) { - return setIssuerFingerprint(false, key); - } - - @Override - public SignatureSubpackets setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key) { - return setIssuerFingerprint(new IssuerFingerprint(isCritical, key.getVersion(), key.getFingerprint())); - } - - @Override - public SignatureSubpackets setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint) { - this.issuerFingerprint = fingerprint; - return this; - } - - public IssuerFingerprint getIssuerFingerprintSubpacket() { - return issuerFingerprint; - } - - @Override - public SignatureSubpackets setKeyFlags(KeyFlag... keyFlags) { - return setKeyFlags(true, keyFlags); - } - - @Override - public SignatureSubpackets setKeyFlags(boolean isCritical, KeyFlag... keyFlags) { - int bitmask = KeyFlag.toBitmask(keyFlags); - return setKeyFlags(new KeyFlags(isCritical, bitmask)); - } - - @Override - public SignatureSubpackets setKeyFlags(@Nullable KeyFlags keyFlags) { - this.keyFlags = keyFlags; - return this; - } - - public KeyFlags getKeyFlagsSubpacket() { - return keyFlags; - } - - @Override - public SignatureSubpackets setSignatureCreationTime(@Nonnull Date creationTime) { - return setSignatureCreationTime(true, creationTime); - } - - @Override - public SignatureSubpackets setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime) { - return setSignatureCreationTime(new SignatureCreationTime(isCritical, creationTime)); - } - - @Override - public SignatureSubpackets setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime) { - this.signatureCreationTime = signatureCreationTime; - return this; - } - - public SignatureCreationTime getSignatureCreationTimeSubpacket() { - return signatureCreationTime; - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime) { - return setSignatureExpirationTime(true, creationTime, expirationTime); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime) { - return setSignatureExpirationTime(isCritical, (expirationTime.getTime() / 1000) - (creationTime.getTime() / 1000)); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(boolean isCritical, long seconds) { - enforceBounds(seconds); - return setSignatureExpirationTime(new SignatureExpirationTime(isCritical, seconds)); - } - - @Override - public SignatureSubpackets setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime) { - this.signatureExpirationTime = expirationTime; - return this; - } - - public SignatureExpirationTime getSignatureExpirationTimeSubpacket() { - return signatureExpirationTime; - } - - @Override - public SignatureSubpackets setSignerUserId(@Nonnull String userId) { - return setSignerUserId(false, userId); - } - - @Override - public SignatureSubpackets setSignerUserId(boolean isCritical, @Nonnull String userId) { - return setSignerUserId(new SignerUserID(isCritical, userId)); - } - - @Override - public SignatureSubpackets setSignerUserId(@Nullable SignerUserID signerUserId) { - this.signerUserId = signerUserId; - return this; - } - - public SignerUserID getSignerUserIdSubpacket() { - return signerUserId; - } - - @Override - public SignatureSubpackets setPrimaryUserId() { - return setPrimaryUserId(true); - } - - @Override - public SignatureSubpackets setPrimaryUserId(boolean isCritical) { - return setPrimaryUserId(new PrimaryUserID(isCritical, true)); - } - - @Override - public SignatureSubpackets setPrimaryUserId(@Nullable PrimaryUserID primaryUserId) { - this.primaryUserId = primaryUserId; - return this; - } - - public PrimaryUserID getPrimaryUserIdSubpacket() { - return primaryUserId; - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(key.getCreationTime(), keyExpirationTime); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(true, keyCreationTime, keyExpirationTime); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { - return setKeyExpirationTime(isCritical, (keyExpirationTime.getTime() / 1000) - (keyCreationTime.getTime() / 1000)); - } - - @Override - public SignatureSubpackets setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration) { - enforceBounds(secondsFromCreationToExpiration); - return setKeyExpirationTime(new KeyExpirationTime(isCritical, secondsFromCreationToExpiration)); - } - - /** - * Enforce that
secondsFromCreationToExpiration
is within bounds of an unsigned 32bit number. - * Values less than 0 are illegal, as well as values greater 0xffffffff. - * - * @param secondsFromCreationToExpiration number to check - * @throws IllegalArgumentException in case of an under- or overflow - */ - private void enforceBounds(long secondsFromCreationToExpiration) { - if (secondsFromCreationToExpiration < 0) { - throw new IllegalArgumentException("Seconds from creation to expiration cannot be less than 0."); - } - if (secondsFromCreationToExpiration > 0xffffffffL) { - throw new IllegalArgumentException("Integer overflow. Seconds from creation to expiration cannot be larger than 0xffffffff"); - } - } - - @Override - public SignatureSubpackets setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime) { - this.keyExpirationTime = keyExpirationTime; - return this; - } - - public KeyExpirationTime getKeyExpirationTimeSubpacket() { - return keyExpirationTime; - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms) { - return setPreferredCompressionAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(Set algorithms) { - return setPreferredCompressionAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < algorithms.size(); i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredCompressionAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_COMP_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - this.preferredCompressionAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_COMP_ALGS) { - throw new IllegalArgumentException("Invalid preferred compression algorithms type."); - } - this.preferredCompressionAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredCompressionAlgorithmsSubpacket() { - return preferredCompressionAlgorithms; - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms) { - return setPreferredSymmetricKeyAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(Set algorithms) { - return setPreferredSymmetricKeyAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < algorithms.size(); i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredSymmetricKeyAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_SYM_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - this.preferredSymmetricKeyAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_SYM_ALGS) { - throw new IllegalArgumentException("Invalid preferred symmetric key algorithms type."); - } - this.preferredSymmetricKeyAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredSymmetricKeyAlgorithmsSubpacket() { - return preferredSymmetricKeyAlgorithms; - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(HashAlgorithm... algorithms) { - return setPreferredHashAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(Set algorithms) { - return setPreferredHashAlgorithms(false, algorithms); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(boolean isCritical, Set algorithms) { - int[] ids = new int[algorithms.size()]; - Iterator iterator = algorithms.iterator(); - for (int i = 0; i < ids.length; i++) { - ids[i] = iterator.next().getAlgorithmId(); - } - return setPreferredHashAlgorithms(new PreferredAlgorithms( - SignatureSubpacketTags.PREFERRED_HASH_ALGS, isCritical, ids)); - } - - @Override - public SignatureSubpackets setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms) { - if (algorithms == null) { - preferredHashAlgorithms = null; - return this; - } - - if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_HASH_ALGS) { - throw new IllegalArgumentException("Invalid preferred hash algorithms type."); - } - this.preferredHashAlgorithms = algorithms; - return this; - } - - public PreferredAlgorithms getPreferredHashAlgorithmsSubpacket() { - return preferredHashAlgorithms; - } - - @Override - public SignatureSubpackets addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue) { - return addNotationData(isCritical, true, notationName, notationValue); - } - - @Override - public SignatureSubpackets addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue) { - return addNotationData(new NotationData(isCritical, isHumanReadable, notationName, notationValue)); - } - - @Override - public SignatureSubpackets addNotationData(@Nonnull NotationData notationData) { - notationDataList.add(notationData); - return this; - } - - @Override - public SignatureSubpackets clearNotationData() { - notationDataList.clear(); - return this; - } - - public List getNotationDataSubpackets() { - return new ArrayList<>(notationDataList); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient) { - return addIntendedRecipientFingerprint(false, recipient); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient) { - return addIntendedRecipientFingerprint(new IntendedRecipientFingerprint(isCritical, recipient.getVersion(), recipient.getFingerprint())); - } - - @Override - public SignatureSubpackets addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint) { - this.intendedRecipientFingerprintList.add(intendedRecipientFingerprint); - return this; - } - - @Override - public SignatureSubpackets clearIntendedRecipientFingerprints() { - intendedRecipientFingerprintList.clear(); - return this; - } - - public List getIntendedRecipientFingerprintSubpackets() { - return new ArrayList<>(intendedRecipientFingerprintList); - } - - @Override - public SignatureSubpackets setExportable(boolean exportable) { - return setExportable(true, exportable); - } - - @Override - public SignatureSubpackets setExportable(boolean isCritical, boolean isExportable) { - return setExportable(new Exportable(isCritical, isExportable)); - } - - @Override - public SignatureSubpackets setExportable(@Nullable Exportable exportable) { - this.exportable = exportable; - return this; - } - - public Exportable getExportableSubpacket() { - return exportable; - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(@Nonnull URL policyUrl) { - return setPolicyUrl(false, policyUrl); - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(boolean isCritical, @Nonnull URL policyUrl) { - return setPolicyUrl(new PolicyURI(isCritical, policyUrl.toString())); - } - - @Override - public BaseSignatureSubpackets setPolicyUrl(@Nullable PolicyURI policyUrl) { - this.policyURI = policyUrl; - return this; - } - - public PolicyURI getPolicyURI() { - return policyURI; - } - - @Override - public BaseSignatureSubpackets setRegularExpression(@Nonnull String regex) { - return setRegularExpression(false, regex); - } - - @Override - public BaseSignatureSubpackets setRegularExpression(boolean isCritical, @Nonnull String regex) { - return setRegularExpression(new RegularExpression(isCritical, regex)); - } - - @Override - public BaseSignatureSubpackets setRegularExpression(@Nullable RegularExpression regex) { - this.regularExpression = regex; - return this; - } - - public RegularExpression getRegularExpression() { - return regularExpression; - } - - @Override - public SignatureSubpackets setRevocable(boolean revocable) { - return setRevocable(true, revocable); - } - - @Override - public SignatureSubpackets setRevocable(boolean isCritical, boolean isRevocable) { - return setRevocable(new Revocable(isCritical, isRevocable)); - } - - @Override - public SignatureSubpackets setRevocable(@Nullable Revocable revocable) { - this.revocable = revocable; - return this; - } - - public Revocable getRevocableSubpacket() { - return revocable; - } - - @Override - public SignatureSubpackets addRevocationKey(@Nonnull PGPPublicKey revocationKey) { - return addRevocationKey(true, revocationKey); - } - - @Override - public SignatureSubpackets addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey) { - return addRevocationKey(isCritical, false, revocationKey); - } - - @Override - public SignatureSubpackets addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey) { - byte clazz = (byte) 0x80; - clazz |= (isSensitive ? 0x40 : 0x00); - return addRevocationKey(new RevocationKey(isCritical, clazz, revocationKey.getAlgorithm(), revocationKey.getFingerprint())); - } - - @Override - public SignatureSubpackets addRevocationKey(@Nonnull RevocationKey revocationKey) { - this.revocationKeyList.add(revocationKey); - return this; - } - - @Override - public SignatureSubpackets clearRevocationKeys() { - revocationKeyList.clear(); - return this; - } - - public List getRevocationKeySubpackets() { - return new ArrayList<>(revocationKeyList); - } - - @Override - public SignatureSubpackets setRevocationReason(RevocationAttributes revocationAttributes) { - return setRevocationReason(false, revocationAttributes); - } - - @Override - public SignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes) { - return setRevocationReason(isCritical, revocationAttributes.getReason(), revocationAttributes.getDescription()); - } - - @Override - public SignatureSubpackets setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description) { - return setRevocationReason(new RevocationReason(isCritical, reason.code(), description)); - } - - @Override - public SignatureSubpackets setRevocationReason(@Nullable RevocationReason reason) { - this.revocationReason = reason; - return this; - } - - public RevocationReason getRevocationReasonSubpacket() { - return revocationReason; - } - - @Override - public SignatureSubpackets setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { - return setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData); - } - - @Override - public SignatureSubpackets setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { - return setSignatureTarget(new SignatureTarget(isCritical, keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId(), hashData)); - } - - @Override - public SignatureSubpackets setSignatureTarget(@Nullable SignatureTarget signatureTarget) { - this.signatureTarget = signatureTarget; - return this; - } - - public SignatureTarget getSignatureTargetSubpacket() { - return signatureTarget; - } - - @Override - public SignatureSubpackets setFeatures(Feature... features) { - return setFeatures(true, features); - } - - @Override - public SignatureSubpackets setFeatures(boolean isCritical, Feature... features) { - byte bitmask = Feature.toBitmask(features); - return setFeatures(new Features(isCritical, bitmask)); - } - - @Override - public SignatureSubpackets setFeatures(@Nullable Features features) { - this.features = features; - return this; - } - - public Features getFeaturesSubpacket() { - return features; - } - - @Override - public SignatureSubpackets setTrust(int depth, int amount) { - return setTrust(true, depth, amount); - } - - @Override - public SignatureSubpackets setTrust(boolean isCritical, int depth, int amount) { - return setTrust(new TrustSignature(isCritical, depth, amount)); - } - - @Override - public SignatureSubpackets setTrust(@Nullable TrustSignature trust) { - this.trust = trust; - return this; - } - - public TrustSignature getTrustSubpacket() { - return trust; - } - - @Override - public SignatureSubpackets addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException { - return addEmbeddedSignature(true, signature); - } - - @Override - public SignatureSubpackets addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException { - byte[] sig = signature.getEncoded(); - byte[] data; - - if (sig.length - 1 > 256) { - data = new byte[sig.length - 3]; - } - else { - data = new byte[sig.length - 2]; - } - - System.arraycopy(sig, sig.length - data.length, data, 0, data.length); - - return addEmbeddedSignature(new EmbeddedSignature(isCritical, false, data)); - } - - @Override - public SignatureSubpackets addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature) { - this.embeddedSignatureList.add(embeddedSignature); - return this; - } - - @Override - public SignatureSubpackets clearEmbeddedSignatures() { - this.embeddedSignatureList.clear(); - return this; - } - - public List getEmbeddedSignatureSubpackets() { - return new ArrayList<>(embeddedSignatureList); - } - - public SignatureSubpackets addResidualSubpacket(SignatureSubpacket subpacket) { - this.residualSubpackets.add(subpacket); - return this; - } - - public List getResidualSubpackets() { - return new ArrayList<>(residualSubpackets); - } - -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java index 8af60a03..2a7d4f83 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java @@ -143,8 +143,8 @@ public class SignatureSubpacketsHelper { addSubpacket(generator, subpackets.getSignatureCreationTimeSubpacket()); addSubpacket(generator, subpackets.getSignatureExpirationTimeSubpacket()); addSubpacket(generator, subpackets.getExportableSubpacket()); - addSubpacket(generator, subpackets.getPolicyURI()); - addSubpacket(generator, subpackets.getRegularExpression()); + addSubpacket(generator, subpackets.getPolicyURISubpacket()); + addSubpacket(generator, subpackets.getRegularExpressionSubpacket()); for (NotationData notationData : subpackets.getNotationDataSubpackets()) { addSubpacket(generator, notationData); } diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt index 5972cd3a..a1c80710 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -24,11 +24,21 @@ fun Date.plusSeconds(seconds: Long): Date? { else Date(this.time + 1000 * seconds) } +val Date.asSeconds: Long + get() = time / 1000 + +fun Date.secondsTill(later: Date): Long { + require(this <= later) { + "Timestamp MUST be before the later timestamp." + } + return later.asSeconds - this.asSeconds +} + /** * Return a new [Date] instance with this instance's time floored down to seconds precision. */ fun Date.toSecondsPrecision(): Date { - return Date((time / 1000) * 1000) + return Date(asSeconds * 1000) } internal val parser: SimpleDateFormat diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index b219a739..3425145b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -144,14 +144,14 @@ class SecretKeyRingEditor( ?: throw AssertionError("Certification for old user-ID MUST NOT be null.") addUserId(newUID, object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { SignatureSubpacketsHelper.applyFrom(oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) if (oldUID == info.primaryUserId && !oldCertification.hashedSubPackets.isPrimaryUserID) { hashedSubpackets.setPrimaryUserId() } } - override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets?) { + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { SignatureSubpacketsHelper.applyFrom(oldCertification.unhashedSubPackets, unhashedSubpackets as SignatureSubpackets) } }, protector) @@ -163,7 +163,7 @@ class SecretKeyRingEditor( subkeyPassphrase: Passphrase, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { val callback = object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets?) { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { SignatureSubpacketsHelper.applyFrom(keySpec.subpackets, hashedSubpackets as SignatureSubpackets) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt new file mode 100644 index 00000000..66618b23 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import java.io.IOException +import java.net.URL +import java.util.* + +interface BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + /** + * Add both an [IssuerKeyID] and [IssuerFingerprint] subpacket pointing to the given key. + * + * @param key key + * @return this + * + * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain any + * [IssuerKeyID] packets. + */ + fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerKeyId(keyId: Long): BaseSignatureSubpackets + + fun setIssuerKeyId(isCritical: Boolean, keyId: Long): BaseSignatureSubpackets + + fun setIssuerKeyId(issuerKeyID: IssuerKeyID?): BaseSignatureSubpackets + + fun setIssuerFingerprint(isCritical: Boolean, issuer: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerFingerprint(issuer: PGPPublicKey): BaseSignatureSubpackets + + fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): BaseSignatureSubpackets + + fun setSignatureCreationTime(creationTime: Date): BaseSignatureSubpackets + + fun setSignatureCreationTime(isCritical: Boolean, creationTime: Date): BaseSignatureSubpackets + + fun setSignatureCreationTime(creationTime: SignatureCreationTime?): BaseSignatureSubpackets + + fun setSignatureExpirationTime(creationTime: Date, expirationTime: Date?): BaseSignatureSubpackets + + fun setSignatureExpirationTime(isCritical: Boolean, creationTime: Date, expirationTime: Date?): BaseSignatureSubpackets + + fun setSignatureExpirationTime(isCritical: Boolean, seconds: Long): BaseSignatureSubpackets + + fun setSignatureExpirationTime(expirationTime: SignatureExpirationTime?): BaseSignatureSubpackets + + fun setSignerUserId(userId: CharSequence): BaseSignatureSubpackets + + fun setSignerUserId(isCritical: Boolean, userId: CharSequence): BaseSignatureSubpackets + + fun setSignerUserId(signerUserID: SignerUserID?): BaseSignatureSubpackets + + fun addNotationData(isCritical: Boolean, notationName: String, notationValue: String): BaseSignatureSubpackets + + fun addNotationData(isCritical: Boolean, isHumanReadable: Boolean, notationName: String, notationValue: String): BaseSignatureSubpackets + + fun addNotationData(notationData: NotationData): BaseSignatureSubpackets + + fun clearNotationData(): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint(isCritical: Boolean, recipientKey: PGPPublicKey): BaseSignatureSubpackets + + fun addIntendedRecipientFingerprint(intendedRecipient: IntendedRecipientFingerprint): BaseSignatureSubpackets + + fun clearIntendedRecipientFingerprints(): BaseSignatureSubpackets + + fun setExportable(): BaseSignatureSubpackets + + fun setExportable(isExportable: Boolean): BaseSignatureSubpackets + + fun setExportable(isCritical: Boolean, isExportable: Boolean): BaseSignatureSubpackets + + fun setExportable(exportable: Exportable?): BaseSignatureSubpackets + + fun setPolicyUrl(policyUrl: URL): BaseSignatureSubpackets + + fun setPolicyUrl(isCritical: Boolean, policyUrl: URL): BaseSignatureSubpackets + + fun setPolicyUrl(policyUrl: PolicyURI?): BaseSignatureSubpackets + + fun setRegularExpression(regex: CharSequence): BaseSignatureSubpackets + + fun setRegularExpression(isCritical: Boolean, regex: CharSequence): BaseSignatureSubpackets + + fun setRegularExpression(regex: RegularExpression?): BaseSignatureSubpackets + + fun setRevocable(): BaseSignatureSubpackets + + fun setRevocable(isRevocable: Boolean): BaseSignatureSubpackets + + fun setRevocable(isCritical: Boolean, isRevocable: Boolean): BaseSignatureSubpackets + + fun setRevocable(revocable: Revocable?): BaseSignatureSubpackets + + fun setSignatureTarget(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): BaseSignatureSubpackets + + fun setSignatureTarget(isCritical: Boolean, keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): BaseSignatureSubpackets + + fun setSignatureTarget(signatureTarget: SignatureTarget?): BaseSignatureSubpackets + + fun setTrust(depth: Int, amount: Int): BaseSignatureSubpackets + + fun setTrust(isCritical: Boolean, depth: Int, amount: Int): BaseSignatureSubpackets + + fun setTrust(trust: TrustSignature?): BaseSignatureSubpackets + + @Throws(IOException::class) + fun addEmbeddedSignature(signature: PGPSignature): BaseSignatureSubpackets + + @Throws(IOException::class) + fun addEmbeddedSignature(isCritical: Boolean, signature: PGPSignature): BaseSignatureSubpackets + + fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): BaseSignatureSubpackets + + fun clearEmbeddedSignatures(): BaseSignatureSubpackets +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt new file mode 100644 index 00000000..423edd8e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +interface CertificationSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt new file mode 100644 index 00000000..327ed4a9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.RevocationReason +import org.pgpainless.key.util.RevocationAttributes + +interface RevocationSignatureSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + fun setRevocationReason(revocationAttributes: RevocationAttributes): RevocationSignatureSubpackets + + fun setRevocationReason(isCritical: Boolean, revocationAttributes: RevocationAttributes): RevocationSignatureSubpackets + + fun setRevocationReason(isCritical: Boolean, reason: RevocationAttributes.Reason, description: CharSequence): RevocationSignatureSubpackets + + fun setRevocationReason(reason: RevocationReason?): RevocationSignatureSubpackets +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt new file mode 100644 index 00000000..ce8e09a9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.Features +import org.bouncycastle.bcpg.sig.KeyExpirationTime +import org.bouncycastle.bcpg.sig.KeyFlags +import org.bouncycastle.bcpg.sig.PreferredAlgorithms +import org.bouncycastle.bcpg.sig.PrimaryUserID +import org.bouncycastle.bcpg.sig.RevocationKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.pgpainless.algorithm.* +import java.util.* + +interface SelfSignatureSubpackets : BaseSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + fun setKeyFlags(vararg keyflags: KeyFlag): SelfSignatureSubpackets + + fun setKeyFlags(keyFlags: List): SelfSignatureSubpackets + + fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SelfSignatureSubpackets + + fun setKeyFlags(keyFlags: KeyFlags?): SelfSignatureSubpackets + + fun setPrimaryUserId(): SelfSignatureSubpackets + + fun setPrimaryUserId(isCritical: Boolean): SelfSignatureSubpackets + + fun setPrimaryUserId(primaryUserID: PrimaryUserID?): SelfSignatureSubpackets + + fun setKeyExpirationTime(key: PGPPublicKey, keyExpirationTime: Date?): SelfSignatureSubpackets + + fun setKeyExpirationTime(keyCreationTime: Date, keyExpirationTime: Date?): SelfSignatureSubpackets + + fun setKeyExpirationTime(isCritical: Boolean, keyCreationTime: Date, keyExpirationTime: Date?): SelfSignatureSubpackets + + fun setKeyExpirationTime(isCritical: Boolean, secondsFromCreationToExpiration: Long): SelfSignatureSubpackets + + fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms(algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredCompressionAlgorithms(preferredAlgorithms: PreferredAlgorithms?): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms(algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredSymmetricKeyAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + + fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets + + fun addRevocationKey(revocationKey: PGPPublicKey): SelfSignatureSubpackets + + fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets + + fun addRevocationKey(isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets + + fun addRevocationKey(revocationKey: RevocationKey): SelfSignatureSubpackets + + fun clearRevocationKeys(): SelfSignatureSubpackets + + fun setFeatures(vararg features: Feature): SelfSignatureSubpackets + + fun setFeatures(isCritical: Boolean, vararg features: Feature): SelfSignatureSubpackets + + fun setFeatures(features: Features?): SelfSignatureSubpackets +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt new file mode 100644 index 00000000..d147dc97 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +interface SignatureSubpacketCallback { + + /** + * Callback method that can be used to modify the hashed subpackets of a signature. + * + * @param hashedSubpackets hashed subpackets + */ + fun modifyHashedSubpackets(hashedSubpackets: S) { + // Empty default implementation to allow for cleaner overriding + } + + /** + * Callback method that can be used to modify the unhashed subpackets of a signature. + * + * @param unhashedSubpackets unhashed subpackets + */ + fun modifyUnhashedSubpackets(unhashedSubpackets: S) { + // Empty default implementation to allow for cleaner overriding + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt new file mode 100644 index 00000000..d7d907f0 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -0,0 +1,491 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import openpgp.secondsTill +import openpgp.toSecondsPrecision +import org.bouncycastle.bcpg.SignatureSubpacket +import org.bouncycastle.bcpg.SignatureSubpacketTags +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.key.util.RevocationAttributes +import java.lang.IllegalArgumentException +import java.net.URL +import java.util.* +import kotlin.experimental.or + +class SignatureSubpackets + : BaseSignatureSubpackets, SelfSignatureSubpackets, CertificationSubpackets, RevocationSignatureSubpackets { + + interface Callback : SignatureSubpacketCallback + + var signatureCreationTimeSubpacket: SignatureCreationTime? = null + var signatureExpirationTimeSubpacket: SignatureExpirationTime? = null + var issuerKeyIdSubpacket: IssuerKeyID? = null + var issuerFingerprintSubpacket: IssuerFingerprint? = null + val notationDataSubpackets: List = mutableListOf() + val intendedRecipientFingerprintSubpackets: List = mutableListOf() + val revocationKeySubpackets: List = mutableListOf() + var exportableSubpacket: Exportable? = null + var signatureTargetSubpacket: SignatureTarget? = null + var featuresSubpacket: Features? = null + var keyFlagsSubpacket: KeyFlags? = null + var trustSubpacket: TrustSignature? = null + var preferredCompressionAlgorithmsSubpacket: PreferredAlgorithms? = null + var preferredSymmetricKeyAlgorithmsSubpacket: PreferredAlgorithms? = null + var preferredHashAlgorithmsSubpacket: PreferredAlgorithms? = null + val embeddedSignatureSubpackets: List = mutableListOf() + var signerUserIdSubpacket: SignerUserID? = null + var keyExpirationTimeSubpacket: KeyExpirationTime? = null + var policyURISubpacket: PolicyURI? = null + var primaryUserIdSubpacket: PrimaryUserID? = null + var regularExpressionSubpacket: RegularExpression? = null + var revocableSubpacket: Revocable? = null + var revocationReasonSubpacket: RevocationReason? = null + val residualSubpackets: List = mutableListOf() + + companion object { + + @JvmStatic + fun refreshHashedSubpackets(issuer: PGPPublicKey, oldSignature: PGPSignature): SignatureSubpackets { + return createHashedSubpacketsFrom(issuer, oldSignature.hashedSubPackets) + } + + @JvmStatic + fun refreshUnhashedSubpackets(oldSignature: PGPSignature): SignatureSubpackets { + return createSubpacketsFrom(oldSignature.unhashedSubPackets) + } + + @JvmStatic + fun createHashedSubpacketsFrom(issuer: PGPPublicKey, base: PGPSignatureSubpacketVector): SignatureSubpackets { + return createSubpacketsFrom(base).apply { + setIssuerFingerprintAndKeyId(issuer) + } + } + + @JvmStatic + fun createSubpacketsFrom(base: PGPSignatureSubpacketVector): SignatureSubpackets { + return SignatureSubpackets().apply { + SignatureSubpacketsHelper.applyFrom(base, this) + } + } + + @JvmStatic + fun createHashedSubpackets(issuer: PGPPublicKey): SignatureSubpackets { + return SignatureSubpackets().apply { + setIssuerFingerprintAndKeyId(issuer) + } + } + + @JvmStatic + fun createEmptySubpackets(): SignatureSubpackets { + return SignatureSubpackets() + } + } + + override fun setRevocationReason(revocationAttributes: RevocationAttributes): SignatureSubpackets = apply { + setRevocationReason(false, revocationAttributes) + } + + override fun setRevocationReason(isCritical: Boolean, revocationAttributes: RevocationAttributes): SignatureSubpackets = apply { + setRevocationReason(isCritical, revocationAttributes.reason, revocationAttributes.description) + } + + override fun setRevocationReason(isCritical: Boolean, reason: RevocationAttributes.Reason, description: CharSequence): SignatureSubpackets = apply { + setRevocationReason(RevocationReason(isCritical, reason.code, description.toString())) + } + + override fun setRevocationReason(reason: RevocationReason?): SignatureSubpackets = apply { + this.revocationReasonSubpacket = reason + } + + override fun setKeyFlags(vararg keyflags: KeyFlag): SignatureSubpackets = apply { + setKeyFlags(true, *keyflags) + } + + override fun setKeyFlags(keyFlags: List): SignatureSubpackets = apply { + setKeyFlags(true, *keyFlags.toTypedArray()) + } + + override fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SignatureSubpackets = apply { + setKeyFlags(KeyFlags(isCritical, KeyFlag.toBitmask(*keyFlags))) + } + + override fun setKeyFlags(keyFlags: KeyFlags?): SignatureSubpackets = apply { + this.keyFlagsSubpacket = keyFlags + } + + override fun setPrimaryUserId(): SignatureSubpackets = apply { + setPrimaryUserId(true) + } + + override fun setPrimaryUserId(isCritical: Boolean): SignatureSubpackets = apply { + setPrimaryUserId(PrimaryUserID(isCritical, true)) + } + + override fun setPrimaryUserId(primaryUserID: PrimaryUserID?): SignatureSubpackets = apply { + this.primaryUserIdSubpacket = primaryUserID + } + + override fun setKeyExpirationTime(key: PGPPublicKey, keyExpirationTime: Date?): SignatureSubpackets = apply { + setKeyExpirationTime(key.creationTime, keyExpirationTime) + } + + override fun setKeyExpirationTime(keyCreationTime: Date, keyExpirationTime: Date?): SignatureSubpackets = apply { + setKeyExpirationTime(true, keyCreationTime, keyExpirationTime) + } + + override fun setKeyExpirationTime(isCritical: Boolean, keyCreationTime: Date, keyExpirationTime: Date?): SignatureSubpackets = apply { + if (keyExpirationTime == null) { + setKeyExpirationTime(isCritical, 0) + } else { + setKeyExpirationTime(isCritical, keyCreationTime.secondsTill(keyExpirationTime)) + } + } + + override fun setKeyExpirationTime(isCritical: Boolean, secondsFromCreationToExpiration: Long): SignatureSubpackets = apply { + enforceExpirationBounds(secondsFromCreationToExpiration) + setKeyExpirationTime(KeyExpirationTime(isCritical, secondsFromCreationToExpiration)) + } + + override fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SignatureSubpackets = apply { + this.keyExpirationTimeSubpacket = keyExpirationTime + } + + override fun setPreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): SignatureSubpackets = apply { + setPreferredCompressionAlgorithms(setOf(*algorithms)) + } + + override fun setPreferredCompressionAlgorithms(algorithms: Collection): SignatureSubpackets = apply { + setPreferredCompressionAlgorithms(false, algorithms) + } + + override fun setPreferredCompressionAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { + setPreferredCompressionAlgorithms(PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_COMP_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray())) + } + + override fun setPreferredCompressionAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { + require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_COMP_ALGS) { + "Invalid preferred compression algorithms type." + } + this.preferredCompressionAlgorithmsSubpacket = algorithms + } + + override fun setPreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): SignatureSubpackets = apply { + setPreferredSymmetricKeyAlgorithms(setOf(*algorithms)) + } + + override fun setPreferredSymmetricKeyAlgorithms(algorithms: Collection): SignatureSubpackets = apply { + setPreferredSymmetricKeyAlgorithms(false, algorithms) + } + + override fun setPreferredSymmetricKeyAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { + setPreferredSymmetricKeyAlgorithms(PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_SYM_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray() + )) + } + + override fun setPreferredSymmetricKeyAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { + require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_SYM_ALGS) { + "Invalid preferred symmetric algorithms type." + } + this.preferredSymmetricKeyAlgorithmsSubpacket = algorithms + } + + override fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SignatureSubpackets = apply { + setPreferredHashAlgorithms(setOf(*algorithms)) + } + + override fun setPreferredHashAlgorithms(algorithms: Collection): SignatureSubpackets = apply { + setPreferredHashAlgorithms(false, algorithms) + } + + override fun setPreferredHashAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { + setPreferredHashAlgorithms(PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_HASH_ALGS, + isCritical, + algorithms.map { it.algorithmId }.toIntArray() + )) + } + + override fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { + require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_HASH_ALGS) { + "Invalid preferred hash algorithms type." + } + this.preferredHashAlgorithmsSubpacket = algorithms + } + + override fun addRevocationKey(revocationKey: PGPPublicKey): SignatureSubpackets = apply { + addRevocationKey(true, revocationKey) + } + + override fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SignatureSubpackets = apply { + addRevocationKey(isCritical, false, revocationKey) + } + + override fun addRevocationKey(isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey): SignatureSubpackets = apply { + val clazz = 0x80.toByte() or if (isSensitive) 0x40.toByte() else 0x00.toByte() + addRevocationKey(RevocationKey(isCritical, clazz, revocationKey.algorithm, revocationKey.fingerprint)) + } + + override fun addRevocationKey(revocationKey: RevocationKey): SignatureSubpackets = apply { + (this.revocationKeySubpackets as MutableList).add(revocationKey) + } + + override fun clearRevocationKeys(): SignatureSubpackets = apply { + (this.revocationKeySubpackets as MutableList).clear() + } + + override fun setFeatures(vararg features: Feature): SignatureSubpackets = apply { + setFeatures(true, *features) + } + + override fun setFeatures(isCritical: Boolean, vararg features: Feature): SignatureSubpackets = apply { + setFeatures(Features(isCritical, Feature.toBitmask(*features))) + } + + override fun setFeatures(features: Features?): SignatureSubpackets = apply { + this.featuresSubpacket = features + } + + override fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): SignatureSubpackets = apply { + setIssuerKeyId(key.keyID) + setIssuerFingerprint(key) + } + + override fun setIssuerKeyId(keyId: Long): SignatureSubpackets = apply { + setIssuerKeyId(false, keyId) + } + + override fun setIssuerKeyId(isCritical: Boolean, keyId: Long): SignatureSubpackets = apply { + setIssuerKeyId(IssuerKeyID(isCritical, keyId)) + } + + override fun setIssuerKeyId(issuerKeyID: IssuerKeyID?): SignatureSubpackets = apply { + this.issuerKeyIdSubpacket = issuerKeyID + } + + override fun setIssuerFingerprint(isCritical: Boolean, issuer: PGPPublicKey): SignatureSubpackets = apply { + setIssuerFingerprint(IssuerFingerprint(isCritical, issuer.version, issuer.fingerprint)) + } + + override fun setIssuerFingerprint(issuer: PGPPublicKey): SignatureSubpackets = apply { + setIssuerFingerprint(false, issuer) + } + + override fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): SignatureSubpackets = apply { + this.issuerFingerprintSubpacket = fingerprint + } + + override fun setSignatureCreationTime(creationTime: Date): SignatureSubpackets = apply { + setSignatureCreationTime(true, creationTime) + } + + override fun setSignatureCreationTime(isCritical: Boolean, creationTime: Date): SignatureSubpackets = apply { + setSignatureCreationTime(SignatureCreationTime(isCritical, creationTime)) + } + + override fun setSignatureCreationTime(creationTime: SignatureCreationTime?): SignatureSubpackets = apply { + this.signatureCreationTimeSubpacket = creationTime + } + override fun setSignatureExpirationTime(creationTime: Date, expirationTime: Date?): SignatureSubpackets = apply { + setSignatureExpirationTime(true, creationTime, expirationTime) + } + + override fun setSignatureExpirationTime(isCritical: Boolean, creationTime: Date, expirationTime: Date?): SignatureSubpackets = apply { + if (expirationTime != null) { + require(creationTime.toSecondsPrecision() < expirationTime.toSecondsPrecision()) { + "Expiration time MUST NOT be less or equal the creation time." + } + setSignatureExpirationTime(SignatureExpirationTime(isCritical, creationTime.secondsTill(expirationTime))) + } else { + setSignatureExpirationTime(SignatureExpirationTime(isCritical, 0)) + } + } + + override fun setSignatureExpirationTime(isCritical: Boolean, seconds: Long): SignatureSubpackets = apply { + enforceExpirationBounds(seconds) + setSignatureExpirationTime(SignatureExpirationTime(isCritical, seconds)) + } + + /** + * Enforce that
seconds
is within bounds of an unsigned 32bit number. + * Values less than 0 are illegal, as well as values greater 0xffffffff. + * + * @param seconds number to check + * @throws IllegalArgumentException in case of an under- or overflow + */ + private fun enforceExpirationBounds(seconds: Long) { + require(seconds <= 0xffffffffL) { + "Integer overflow. Seconds from creation to expiration (${seconds}) cannot be larger than ${0xffffffffL}." + } + require(seconds >= 0) { + "Seconds from creation to expiration cannot be less than 0." + } + } + + override fun setSignatureExpirationTime(expirationTime: SignatureExpirationTime?): SignatureSubpackets = apply { + this.signatureExpirationTimeSubpacket = expirationTime + } + + override fun setSignerUserId(userId: CharSequence): SignatureSubpackets = apply { + setSignerUserId(false, userId) + } + + override fun setSignerUserId(isCritical: Boolean, userId: CharSequence): SignatureSubpackets = apply { + setSignerUserId(SignerUserID(isCritical, userId.toString())) + } + + override fun setSignerUserId(signerUserID: SignerUserID?): SignatureSubpackets = apply { + this.signerUserIdSubpacket = signerUserID + } + + override fun addNotationData(isCritical: Boolean, notationName: String, notationValue: String): SignatureSubpackets = apply { + addNotationData(isCritical, true, notationName, notationValue) + } + + override fun addNotationData(isCritical: Boolean, isHumanReadable: Boolean, notationName: String, notationValue: String): SignatureSubpackets = apply { + addNotationData(NotationData(isCritical, isHumanReadable, notationName, notationValue)) + } + + override fun addNotationData(notationData: NotationData): SignatureSubpackets = apply { + (this.notationDataSubpackets as MutableList).add(notationData) + } + + override fun clearNotationData(): SignatureSubpackets = apply { + (this.notationDataSubpackets as MutableList).clear() + } + + override fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): SignatureSubpackets = apply { + addIntendedRecipientFingerprint(false, recipientKey) + } + + override fun addIntendedRecipientFingerprint(isCritical: Boolean, recipientKey: PGPPublicKey): SignatureSubpackets = apply { + addIntendedRecipientFingerprint(IntendedRecipientFingerprint(isCritical, recipientKey.version, recipientKey.fingerprint)) + } + + override fun addIntendedRecipientFingerprint(intendedRecipient: IntendedRecipientFingerprint): SignatureSubpackets = apply { + (this.intendedRecipientFingerprintSubpackets as MutableList).add(intendedRecipient) + } + + override fun clearIntendedRecipientFingerprints(): SignatureSubpackets = apply { + (this.intendedRecipientFingerprintSubpackets as MutableList).clear() + } + + override fun setExportable(): SignatureSubpackets = apply { + setExportable(true) + } + + override fun setExportable(isExportable: Boolean): SignatureSubpackets = apply { + setExportable(true, isExportable) + } + + override fun setExportable(isCritical: Boolean, isExportable: Boolean): SignatureSubpackets = apply { + setExportable(Exportable(isCritical, isExportable)) + } + + override fun setExportable(exportable: Exportable?): SignatureSubpackets = apply { + this.exportableSubpacket = exportable + } + + override fun setPolicyUrl(policyUrl: URL): SignatureSubpackets = apply { + setPolicyUrl(false, policyUrl) + } + + override fun setPolicyUrl(isCritical: Boolean, policyUrl: URL): SignatureSubpackets = apply { + setPolicyUrl(PolicyURI(isCritical, policyUrl.toString())) + } + + override fun setPolicyUrl(policyUrl: PolicyURI?): SignatureSubpackets = apply { + this.policyURISubpacket = policyURISubpacket + } + + override fun setRegularExpression(regex: CharSequence): SignatureSubpackets = apply { + setRegularExpression(false, regex) + } + + override fun setRegularExpression(isCritical: Boolean, regex: CharSequence): SignatureSubpackets = apply { + setRegularExpression(RegularExpression(isCritical, regex.toString())) + } + + override fun setRegularExpression(regex: RegularExpression?): SignatureSubpackets = apply { + this.regularExpressionSubpacket = regex + } + + override fun setRevocable(): SignatureSubpackets = apply { + setRevocable(true) + } + + override fun setRevocable(isRevocable: Boolean): SignatureSubpackets = apply { + setRevocable(true, isRevocable) + } + + override fun setRevocable(isCritical: Boolean, isRevocable: Boolean): SignatureSubpackets = apply { + setRevocable(Revocable(isCritical, isRevocable)) + } + + override fun setRevocable(revocable: Revocable?): SignatureSubpackets = apply { + this.revocableSubpacket = revocable + } + + override fun setSignatureTarget(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): SignatureSubpackets = apply { + setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData) + } + + override fun setSignatureTarget(isCritical: Boolean, keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): SignatureSubpackets = apply { + setSignatureTarget(SignatureTarget(isCritical, keyAlgorithm.algorithmId, hashAlgorithm.algorithmId, hashData)) + } + + override fun setSignatureTarget(signatureTarget: SignatureTarget?): SignatureSubpackets = apply { + this.signatureTargetSubpacket = signatureTarget + } + + override fun setTrust(depth: Int, amount: Int): SignatureSubpackets = apply { + setTrust(true, depth, amount) + } + + override fun setTrust(isCritical: Boolean, depth: Int, amount: Int): SignatureSubpackets = apply { + setTrust(TrustSignature(isCritical, depth, amount)) + } + + override fun setTrust(trust: TrustSignature?): SignatureSubpackets = apply { + this.trustSubpacket = trust + } + + override fun addEmbeddedSignature(signature: PGPSignature): SignatureSubpackets = apply { + addEmbeddedSignature(true, signature) + } + + override fun addEmbeddedSignature(isCritical: Boolean, signature: PGPSignature): SignatureSubpackets = apply { + val sig = signature.encoded + val data = if (sig.size - 1 > 256) { + ByteArray(sig.size - 3) + } else { + ByteArray(sig.size - 2) + } + System.arraycopy(sig, sig.size - data.size, data, 0, data.size) + addEmbeddedSignature(EmbeddedSignature(isCritical, false, data)) + } + + override fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): SignatureSubpackets = apply { + (this.embeddedSignatureSubpackets as MutableList).add(embeddedSignature) + } + + override fun clearEmbeddedSignatures(): SignatureSubpackets = apply { + (this.embeddedSignatureSubpackets as MutableList).clear() + } + + fun addResidualSubpacket(subpacket: org.bouncycastle.bcpg.SignatureSubpacket): SignatureSubpackets = apply { + (residualSubpackets as MutableList).add(subpacket) + } +} \ No newline at end of file From b72f95b46cc00caff7eb93505a0045f76a4ee884 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 20 Oct 2023 14:55:51 +0200 Subject: [PATCH 185/351] Kotlin conversion: SignatureSubpacketsHelper --- .../subpackets/SignatureSubpacketsHelper.java | 196 ------------------ .../signature/subpackets/package-info.java | 8 - .../subpackets/SignatureSubpackets.kt | 8 +- .../subpackets/SignatureSubpacketsHelper.kt | 159 ++++++++++++++ 4 files changed, 161 insertions(+), 210 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java deleted file mode 100644 index 2a7d4f83..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.java +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.subpackets; - -import org.bouncycastle.bcpg.SignatureSubpacket; -import org.bouncycastle.bcpg.sig.EmbeddedSignature; -import org.bouncycastle.bcpg.sig.Exportable; -import org.bouncycastle.bcpg.sig.Features; -import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; -import org.bouncycastle.bcpg.sig.KeyExpirationTime; -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.PolicyURI; -import org.bouncycastle.bcpg.sig.PreferredAlgorithms; -import org.bouncycastle.bcpg.sig.PrimaryUserID; -import org.bouncycastle.bcpg.sig.RegularExpression; -import org.bouncycastle.bcpg.sig.Revocable; -import org.bouncycastle.bcpg.sig.RevocationKey; -import org.bouncycastle.bcpg.sig.RevocationReason; -import org.bouncycastle.bcpg.sig.SignatureExpirationTime; -import org.bouncycastle.bcpg.sig.SignatureTarget; -import org.bouncycastle.bcpg.sig.SignerUserID; -import org.bouncycastle.bcpg.sig.TrustSignature; -import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.pgpainless.algorithm.Feature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.key.util.RevocationAttributes; - -public class SignatureSubpacketsHelper { - - public static SignatureSubpackets applyFrom(PGPSignatureSubpacketVector vector, SignatureSubpackets subpackets) { - for (SignatureSubpacket subpacket : vector.toArray()) { - org.pgpainless.algorithm.SignatureSubpacket type = org.pgpainless.algorithm.SignatureSubpacket.requireFromCode(subpacket.getType()); - switch (type) { - case signatureCreationTime: - case issuerKeyId: - case issuerFingerprint: - // ignore, we override this anyways - break; - case signatureExpirationTime: - SignatureExpirationTime sigExpTime = (SignatureExpirationTime) subpacket; - subpackets.setSignatureExpirationTime(sigExpTime.isCritical(), sigExpTime.getTime()); - break; - case exportableCertification: - Exportable exp = (Exportable) subpacket; - subpackets.setExportable(exp.isCritical(), exp.isExportable()); - break; - case trustSignature: - TrustSignature trustSignature = (TrustSignature) subpacket; - subpackets.setTrust(trustSignature.isCritical(), trustSignature.getDepth(), trustSignature.getTrustAmount()); - break; - case revocable: - Revocable rev = (Revocable) subpacket; - subpackets.setRevocable(rev.isCritical(), rev.isRevocable()); - break; - case keyExpirationTime: - KeyExpirationTime keyExpTime = (KeyExpirationTime) subpacket; - subpackets.setKeyExpirationTime(keyExpTime.isCritical(), keyExpTime.getTime()); - break; - case preferredSymmetricAlgorithms: - subpackets.setPreferredSymmetricKeyAlgorithms((PreferredAlgorithms) subpacket); - break; - case revocationKey: - RevocationKey revocationKey = (RevocationKey) subpacket; - subpackets.addRevocationKey(revocationKey); - break; - case notationData: - NotationData notationData = (NotationData) subpacket; - subpackets.addNotationData(notationData.isCritical(), notationData.getNotationName(), notationData.getNotationValue()); - break; - case preferredHashAlgorithms: - subpackets.setPreferredHashAlgorithms((PreferredAlgorithms) subpacket); - break; - case preferredCompressionAlgorithms: - subpackets.setPreferredCompressionAlgorithms((PreferredAlgorithms) subpacket); - break; - case primaryUserId: - PrimaryUserID primaryUserID = (PrimaryUserID) subpacket; - subpackets.setPrimaryUserId(primaryUserID); - break; - case keyFlags: - KeyFlags flags = (KeyFlags) subpacket; - subpackets.setKeyFlags(flags.isCritical(), KeyFlag.fromBitmask(flags.getFlags()).toArray(new KeyFlag[0])); - break; - case signerUserId: - SignerUserID signerUserID = (SignerUserID) subpacket; - subpackets.setSignerUserId(signerUserID.isCritical(), signerUserID.getID()); - break; - case revocationReason: - RevocationReason reason = (RevocationReason) subpacket; - subpackets.setRevocationReason(reason.isCritical(), - RevocationAttributes.Reason.fromCode(reason.getRevocationReason()), - reason.getRevocationDescription()); - break; - case features: - Features f = (Features) subpacket; - subpackets.setFeatures(f.isCritical(), Feature.fromBitmask(f.getData()[0]).toArray(new Feature[0])); - break; - case signatureTarget: - SignatureTarget target = (SignatureTarget) subpacket; - subpackets.setSignatureTarget(target.isCritical(), - PublicKeyAlgorithm.requireFromId(target.getPublicKeyAlgorithm()), - HashAlgorithm.requireFromId(target.getHashAlgorithm()), - target.getHashData()); - break; - case embeddedSignature: - EmbeddedSignature embeddedSignature = (EmbeddedSignature) subpacket; - subpackets.addEmbeddedSignature(embeddedSignature); - break; - case intendedRecipientFingerprint: - IntendedRecipientFingerprint intendedRecipientFingerprint = (IntendedRecipientFingerprint) subpacket; - subpackets.addIntendedRecipientFingerprint(intendedRecipientFingerprint); - break; - case policyUrl: - PolicyURI policyURI = (PolicyURI) subpacket; - subpackets.setPolicyUrl(policyURI); - break; - case regularExpression: - RegularExpression regex = (RegularExpression) subpacket; - subpackets.setRegularExpression(regex); - break; - - case keyServerPreferences: - case preferredKeyServers: - case placeholder: - case preferredAEADAlgorithms: - case attestedCertification: - subpackets.addResidualSubpacket(subpacket); - break; - } - } - return subpackets; - } - - public static PGPSignatureSubpacketGenerator applyTo(SignatureSubpackets subpackets, PGPSignatureSubpacketGenerator generator) { - addSubpacket(generator, subpackets.getIssuerKeyIdSubpacket()); - addSubpacket(generator, subpackets.getIssuerFingerprintSubpacket()); - addSubpacket(generator, subpackets.getSignatureCreationTimeSubpacket()); - addSubpacket(generator, subpackets.getSignatureExpirationTimeSubpacket()); - addSubpacket(generator, subpackets.getExportableSubpacket()); - addSubpacket(generator, subpackets.getPolicyURISubpacket()); - addSubpacket(generator, subpackets.getRegularExpressionSubpacket()); - for (NotationData notationData : subpackets.getNotationDataSubpackets()) { - addSubpacket(generator, notationData); - } - for (IntendedRecipientFingerprint intendedRecipientFingerprint : subpackets.getIntendedRecipientFingerprintSubpackets()) { - addSubpacket(generator, intendedRecipientFingerprint); - } - for (RevocationKey revocationKey : subpackets.getRevocationKeySubpackets()) { - addSubpacket(generator, revocationKey); - } - addSubpacket(generator, subpackets.getSignatureTargetSubpacket()); - addSubpacket(generator, subpackets.getFeaturesSubpacket()); - addSubpacket(generator, subpackets.getKeyFlagsSubpacket()); - addSubpacket(generator, subpackets.getTrustSubpacket()); - addSubpacket(generator, subpackets.getPreferredCompressionAlgorithmsSubpacket()); - addSubpacket(generator, subpackets.getPreferredSymmetricKeyAlgorithmsSubpacket()); - addSubpacket(generator, subpackets.getPreferredHashAlgorithmsSubpacket()); - for (EmbeddedSignature embeddedSignature : subpackets.getEmbeddedSignatureSubpackets()) { - addSubpacket(generator, embeddedSignature); - } - addSubpacket(generator, subpackets.getSignerUserIdSubpacket()); - addSubpacket(generator, subpackets.getKeyExpirationTimeSubpacket()); - addSubpacket(generator, subpackets.getPrimaryUserIdSubpacket()); - addSubpacket(generator, subpackets.getRevocableSubpacket()); - addSubpacket(generator, subpackets.getRevocationReasonSubpacket()); - for (SignatureSubpacket subpacket : subpackets.getResidualSubpackets()) { - addSubpacket(generator, subpacket); - } - - return generator; - } - - private static void addSubpacket(PGPSignatureSubpacketGenerator generator, SignatureSubpacket subpacket) { - if (subpacket != null) { - generator.addCustomSubpacket(subpacket); - } - } - - public static PGPSignatureSubpacketVector toVector(SignatureSubpackets subpackets) { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - applyTo(subpackets, generator); - return generator.generate(); - } - - public static PGPSignatureSubpacketVector toVector(RevocationSignatureSubpackets subpackets) { - PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); - applyTo((SignatureSubpackets) subpackets, generator); - return generator.generate(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java deleted file mode 100644 index 09dfd6a2..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature.subpackets; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt index d7d907f0..a88e4e8a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -70,16 +70,12 @@ class SignatureSubpackets @JvmStatic fun createSubpacketsFrom(base: PGPSignatureSubpacketVector): SignatureSubpackets { - return SignatureSubpackets().apply { - SignatureSubpacketsHelper.applyFrom(base, this) - } + return SignatureSubpacketsHelper.applyFrom(base, SignatureSubpackets()) } @JvmStatic fun createHashedSubpackets(issuer: PGPPublicKey): SignatureSubpackets { - return SignatureSubpackets().apply { - setIssuerFingerprintAndKeyId(issuer) - } + return createEmptySubpackets().setIssuerFingerprintAndKeyId(issuer) } @JvmStatic diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt new file mode 100644 index 00000000..6767b0af --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets + +import org.bouncycastle.bcpg.sig.* +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector +import org.pgpainless.algorithm.* +import org.pgpainless.key.util.RevocationAttributes + +class SignatureSubpacketsHelper { + + companion object { + @JvmStatic + fun applyFrom(vector: PGPSignatureSubpacketVector, subpackets: SignatureSubpackets) = subpackets.apply { + for (subpacket in vector.toArray()) { + val type = SignatureSubpacket.requireFromCode(subpacket.type) + when (type) { + SignatureSubpacket.signatureCreationTime, + SignatureSubpacket.issuerKeyId, + SignatureSubpacket.issuerFingerprint -> { /* ignore, we override this anyway */ } + SignatureSubpacket.signatureExpirationTime -> (subpacket as SignatureExpirationTime).let { + subpackets.setSignatureExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.exportableCertification -> (subpacket as Exportable).let { + subpackets.setExportable(it.isCritical, it.isExportable) + } + SignatureSubpacket.trustSignature -> (subpacket as TrustSignature).let { + subpackets.setTrust(it.isCritical, it.depth, it.trustAmount) + } + SignatureSubpacket.revocable -> (subpacket as Revocable).let { + subpackets.setRevocable(it.isCritical, it.isRevocable) + } + SignatureSubpacket.keyExpirationTime -> (subpacket as KeyExpirationTime).let { + subpackets.setKeyExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.preferredSymmetricAlgorithms -> (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredSymmetricKeyAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredHashAlgorithms -> (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredHashAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredCompressionAlgorithms -> (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredCompressionAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.revocationKey -> (subpacket as RevocationKey).let { + subpackets.addRevocationKey(RevocationKey(it.isCritical, it.signatureClass, it.algorithm, it.fingerprint)) + } + SignatureSubpacket.notationData -> (subpacket as NotationData).let { + subpackets.addNotationData(it.isCritical, it.isHumanReadable, it.notationName, it.notationValue) + } + SignatureSubpacket.primaryUserId -> (subpacket as PrimaryUserID).let { + subpackets.setPrimaryUserId(PrimaryUserID(it.isCritical, it.isPrimaryUserID)) + } + SignatureSubpacket.keyFlags -> (subpacket as KeyFlags).let { + subpackets.setKeyFlags(it.isCritical, *(KeyFlag.fromBitmask(it.flags).toTypedArray())) + } + SignatureSubpacket.signerUserId -> (subpacket as SignerUserID).let { + subpackets.setSignerUserId(it.isCritical, it.id) + } + SignatureSubpacket.revocationReason -> (subpacket as RevocationReason).let { + subpackets.setRevocationReason(it.isCritical, RevocationAttributes.Reason.fromCode(it.revocationReason), it.revocationDescription) + } + SignatureSubpacket.features -> (subpacket as Features).let { + subpackets.setFeatures(it.isCritical, *(Feature.fromBitmask(it.features.toInt()).toTypedArray())) + } + SignatureSubpacket.signatureTarget -> (subpacket as SignatureTarget).let { + subpackets.setSignatureTarget(it.isCritical, + PublicKeyAlgorithm.requireFromId(it.publicKeyAlgorithm), + HashAlgorithm.requireFromId(it.hashAlgorithm), + it.hashData) + } + SignatureSubpacket.embeddedSignature -> (subpacket as EmbeddedSignature).let { + subpackets.addEmbeddedSignature(it) + } + SignatureSubpacket.intendedRecipientFingerprint -> (subpacket as IntendedRecipientFingerprint).let { + subpackets.addIntendedRecipientFingerprint(it) + } + SignatureSubpacket.policyUrl -> (subpacket as PolicyURI).let { + subpackets.setPolicyUrl(it) + } + SignatureSubpacket.regularExpression -> (subpacket as RegularExpression).let { + subpackets.setRegularExpression(it) + } + SignatureSubpacket.keyServerPreferences, + SignatureSubpacket.preferredKeyServers, + SignatureSubpacket.placeholder, + SignatureSubpacket.preferredAEADAlgorithms, + SignatureSubpacket.attestedCertification -> subpackets.addResidualSubpacket(subpacket) + } + } + } + + @JvmStatic + fun applyTo(subpackets: SignatureSubpackets, generator: PGPSignatureSubpacketGenerator): PGPSignatureSubpacketGenerator { + return generator.apply { + addSubpacket(subpackets.issuerKeyIdSubpacket) + addSubpacket(subpackets.issuerFingerprintSubpacket) + addSubpacket(subpackets.signatureCreationTimeSubpacket) + addSubpacket(subpackets.signatureExpirationTimeSubpacket) + addSubpacket(subpackets.exportableSubpacket) + addSubpacket(subpackets.policyURISubpacket) + addSubpacket(subpackets.regularExpressionSubpacket) + for (notation in subpackets.notationDataSubpackets) { + addSubpacket(notation) + } + for (recipient in subpackets.intendedRecipientFingerprintSubpackets) { + addSubpacket(recipient) + } + for (revocationKey in subpackets.revocationKeySubpackets) { + addSubpacket(revocationKey) + } + addSubpacket(subpackets.signatureTargetSubpacket) + addSubpacket(subpackets.featuresSubpacket) + addSubpacket(subpackets.keyFlagsSubpacket) + addSubpacket(subpackets.trustSubpacket) + addSubpacket(subpackets.preferredCompressionAlgorithmsSubpacket) + addSubpacket(subpackets.preferredSymmetricKeyAlgorithmsSubpacket) + addSubpacket(subpackets.preferredHashAlgorithmsSubpacket) + for (embedded in subpackets.embeddedSignatureSubpackets) { + addSubpacket(embedded) + } + addSubpacket(subpackets.signerUserIdSubpacket) + addSubpacket(subpackets.keyExpirationTimeSubpacket) + addSubpacket(subpackets.primaryUserIdSubpacket) + addSubpacket(subpackets.revocableSubpacket) + addSubpacket(subpackets.revocationReasonSubpacket) + for (residual in subpackets.residualSubpackets) { + addSubpacket(residual) + } + } + } + + @JvmStatic + private fun PGPSignatureSubpacketGenerator.addSubpacket(subpacket: org.bouncycastle.bcpg.SignatureSubpacket?) { + if (subpacket != null) { + this.addCustomSubpacket(subpacket) + } + } + + @JvmStatic + fun toVector(subpackets: SignatureSubpackets): PGPSignatureSubpacketVector { + return PGPSignatureSubpacketGenerator().let { + applyTo(subpackets, it) + it.generate() + } + } + + @JvmStatic + fun toVector(subpackets: RevocationSignatureSubpackets): PGPSignatureSubpacketVector { + return PGPSignatureSubpacketGenerator().let { + applyTo(subpackets as SignatureSubpackets, it) + it.generate() + } + } + } +} \ No newline at end of file From 384347ed1ae7ddf32cf501f728de15bd2f3d249d Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 23 Oct 2023 12:30:18 +0530 Subject: [PATCH 186/351] build: add `ktfmt` via Spotless --- build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle b/build.gradle index 57cc04c4..48791e21 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { plugins { id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: 'version.gradle' @@ -30,6 +31,7 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' apply plugin: 'kotlin' + apply plugin: 'com.diffplug.spotless' compileJava { options.release = 8 @@ -47,6 +49,12 @@ allprojects { toolVersion = '10.12.1' } + spotless { + kotlin { + ktfmt().dropboxStyle() + } + } + group 'org.pgpainless' description = "Simple to use OpenPGP API for Java based on Bouncycastle" version = shortVersion From 51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 23 Oct 2023 14:24:31 +0200 Subject: [PATCH 187/351] Apply new formatting from 'gradle spotlessApply' --- .../src/main/kotlin/openpgp/DateExtensions.kt | 29 +- .../src/main/kotlin/openpgp/LongExtensions.kt | 12 +- .../CachingBcPublicKeyDataDecryptorFactory.kt | 40 +- .../extensions/PGPKeyRingExtensions.kt | 48 +- .../extensions/PGPPublicKeyExtensions.kt | 38 +- .../extensions/PGPSecretKeyExtensions.kt | 16 +- .../extensions/PGPSecretKeyRingExtensions.kt | 48 +- .../extensions/PGPSignatureExtensions.kt | 97 +-- .../main/kotlin/org/pgpainless/PGPainless.kt | 74 +- .../org/pgpainless/algorithm/AEADAlgorithm.kt | 14 +- .../pgpainless/algorithm/AlgorithmSuite.kt | 26 +- .../pgpainless/algorithm/CertificationType.kt | 15 +- .../algorithm/CompressionAlgorithm.kt | 18 +- .../algorithm/DocumentSignatureType.kt | 10 +- .../pgpainless/algorithm/EncryptionPurpose.kt | 16 +- .../org/pgpainless/algorithm/Feature.kt | 57 +- .../org/pgpainless/algorithm/HashAlgorithm.kt | 50 +- .../org/pgpainless/algorithm/KeyFlag.kt | 57 +- .../org/pgpainless/algorithm/OpenPgpPacket.kt | 11 +- .../algorithm/PublicKeyAlgorithm.kt | 82 +-- .../pgpainless/algorithm/RevocationState.kt | 38 +- .../algorithm/RevocationStateType.kt | 14 +- .../algorithm/SignatureSubpacket.kt | 328 ++++----- .../org/pgpainless/algorithm/SignatureType.kt | 156 ++-- .../pgpainless/algorithm/StreamEncoding.kt | 43 +- .../algorithm/SymmetricKeyAlgorithm.kt | 103 +-- .../pgpainless/algorithm/Trustworthiness.kt | 52 +- .../negotiation/HashAlgorithmNegotiator.kt | 26 +- .../SymmetricKeyAlgorithmNegotiator.kt | 38 +- .../authentication/CertificateAuthenticity.kt | 31 +- .../authentication/CertificateAuthority.kt | 68 +- .../ConsumerOptions.kt | 172 ++--- .../CustomPublicKeyDataDecryptorFactory.kt | 14 +- .../DecryptionBuilder.kt | 12 +- .../DecryptionBuilderInterface.kt | 7 +- .../DecryptionStream.kt | 8 +- .../HardwareSecurity.kt | 49 +- .../IntegrityProtectedInputStream.kt | 16 +- .../MessageInspector.kt | 41 +- .../MessageMetadata.kt | 354 ++++----- .../MissingKeyPassphraseStrategy.kt | 13 +- .../MissingPublicKeyCallback.kt | 17 +- .../OpenPgpMessageInputStream.kt | 445 +++++++----- .../SignatureVerification.kt | 38 +- .../TeeBCPGInputStream.kt | 55 +- .../ClearsignedMessageUtil.kt | 40 +- .../InMemoryMultiPassStrategy.kt | 12 +- .../cleartext_signatures/MultiPassStrategy.kt | 37 +- .../WriteToFileMultiPassStrategy.kt | 16 +- .../syntax_check/InputSymbol.kt | 31 +- .../syntax_check/OpenPgpMessageSyntax.kt | 15 +- .../syntax_check/PDA.kt | 67 +- .../syntax_check/StackSymbol.kt | 14 +- .../syntax_check/State.kt | 6 +- .../syntax_check/Syntax.kt | 19 +- .../syntax_check/Transition.kt | 22 +- .../encryption_signing/BcHashContextSigner.kt | 34 +- .../BcPGPHashContextContentSignerBuilder.kt | 55 +- .../encryption_signing/CRLFGeneratorStream.kt | 18 +- .../encryption_signing/EncryptionBuilder.kt | 35 +- .../EncryptionBuilderInterface.kt | 8 +- .../encryption_signing/EncryptionOptions.kt | 176 +++-- .../encryption_signing/EncryptionResult.kt | 50 +- .../encryption_signing/EncryptionStream.kt | 96 ++- .../PGPHashContextContentSignerBuilder.kt | 24 +- .../encryption_signing/ProducerOptions.kt | 118 ++- .../SignatureGenerationStream.kt | 21 +- .../encryption_signing/SigningOptions.kt | 342 +++++---- .../implementation/BcImplementationFactory.kt | 112 +-- .../implementation/ImplementationFactory.kt | 78 +- .../JceImplementationFactory.kt | 120 +-- .../org/pgpainless/key/OpenPgpFingerprint.kt | 96 +-- .../pgpainless/key/OpenPgpV4Fingerprint.kt | 31 +- .../pgpainless/key/OpenPgpV5Fingerprint.kt | 22 +- .../pgpainless/key/OpenPgpV6Fingerprint.kt | 22 +- .../org/pgpainless/key/SubkeyIdentifier.kt | 40 +- .../org/pgpainless/key/_64DigitFingerprint.kt | 54 +- .../key/certification/CertifyCertificate.kt | 109 +-- .../key/collection/PGPKeyRingCollection.kt | 54 +- .../key/generation/KeyRingBuilder.kt | 141 ++-- .../key/generation/KeyRingBuilderInterface.kt | 13 +- .../key/generation/KeyRingTemplates.kt | 229 +++--- .../org/pgpainless/key/generation/KeySpec.kt | 12 +- .../key/generation/KeySpecBuilder.kt | 54 +- .../key/generation/KeySpecBuilderInterface.kt | 12 +- .../key/generation/type/KeyLength.kt | 2 +- .../pgpainless/key/generation/type/KeyType.kt | 43 +- .../key/generation/type/ecc/EllipticCurve.kt | 23 +- .../key/generation/type/ecc/ecdh/ECDH.kt | 5 +- .../key/generation/type/ecc/ecdsa/ECDSA.kt | 5 +- .../key/generation/type/eddsa/EdDSA.kt | 5 +- .../key/generation/type/eddsa/EdDSACurve.kt | 6 +- .../key/generation/type/elgamal/ElGamal.kt | 5 +- .../generation/type/elgamal/ElGamalLength.kt | 79 +- .../pgpainless/key/generation/type/rsa/RSA.kt | 13 +- .../key/generation/type/rsa/RsaLength.kt | 2 +- .../pgpainless/key/generation/type/xdh/XDH.kt | 7 +- .../key/generation/type/xdh/XDHSpec.kt | 7 +- .../org/pgpainless/key/info/KeyAccessor.kt | 77 +- .../kotlin/org/pgpainless/key/info/KeyInfo.kt | 66 +- .../org/pgpainless/key/info/KeyRingInfo.kt | 686 +++++++++--------- .../secretkeyring/SecretKeyRingEditor.kt | 632 ++++++++++------ .../SecretKeyRingEditorInterface.kt | 442 +++++------ .../pgpainless/key/parsing/KeyRingReader.kt | 172 ++--- .../protection/BaseSecretKeyRingProtector.kt | 32 +- .../CachingSecretKeyRingProtector.kt | 89 ++- .../protection/KeyRingProtectionSettings.kt | 37 +- .../PasswordBasedSecretKeyRingProtector.kt | 58 +- .../key/protection/SecretKeyRingProtector.kt | 86 ++- .../key/protection/UnlockSecretKey.kt | 34 +- .../protection/UnprotectedKeysProtector.kt | 5 +- .../key/protection/fixes/S2KUsageFix.kt | 40 +- .../MapBasedPassphraseProvider.kt | 7 +- .../SecretKeyPassphraseProvider.kt | 19 +- .../SolitaryPassphraseProvider.kt | 6 +- .../org/pgpainless/key/util/KeyIdUtil.kt | 13 +- .../org/pgpainless/key/util/KeyRingUtils.kt | 256 ++++--- .../util/PublicKeyParameterValidationUtil.kt | 110 ++- .../key/util/RevocationAttributes.kt | 99 ++- .../kotlin/org/pgpainless/key/util/UserId.kt | 125 ++-- .../kotlin/org/pgpainless/policy/Policy.kt | 314 ++++---- .../provider/BouncyCastleProviderFactory.kt | 4 +- .../pgpainless/provider/ProviderFactory.kt | 17 +- .../pgpainless/signature/SignatureUtils.kt | 151 ++-- .../consumer/CertificateValidator.kt | 223 +++--- .../consumer/OnePassSignatureCheck.kt | 15 +- .../signature/consumer/SignatureCheck.kt | 18 +- .../SignatureCreationDateComparator.kt | 18 +- .../signature/consumer/SignaturePicker.kt | 273 ++++--- .../consumer/SignatureValidityComparator.kt | 15 +- .../subpackets/BaseSignatureSubpackets.kt | 63 +- .../subpackets/CertificationSubpackets.kt | 3 +- .../RevocationSignatureSubpackets.kt | 17 +- .../subpackets/SelfSignatureSubpackets.kt | 65 +- .../subpackets/SignatureSubpacketCallback.kt | 2 +- .../subpackets/SignatureSubpackets.kt | 405 +++++++---- .../subpackets/SignatureSubpacketsHelper.kt | 202 ++++-- .../subpackets/SignatureSubpacketsUtil.kt | 322 ++++---- .../kotlin/org/pgpainless/util/ArmorUtils.kt | 272 +++---- .../util/ArmoredInputStreamFactory.kt | 23 +- .../util/ArmoredOutputStreamFactory.kt | 69 +- .../kotlin/org/pgpainless/util/DateUtil.kt | 17 +- .../kotlin/org/pgpainless/util/MultiMap.kt | 59 +- .../org/pgpainless/util/NotationRegistry.kt | 16 +- .../kotlin/org/pgpainless/util/Passphrase.kt | 63 +- .../kotlin/org/pgpainless/util/SessionKey.kt | 13 +- .../util/selection/userid/SelectUserId.kt | 81 +-- 147 files changed, 6186 insertions(+), 5198 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt index a1c80710..2763cb55 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/DateExtensions.kt @@ -11,40 +11,38 @@ import java.util.* /** * Return a new date which represents this date plus the given amount of seconds added. * - * Since '0' is a special date value in the OpenPGP specification - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no expiration for + * expiration dates), this method will return 'null' if seconds is 0. * * @param date date * @param seconds number of seconds to be added * @return date plus seconds or null if seconds is '0' */ fun Date.plusSeconds(seconds: Long): Date? { - require(Long.MAX_VALUE - time > seconds) { "Adding $seconds seconds to this date would cause time to overflow." } - return if (seconds == 0L) null - else Date(this.time + 1000 * seconds) + require(Long.MAX_VALUE - time > seconds) { + "Adding $seconds seconds to this date would cause time to overflow." + } + return if (seconds == 0L) null else Date(this.time + 1000 * seconds) } val Date.asSeconds: Long get() = time / 1000 fun Date.secondsTill(later: Date): Long { - require(this <= later) { - "Timestamp MUST be before the later timestamp." - } + require(this <= later) { "Timestamp MUST be before the later timestamp." } return later.asSeconds - this.asSeconds } -/** - * Return a new [Date] instance with this instance's time floored down to seconds precision. - */ +/** Return a new [Date] instance with this instance's time floored down to seconds precision. */ fun Date.toSecondsPrecision(): Date { return Date(asSeconds * 1000) } internal val parser: SimpleDateFormat - // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every invocation. - get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss z") - .apply { timeZone = TimeZone.getTimeZone("UTC") } + // Java's SimpleDateFormat is not thread-safe, therefore we return a new instance on every + // invocation. + get() = + SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").apply { timeZone = TimeZone.getTimeZone("UTC") } /** * Format a date as UTC timestamp. @@ -55,12 +53,13 @@ fun Date.formatUTC(): String = parser.format(this) /** * Parse a UTC timestamp into a date. + * * @return date */ fun String.parseUTC(): Date { return try { parser.parse(this) - } catch (e : ParseException) { + } catch (e: ParseException) { throw IllegalArgumentException("Malformed UTC timestamp: $this", e) } } diff --git a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt index 13f94943..c6c318b3 100644 --- a/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt +++ b/pgpainless-core/src/main/kotlin/openpgp/LongExtensions.kt @@ -4,22 +4,18 @@ package openpgp -/** - * Format this Long as an OpenPGP key-ID (16 digit uppercase hex number). - */ +/** Format this Long as an OpenPGP key-ID (16 digit uppercase hex number). */ fun Long.openPgpKeyId(): String { return String.format("%016X", this).uppercase() } -/** - * Parse a Long form a 16 digit hex encoded OpenPgp key-ID. - */ +/** Parse a Long form a 16 digit hex encoded OpenPgp key-ID. */ fun Long.Companion.fromOpenPgpKeyId(hexKeyId: String): Long { require("^[0-9A-Fa-f]{16}$".toRegex().matches(hexKeyId)) { "Provided long key-id does not match expected format. " + - "A long key-id consists of 16 hexadecimal characters." + "A long key-id consists of 16 hexadecimal characters." } // Calling toLong() only fails with a NumberFormatException. // Therefore, we call toULong(16).toLong(), which seems to work. return hexKeyId.toULong(16).toLong() -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt index 60b860f2..3a6f5351 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt @@ -11,40 +11,46 @@ import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactor import org.pgpainless.key.SubkeyIdentifier /** - * Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys. - * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys. That + * way, if a message needs to be decrypted multiple times, expensive private key operations can be + * omitted. * - * This implementation changes the behavior or [recoverSessionData] to first return any - * cache hits. - * If no hit is found, the method call is delegated to the underlying [PublicKeyDataDecryptorFactory]. - * The result of that is then placed in the cache and returned. + * This implementation changes the behavior or [recoverSessionData] to first return any cache hits. + * If no hit is found, the method call is delegated to the underlying + * [PublicKeyDataDecryptorFactory]. The result of that is then placed in the cache and returned. */ class CachingBcPublicKeyDataDecryptorFactory( - privateKey: PGPPrivateKey, - override val subkeyIdentifier: SubkeyIdentifier + privateKey: PGPPrivateKey, + override val subkeyIdentifier: SubkeyIdentifier ) : BcPublicKeyDataDecryptorFactory(privateKey), CustomPublicKeyDataDecryptorFactory { private val cachedSessions: MutableMap = mutableMapOf() - override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray = - lookupSessionKeyData(secKeyData) ?: - costlyRecoverSessionData(keyAlgorithm, secKeyData) - .also { cacheSessionKeyData(secKeyData, it) } + override fun recoverSessionData( + keyAlgorithm: Int, + secKeyData: Array + ): ByteArray = + lookupSessionKeyData(secKeyData) + ?: costlyRecoverSessionData(keyAlgorithm, secKeyData).also { + cacheSessionKeyData(secKeyData, it) + } private fun lookupSessionKeyData(secKeyData: Array): ByteArray? = - cachedSessions[toKey(secKeyData)]?.clone() + cachedSessions[toKey(secKeyData)]?.clone() - private fun costlyRecoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray = - super.recoverSessionData(keyAlgorithm, secKeyData) + private fun costlyRecoverSessionData( + keyAlgorithm: Int, + secKeyData: Array + ): ByteArray = super.recoverSessionData(keyAlgorithm, secKeyData) private fun cacheSessionKeyData(secKeyData: Array, sessionKey: ByteArray) { cachedSessions[toKey(secKeyData)] = sessionKey.clone() } private fun toKey(secKeyData: Array): String = - Base64.toBase64String(secKeyData[0]) + Base64.toBase64String(secKeyData[0]) fun clear() { cachedSessions.clear() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt index afa38aa6..611fa591 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -13,12 +13,10 @@ import org.pgpainless.PGPainless import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier -/** - * Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. - */ +/** Return true, if this [PGPKeyRing] contains the subkey identified by the [SubkeyIdentifier]. */ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = - this.publicKey.keyID == subkeyIdentifier.primaryKeyId && - this.getPublicKey(subkeyIdentifier.subkeyId) != null + this.publicKey.keyID == subkeyIdentifier.primaryKeyId && + this.getPublicKey(subkeyIdentifier.subkeyId) != null /** * Return true, if the [PGPKeyRing] contains a public key with the given key-ID. @@ -26,8 +24,7 @@ fun PGPKeyRing.matches(subkeyIdentifier: SubkeyIdentifier): Boolean = * @param keyId keyId * @return true if key with the given key-ID is present, false otherwise */ -fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = - this.getPublicKey(keyId) != null +fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = this.getPublicKey(keyId) != null /** * Return true, if the [PGPKeyRing] contains a public key with the given fingerprint. @@ -36,7 +33,7 @@ fun PGPKeyRing.hasPublicKey(keyId: Long): Boolean = * @return true if key with the given fingerprint is present, false otherwise */ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = - this.getPublicKey(fingerprint) != null + this.getPublicKey(fingerprint) != null /** * Return the [PGPPublicKey] with the given [OpenPgpFingerprint] or null, if no such key is present. @@ -45,36 +42,33 @@ fun PGPKeyRing.hasPublicKey(fingerprint: OpenPgpFingerprint): Boolean = * @return public key */ fun PGPKeyRing.getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = - this.getPublicKey(fingerprint.bytes) + this.getPublicKey(fingerprint.bytes) fun PGPKeyRing.requirePublicKey(keyId: Long): PGPPublicKey = - getPublicKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + getPublicKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") fun PGPKeyRing.requirePublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey = - getPublicKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + getPublicKey(fingerprint) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with fingerprint $fingerprint.") /** - * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. - * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to - * identify the [PGPPublicKey] via its key-ID. + * Return the [PGPPublicKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If + * the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID + * subpacket to identify the [PGPPublicKey] via its key-ID. */ fun PGPKeyRing.getPublicKeyFor(signature: PGPSignature): PGPPublicKey? = - signature.fingerprint?.let { this.getPublicKey(it) } ?: - this.getPublicKey(signature.keyID) + signature.fingerprint?.let { this.getPublicKey(it) } ?: this.getPublicKey(signature.keyID) -/** - * Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. - */ +/** Return the [PGPPublicKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPKeyRing.getPublicKeyFor(onePassSignature: PGPOnePassSignature): PGPPublicKey? = - this.getPublicKey(onePassSignature.keyID) + this.getPublicKey(onePassSignature.keyID) -/** - * Return the [OpenPgpFingerprint] of this OpenPGP key. - */ +/** Return the [OpenPgpFingerprint] of this OpenPGP key. */ val PGPKeyRing.openPgpFingerprint: OpenPgpFingerprint get() = OpenPgpFingerprint.of(this) -/** - * Return this OpenPGP key as an ASCII armored String. - */ -fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) \ No newline at end of file +/** Return this OpenPGP key as an ASCII armored String. */ +fun PGPKeyRing.toAsciiArmor(): String = PGPainless.asciiArmor(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt index ad51c6f4..847f1cf1 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -15,8 +15,8 @@ import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.type.eddsa.EdDSACurve /** - * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and [PublicKeyAlgorithm.EDDSA], - * this method returns the name of the underlying elliptic curve. + * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and + * [PublicKeyAlgorithm.EDDSA], this method returns the name of the underlying elliptic curve. * * For other key types or unknown curves, this method throws an [IllegalArgumentException]. * @@ -24,27 +24,29 @@ import org.pgpainless.key.generation.type.eddsa.EdDSACurve */ fun PGPPublicKey.getCurveName(): String { PublicKeyAlgorithm.requireFromId(algorithm) - .let { - when (it) { - PublicKeyAlgorithm.ECDSA -> publicKeyPacket.key as ECDSAPublicBCPGKey - PublicKeyAlgorithm.ECDH -> publicKeyPacket.key as ECDHPublicBCPGKey - PublicKeyAlgorithm.EDDSA -> publicKeyPacket.key as EdDSAPublicBCPGKey - else -> throw IllegalArgumentException("No an elliptic curve public key ($it).") - } + .let { + when (it) { + PublicKeyAlgorithm.ECDSA -> publicKeyPacket.key as ECDSAPublicBCPGKey + PublicKeyAlgorithm.ECDH -> publicKeyPacket.key as ECDHPublicBCPGKey + PublicKeyAlgorithm.EDDSA -> publicKeyPacket.key as EdDSAPublicBCPGKey + else -> throw IllegalArgumentException("No an elliptic curve public key ($it).") } - .let { if (it.curveOID == GNUObjectIdentifiers.Ed25519) return EdDSACurve._Ed25519.curveName else it.curveOID} - .let { it to ECUtil.getCurveName(it) } - .let { if (it.second != null) return it.second else throw IllegalArgumentException("Unknown curve: ${it.first}") } + } + .let { + if (it.curveOID == GNUObjectIdentifiers.Ed25519) return EdDSACurve._Ed25519.curveName + else it.curveOID + } + .let { it to ECUtil.getCurveName(it) } + .let { + if (it.second != null) return it.second + else throw IllegalArgumentException("Unknown curve: ${it.first}") + } } -/** - * Return the [PublicKeyAlgorithm] of this key. - */ +/** Return the [PublicKeyAlgorithm] of this key. */ val PGPPublicKey.publicKeyAlgorithm: PublicKeyAlgorithm get() = PublicKeyAlgorithm.requireFromId(algorithm) -/** - * Return the [OpenPgpFingerprint] of this key. - */ +/** Return the [OpenPgpFingerprint] of this key. */ val PGPPublicKey.openPgpFingerprint: OpenPgpFingerprint get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt index 3d759d1a..6049742c 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -7,7 +7,6 @@ package org.bouncycastle.extensions import org.bouncycastle.bcpg.S2K import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey -import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.pgpainless.algorithm.PublicKeyAlgorithm @@ -28,7 +27,7 @@ import org.pgpainless.util.Passphrase */ @Throws(PGPException::class, KeyIntegrityException::class) fun PGPSecretKey.unlock(passphrase: Passphrase): PGPPrivateKey = - UnlockSecretKey.unlockSecretKey(this, passphrase) + UnlockSecretKey.unlockSecretKey(this, passphrase) /** * Unlock the secret key to get its [PGPPrivateKey]. @@ -39,8 +38,9 @@ fun PGPSecretKey.unlock(passphrase: Passphrase): PGPPrivateKey = */ @Throws(PGPException::class, KeyIntegrityException::class) @JvmOverloads -fun PGPSecretKey.unlock(protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()): PGPPrivateKey = - UnlockSecretKey.unlockSecretKey(this, protector) +fun PGPSecretKey.unlock( + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() +): PGPPrivateKey = UnlockSecretKey.unlockSecretKey(this, protector) /** * Unlock the secret key to get its [PGPPrivateKey]. @@ -74,14 +74,10 @@ fun PGPSecretKey?.isDecrypted(): Boolean = (this == null) || (s2KUsage == 0) */ fun PGPSecretKey?.hasDummyS2K(): Boolean = (this != null) && (s2K?.type == S2K.GNU_DUMMY_S2K) -/** - * Return the [PublicKeyAlgorithm] of this key. - */ +/** Return the [PublicKeyAlgorithm] of this key. */ val PGPSecretKey.publicKeyAlgorithm: PublicKeyAlgorithm get() = publicKey.publicKeyAlgorithm -/** - * Return the [OpenPgpFingerprint] of this key. - */ +/** Return the [OpenPgpFingerprint] of this key. */ val PGPSecretKey.openPgpFingerprint: OpenPgpFingerprint get() = OpenPgpFingerprint.of(this) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index d0529d51..2116c748 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -8,9 +8,7 @@ import openpgp.openPgpKeyId import org.bouncycastle.openpgp.* import org.pgpainless.key.OpenPgpFingerprint -/** - * OpenPGP certificate containing the public keys of this OpenPGP key. - */ +/** OpenPGP certificate containing the public keys of this OpenPGP key. */ val PGPSecretKeyRing.certificate: PGPPublicKeyRing get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) @@ -20,8 +18,7 @@ val PGPSecretKeyRing.certificate: PGPPublicKeyRing * @param keyId keyId of the secret key * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise */ -fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = - this.getSecretKey(keyId) != null +fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = this.getSecretKey(keyId) != null /** * Return true, if the [PGPSecretKeyRing] contains a [PGPSecretKey] with the given fingerprint. @@ -30,7 +27,7 @@ fun PGPSecretKeyRing.hasSecretKey(keyId: Long): Boolean = * @return true, if the [PGPSecretKeyRing] has a matching [PGPSecretKey], false otherwise */ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = - this.getSecretKey(fingerprint) != null + this.getSecretKey(fingerprint) != null /** * Return the [PGPSecretKey] with the given [OpenPgpFingerprint]. @@ -39,41 +36,44 @@ fun PGPSecretKeyRing.hasSecretKey(fingerprint: OpenPgpFingerprint): Boolean = * @return the secret key or null */ fun PGPSecretKeyRing.getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = - this.getSecretKey(fingerprint.bytes) + this.getSecretKey(fingerprint.bytes) /** * Return the [PGPSecretKey] with the given key-ID. * - * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given key-ID + * @throws NoSuchElementException if the OpenPGP key doesn't contain a secret key with the given + * key-ID */ fun PGPSecretKeyRing.requireSecretKey(keyId: Long): PGPSecretKey = - getSecretKey(keyId) ?: throw NoSuchElementException("OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") + getSecretKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with id ${keyId.openPgpKeyId()}.") /** * Return the [PGPSecretKey] with the given fingerprint. * - * @throws NoSuchElementException of the OpenPGP key doesn't contain a secret key with the given fingerprint + * @throws NoSuchElementException of the OpenPGP key doesn't contain a secret key with the given + * fingerprint */ fun PGPSecretKeyRing.requireSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey = - getSecretKey(fingerprint) ?: throw NoSuchElementException("OpenPGP key does not contain key with fingerprint $fingerprint.") + getSecretKey(fingerprint) + ?: throw NoSuchElementException( + "OpenPGP key does not contain key with fingerprint $fingerprint.") /** - * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. - * If the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID subpacket to - * identify the [PGPSecretKey] via its key-ID. + * Return the [PGPSecretKey] that matches the [OpenPgpFingerprint] of the given [PGPSignature]. If + * the [PGPSignature] does not carry an issuer-fingerprint subpacket, fall back to the issuer-keyID + * subpacket to identify the [PGPSecretKey] via its key-ID. */ fun PGPSecretKeyRing.getSecretKeyFor(signature: PGPSignature): PGPSecretKey? = - signature.fingerprint?.let { this.getSecretKey(it) } ?: - this.getSecretKey(signature.keyID) + signature.fingerprint?.let { this.getSecretKey(it) } ?: this.getSecretKey(signature.keyID) -/** - * Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. - */ +/** Return the [PGPSecretKey] that matches the key-ID of the given [PGPOnePassSignature] packet. */ fun PGPSecretKeyRing.getSecretKeyFor(onePassSignature: PGPOnePassSignature): PGPSecretKey? = - this.getSecretKey(onePassSignature.keyID) + this.getSecretKey(onePassSignature.keyID) fun PGPSecretKeyRing.getSecretKeyFor(pkesk: PGPPublicKeyEncryptedData): PGPSecretKey? = - when(pkesk.version) { - 3 -> this.getSecretKey(pkesk.keyID) - else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") - } \ No newline at end of file + when (pkesk.version) { + 3 -> this.getSecretKey(pkesk.keyID) + else -> throw NotImplementedError("Version 6 PKESKs are not yet supported.") + } diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 4fe97bc7..2be011bd 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -4,6 +4,7 @@ package org.bouncycastle.extensions +import java.util.* import openpgp.plusSeconds import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature @@ -12,84 +13,84 @@ import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -import java.util.* /** * Return the value of the KeyExpirationDate subpacket, or null, if the signature does not carry * such a subpacket. */ fun PGPSignature.getKeyExpirationDate(keyCreationDate: Date): Date? = - SignatureSubpacketsUtil.getKeyExpirationTime(this) - ?.let { keyCreationDate.plusSeconds(it.time) } + SignatureSubpacketsUtil.getKeyExpirationTime(this)?.let { keyCreationDate.plusSeconds(it.time) } /** - * Return the value of the signature ExpirationTime subpacket, or null, if the signature - * does not carry such a subpacket. + * Return the value of the signature ExpirationTime subpacket, or null, if the signature does not + * carry such a subpacket. */ val PGPSignature.signatureExpirationDate: Date? - get() = SignatureSubpacketsUtil.getSignatureExpirationTime(this) - ?.let { this.creationTime.plusSeconds(it.time) } + get() = + SignatureSubpacketsUtil.getSignatureExpirationTime(this)?.let { + this.creationTime.plusSeconds(it.time) + } -/** - * Return true, if the signature is expired at the given reference time. - */ +/** Return true, if the signature is expired at the given reference time. */ fun PGPSignature.isExpired(referenceTime: Date = Date()) = - signatureExpirationDate?.let { referenceTime >= it } ?: false + signatureExpirationDate?.let { referenceTime >= it } ?: false /** * Return the key-ID of the issuer, determined by examining the IssuerKeyId and IssuerFingerprint * subpackets of the signature. */ val PGPSignature.issuerKeyId: Long - get() = when (version) { - 2, 3 -> keyID - else -> { - SignatureSubpacketsUtil.getIssuerKeyIdAsLong(this) - ?.let { if (it != 0L) it else null } - ?: fingerprint?.keyId - ?: 0L + get() = + when (version) { + 2, + 3 -> keyID + else -> { + SignatureSubpacketsUtil.getIssuerKeyIdAsLong(this)?.let { + if (it != 0L) it else null + } + ?: fingerprint?.keyId ?: 0L + } } - } -/** - * Return true, if the signature was likely issued by a key with the given fingerprint. - */ +/** Return true, if the signature was likely issued by a key with the given fingerprint. */ fun PGPSignature.wasIssuedBy(fingerprint: OpenPgpFingerprint): Boolean = - this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) + this.fingerprint?.let { it.keyId == fingerprint.keyId } ?: (keyID == fingerprint.keyId) /** * Return true, if the signature was likely issued by a key with the given fingerprint. + * * @param fingerprint fingerprint bytes */ @Deprecated("Discouraged in favor of method taking an OpenPgpFingerprint.") fun PGPSignature.wasIssuedBy(fingerprint: ByteArray): Boolean = - try { - wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) - } catch (e : IllegalArgumentException) { - // Unknown fingerprint length / format - false - } - -fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = - wasIssuedBy(OpenPgpFingerprint.of(key)) - -/** - * Return true, if this signature is a hard revocation. - */ -val PGPSignature.isHardRevocation - get() = when (SignatureType.requireFromCode(signatureType)) { - SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION, SignatureType.CERTIFICATION_REVOCATION -> { - SignatureSubpacketsUtil.getRevocationReason(this) - ?.let { Reason.isHardRevocation(it.revocationReason) } - ?: true // no reason -> hard revocation - } - else -> false // Not a revocation + try { + wasIssuedBy(OpenPgpFingerprint.parseFromBinary(fingerprint)) + } catch (e: IllegalArgumentException) { + // Unknown fingerprint length / format + false } +fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFingerprint.of(key)) + +/** Return true, if this signature is a hard revocation. */ +val PGPSignature.isHardRevocation + get() = + when (SignatureType.requireFromCode(signatureType)) { + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION, + SignatureType.CERTIFICATION_REVOCATION -> { + SignatureSubpacketsUtil.getRevocationReason(this)?.let { + Reason.isHardRevocation(it.revocationReason) + } + ?: true // no reason -> hard revocation + } + else -> false // Not a revocation + } + fun PGPSignature?.toRevocationState() = - if (this == null) RevocationState.notRevoked() - else if (isHardRevocation) RevocationState.hardRevoked() - else RevocationState.softRevoked(creationTime) + if (this == null) RevocationState.notRevoked() + else if (isHardRevocation) RevocationState.hardRevoked() + else RevocationState.softRevoked(creationTime) val PGPSignature.fingerprint: OpenPgpFingerprint? - get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) \ No newline at end of file + get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt index 6aa9799e..d866ac93 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -4,6 +4,8 @@ package org.pgpainless +import java.io.OutputStream +import java.util.* import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRing @@ -19,8 +21,6 @@ import org.pgpainless.key.parsing.KeyRingReader import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.util.ArmorUtils -import java.io.OutputStream -import java.util.* class PGPainless private constructor() { @@ -28,25 +28,24 @@ class PGPainless private constructor() { /** * Generate a fresh OpenPGP key ring from predefined templates. + * * @return templates */ - @JvmStatic - fun generateKeyRing() = KeyRingTemplates() + @JvmStatic fun generateKeyRing() = KeyRingTemplates() /** * Build a custom OpenPGP key ring. * * @return builder */ - @JvmStatic - fun buildKeyRing() = KeyRingBuilder() + @JvmStatic fun buildKeyRing() = KeyRingBuilder() /** * Read an existing OpenPGP key ring. + * * @return builder */ - @JvmStatic - fun readKeyRing() = KeyRingReader() + @JvmStatic fun readKeyRing() = KeyRingReader() /** * Extract a public key certificate from a secret key. @@ -56,10 +55,11 @@ class PGPainless private constructor() { */ @JvmStatic fun extractCertificate(secretKey: PGPSecretKeyRing) = - KeyRingUtils.publicKeyRingFrom(secretKey) + KeyRingUtils.publicKeyRingFrom(secretKey) /** - * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key server) together. + * Merge two copies of the same certificate (e.g. an old copy, and one retrieved from a key + * server) together. * * @param originalCopy local, older copy of the cert * @param updatedCopy updated, newer copy of the cert @@ -67,31 +67,27 @@ class PGPainless private constructor() { * @throws PGPException in case of an error */ @JvmStatic - fun mergeCertificate(originalCopy: PGPPublicKeyRing, - updatedCopy: PGPPublicKeyRing) = - PGPPublicKeyRing.join(originalCopy, updatedCopy) + fun mergeCertificate(originalCopy: PGPPublicKeyRing, updatedCopy: PGPPublicKeyRing) = + PGPPublicKeyRing.join(originalCopy, updatedCopy) /** * Wrap a key or certificate in ASCII armor. * * @param key key or certificate * @return ascii armored string - * * @throws IOException in case of an error during the armoring process */ @JvmStatic fun asciiArmor(key: PGPKeyRing) = - if (key is PGPSecretKeyRing) - ArmorUtils.toAsciiArmoredString(key) - else - ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing) + if (key is PGPSecretKeyRing) ArmorUtils.toAsciiArmoredString(key) + else ArmorUtils.toAsciiArmoredString(key as PGPPublicKeyRing) /** - * Wrap a key of certificate in ASCII armor and write the result into the given [OutputStream]. + * Wrap a key of certificate in ASCII armor and write the result into the given + * [OutputStream]. * * @param key key or certificate * @param outputStream output stream - * * @throws IOException in case of an error during the armoring process */ @JvmStatic @@ -106,33 +102,34 @@ class PGPainless private constructor() { * * @param signature detached signature * @return ascii armored string - * * @throws IOException in case of an error during the armoring process */ @JvmStatic fun asciiArmor(signature: PGPSignature) = ArmorUtils.toAsciiArmoredString(signature) /** - * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using OpenPGP. + * Create an [EncryptionBuilder], which can be used to encrypt and/or sign data using + * OpenPGP. * * @return builder */ - @JvmStatic - fun encryptAndOrSign() = EncryptionBuilder() + @JvmStatic fun encryptAndOrSign() = EncryptionBuilder() /** - * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using OpenPGP. + * Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using + * OpenPGP. * * @return builder */ - @JvmStatic - fun decryptAndOrVerify() = DecryptionBuilder() + @JvmStatic fun decryptAndOrVerify() = DecryptionBuilder() /** - * Make changes to a secret key at the given reference time. - * This method can be used to change key expiration dates and passphrases, or add/revoke user-ids and subkeys. + * Make changes to a secret key at the given reference time. This method can be used to + * change key expiration dates and passphrases, or add/revoke user-ids and subkeys. + * *
otherDepth
. + * Return true, if the certified cert can introduce certificates with trust depth of + *
otherDepth
. * * @param otherDepth other certifications trust depth * @return true if the cert can introduce the other @@ -54,7 +54,8 @@ class Trustworthiness(amount: Int, depth: Int) { fun canIntroduce(otherDepth: Int) = depth > otherDepth /** - * Return true, if the certified cert can introduce certificates with the given
other
trust depth. + * Return true, if the certified cert can introduce certificates with the given
other
+ * trust depth. * * @param other other certificates trust depth * @return true if the cert can introduce the other @@ -66,33 +67,29 @@ class Trustworthiness(amount: Int, depth: Int) { const val MARGINALLY_CONVINCED = 60 // default value for marginally convinced const val NOT_TRUSTED = 0 // 0 is not trusted - @JvmStatic - private val validRange = 0..255 + @JvmStatic private val validRange = 0..255 /** * This means that we are fully convinced of the trustworthiness of the key. * * @return builder */ - @JvmStatic - fun fullyTrusted() = Builder(THRESHOLD_FULLY_CONVINCED) + @JvmStatic fun fullyTrusted() = Builder(THRESHOLD_FULLY_CONVINCED) /** - * This means that we are marginally (partially) convinced of the trustworthiness of the key. + * This means that we are marginally (partially) convinced of the trustworthiness of the + * key. * * @return builder */ - @JvmStatic - fun marginallyTrusted() = Builder(MARGINALLY_CONVINCED) + @JvmStatic fun marginallyTrusted() = Builder(MARGINALLY_CONVINCED) /** - * This means that we do not trust the key. - * Can be used to overwrite previous trust. + * This means that we do not trust the key. Can be used to overwrite previous trust. * * @return builder */ - @JvmStatic - fun untrusted() = Builder(NOT_TRUSTED) + @JvmStatic fun untrusted() = Builder(NOT_TRUSTED) @JvmStatic private fun capAmount(amount: Int): Int { @@ -114,29 +111,28 @@ class Trustworthiness(amount: Int, depth: Int) { class Builder(val amount: Int) { /** - * The key is a trusted introducer (depth 1). - * Certifications made by this key are considered trustworthy. + * The key is a trusted introducer (depth 1). Certifications made by this key are considered + * trustworthy. * * @return trust */ fun introducer() = Trustworthiness(amount, 1) /** - * The key is a meta introducer (depth 2). - * This key can introduce trusted introducers of depth 1. + * The key is a meta introducer (depth 2). This key can introduce trusted introducers of + * depth 1. * * @return trust */ fun metaIntroducer() = Trustworthiness(amount, 2) /** - * The key is a meta introducer of depth
n
. - * This key can introduce meta introducers of depth
n - 1
. + * The key is a meta introducer of depth
n
. This key can introduce meta + * introducers of depth
n - 1
. * * @param n depth * @return trust */ fun metaIntroducerOfDepth(d: Int) = Trustworthiness(amount, d) } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt index 98cbe522..31d6b118 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt @@ -26,8 +26,8 @@ interface HashAlgorithmNegotiator { companion object { /** - * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for non-revocation signatures - * based on the given [Policy]. + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for + * non-revocation signatures based on the given [Policy]. * * @param policy algorithm policy * @return negotiator @@ -38,8 +38,8 @@ interface HashAlgorithmNegotiator { } /** - * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for revocation signatures - * based on the given [Policy]. + * Return an instance that negotiates [HashAlgorithms][HashAlgorithm] used for revocation + * signatures based on the given [Policy]. * * @param policy algorithm policy * @return negotiator @@ -57,15 +57,17 @@ interface HashAlgorithmNegotiator { * @return negotiator */ @JvmStatic - fun negotiateByPolicy(hashAlgorithmPolicy: Policy.HashAlgorithmPolicy): HashAlgorithmNegotiator { - return object: HashAlgorithmNegotiator { - override fun negotiateHashAlgorithm(orderedPrefs: Set): HashAlgorithm { - return orderedPrefs.firstOrNull { - hashAlgorithmPolicy.isAcceptable(it) - } ?: hashAlgorithmPolicy.defaultHashAlgorithm() + fun negotiateByPolicy( + hashAlgorithmPolicy: Policy.HashAlgorithmPolicy + ): HashAlgorithmNegotiator { + return object : HashAlgorithmNegotiator { + override fun negotiateHashAlgorithm( + orderedPrefs: Set + ): HashAlgorithm { + return orderedPrefs.firstOrNull { hashAlgorithmPolicy.isAcceptable(it) } + ?: hashAlgorithmPolicy.defaultHashAlgorithm() } - } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt index 1a6dfe7f..d11f2c03 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/SymmetricKeyAlgorithmNegotiator.kt @@ -4,38 +4,41 @@ package org.pgpainless.algorithm.negotiation +import java.lang.IllegalArgumentException import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.policy.Policy -import java.lang.IllegalArgumentException interface SymmetricKeyAlgorithmNegotiator { /** - * Negotiate a symmetric encryption algorithm. - * If the override is non-null, it will be returned instead of performing an actual negotiation. - * Otherwise, the list of ordered sets containing the preferences of different recipient keys will be - * used to determine a suitable symmetric encryption algorithm. + * Negotiate a symmetric encryption algorithm. If the override is non-null, it will be returned + * instead of performing an actual negotiation. Otherwise, the list of ordered sets containing + * the preferences of different recipient keys will be used to determine a suitable 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 */ - fun negotiate(policy: Policy.SymmetricKeyAlgorithmPolicy, - override: SymmetricKeyAlgorithm?, - keyPreferences: List>): SymmetricKeyAlgorithm + fun negotiate( + policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List> + ): SymmetricKeyAlgorithm companion object { @JvmStatic fun byPopularity(): SymmetricKeyAlgorithmNegotiator { - return object: SymmetricKeyAlgorithmNegotiator { + return object : SymmetricKeyAlgorithmNegotiator { override fun negotiate( - policy: Policy.SymmetricKeyAlgorithmPolicy, - override: SymmetricKeyAlgorithm?, - keyPreferences: List>): - SymmetricKeyAlgorithm { + policy: Policy.SymmetricKeyAlgorithmPolicy, + override: SymmetricKeyAlgorithm?, + keyPreferences: List> + ): SymmetricKeyAlgorithm { if (override == SymmetricKeyAlgorithm.NULL) { - throw IllegalArgumentException("Algorithm override cannot be NULL (plaintext).") + throw IllegalArgumentException( + "Algorithm override cannot be NULL (plaintext).") } if (override != null) { @@ -53,7 +56,9 @@ interface SymmetricKeyAlgorithmNegotiator { // Pivot map and sort by popularity ascending // score to list(algo) - val byScore = supportWeight.toList() + val byScore = + supportWeight + .toList() .map { e -> e.second to e.first } .groupBy { e -> e.first } .map { e -> e.key to e.value.map { it.second }.toList() } @@ -70,8 +75,7 @@ interface SymmetricKeyAlgorithmNegotiator { return policy.defaultSymmetricKeyAlgorithm } - } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt index 2024c710..f3d60bf6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthenticity.kt @@ -6,19 +6,19 @@ package org.pgpainless.authentication import org.bouncycastle.openpgp.PGPPublicKeyRing -class CertificateAuthenticity(val userId: String, - val certificate: PGPPublicKeyRing, - val certificationChains: Map, - val targetAmount: Int) { +class CertificateAuthenticity( + val userId: String, + val certificate: PGPPublicKeyRing, + val certificationChains: Map, + val targetAmount: Int +) { val totalTrustAmount: Int get() = certificationChains.values.sum() - /** - * Return the degree of authentication of the binding in percent. - * 100% means full authentication. - * Values smaller than 100% mean partial authentication. + * Return the degree of authentication of the binding in percent. 100% means full + * authentication. Values smaller than 100% mean partial authentication. * * @return authenticity in percent */ @@ -42,16 +42,7 @@ class CertificateAuthenticity(val userId: String, * @param trustAmount actual trust amount of the chain * @param chainLinks links of the chain, starting at the trust-root, ending at the target. */ -class CertificationChain( - val trustAmount: Int, - val chainLinks: List) { +class CertificationChain(val trustAmount: Int, val chainLinks: List) {} -} - -/** - * A chain link contains a node in the trust chain. - */ -class ChainLink( - val certificate: PGPPublicKeyRing) { - -} \ No newline at end of file +/** A chain link contains a node in the trust chain. */ +class ChainLink(val certificate: PGPPublicKeyRing) {} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt index e510f48a..093c2325 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/authentication/CertificateAuthority.kt @@ -2,14 +2,14 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.authentication; +package org.pgpainless.authentication -import org.pgpainless.key.OpenPgpFingerprint import java.util.* +import org.pgpainless.key.OpenPgpFingerprint /** - * Interface for a CA that can authenticate trust-worthy certificates. - * Such a CA might be a fixed list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust. + * Interface for a CA that can authenticate trust-worthy certificates. Such a CA might be a fixed + * list of trustworthy certificates, or a dynamic implementation like the Web-of-Trust. * * @see PGPainless-WOT * @see OpenPGP Web of Trust @@ -17,52 +17,58 @@ import java.util.* interface CertificateAuthority { /** - * Determine the authenticity of the binding between the given fingerprint and the userId. - * In other words, determine, how much evidence can be gathered, that the certificate with the given - * fingerprint really belongs to the user with the given userId. + * Determine the authenticity of the binding between the given fingerprint and the userId. In + * other words, determine, how much evidence can be gathered, that the certificate with the + * given fingerprint really belongs to the user with the given userId. * * @param fingerprint fingerprint of the certificate * @param userId userId - * @param email if true, the userId will be treated as an email address and all user-IDs containing - * the email address will be matched. + * @param email if true, the userId will be treated as an email address and all user-IDs + * containing the email address will be matched. * @param referenceTime reference time at which the binding shall be evaluated - * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) * @return information about the authenticity of the binding */ - fun authenticateBinding(fingerprint: OpenPgpFingerprint, - userId: String, - email: Boolean, - referenceTime: Date, - targetAmount: Int): CertificateAuthenticity; + fun authenticateBinding( + fingerprint: OpenPgpFingerprint, + userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int + ): CertificateAuthenticity /** * Lookup certificates, which carry a trustworthy binding to the given userId. * * @param userId userId - * @param email if true, the user-ID will be treated as an email address and all user-IDs containing - * the email address will be matched. + * @param email if true, the user-ID will be treated as an email address and all user-IDs + * containing the email address will be matched. * @param referenceTime reference time at which the binding shall be evaluated - * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) * @return list of identified bindings */ - fun lookupByUserId(userId: String, - email: Boolean, - referenceTime: Date, - targetAmount: Int): List + fun lookupByUserId( + userId: String, + email: Boolean, + referenceTime: Date, + targetAmount: Int + ): List /** - * Identify trustworthy bindings for a certificate. - * The result is a list of authenticatable userIds on the certificate. + * Identify trustworthy bindings for a certificate. The result is a list of authenticatable + * userIds on the certificate. * * @param fingerprint fingerprint of the certificate * @param referenceTime reference time for trust calculations - * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) + * @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly + * authenticated, 60 = partially authenticated...) * @return list of identified bindings */ - fun identifyByFingerprint(fingerprint: OpenPgpFingerprint, - referenceTime: Date, - targetAmount: Int): List + fun identifyByFingerprint( + fingerprint: OpenPgpFingerprint, + referenceTime: Date, + targetAmount: Int + ): List } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index e0ec1fd5..dff33b2d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -4,6 +4,9 @@ package org.pgpainless.decryption_verification +import java.io.IOException +import java.io.InputStream +import java.util.* import org.bouncycastle.extensions.getPublicKeyFor import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory @@ -14,13 +17,8 @@ import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.signature.SignatureUtils import org.pgpainless.util.Passphrase import org.pgpainless.util.SessionKey -import java.io.IOException -import java.io.InputStream -import java.util.* -/** - * Options for decryption and signature verification. - */ +/** Options for decryption and signature verification. */ class ConsumerOptions { private var ignoreMDCErrors = false @@ -34,15 +32,16 @@ class ConsumerOptions { private var missingCertificateCallback: MissingPublicKeyCallback? = null private var sessionKey: SessionKey? = null - private val customDecryptorFactories = mutableMapOf() + private val customDecryptorFactories = + mutableMapOf() private val decryptionKeys = mutableMapOf() private val decryptionPassphrases = mutableSetOf() private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() /** - * Consider signatures on the message made before the given timestamp invalid. - * Null means no limitation. + * Consider signatures on the message made before the given timestamp invalid. Null means no + * limitation. * * @param timestamp timestamp * @return options @@ -54,8 +53,8 @@ class ConsumerOptions { fun getVerifyNotBefore() = verifyNotBefore /** - * Consider signatures on the message made after the given timestamp invalid. - * Null means no limitation. + * Consider signatures on the message made after the given timestamp invalid. Null means no + * limitation. * * @param timestamp timestamp * @return options @@ -82,26 +81,27 @@ class ConsumerOptions { * @param verificationCerts certificates for signature verification * @return options */ - fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = apply { - for (cert in verificationCerts) { - addVerificationCert(cert) + fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = + apply { + for (cert in verificationCerts) { + addVerificationCert(cert) + } } - } /** * Add some detached signatures from the given [InputStream] for verification. * * @param signatureInputStream input stream of detached signatures * @return options - * * @throws IOException in case of an IO error * @throws PGPException in case of an OpenPGP error */ @Throws(IOException::class, PGPException::class) - fun addVerificationOfDetachedSignatures(signatureInputStream: InputStream): ConsumerOptions = apply { - val signatures = SignatureUtils.readSignatures(signatureInputStream) - addVerificationOfDetachedSignatures(signatures) - } + fun addVerificationOfDetachedSignatures(signatureInputStream: InputStream): ConsumerOptions = + apply { + val signatures = SignatureUtils.readSignatures(signatureInputStream) + addVerificationOfDetachedSignatures(signatures) + } /** * Add some detached signatures for verification. @@ -109,7 +109,9 @@ class ConsumerOptions { * @param detachedSignatures detached signatures * @return options */ - fun addVerificationOfDetachedSignatures(detachedSignatures: List): ConsumerOptions = apply { + fun addVerificationOfDetachedSignatures( + detachedSignatures: List + ): ConsumerOptions = apply { for (signature in detachedSignatures) { addVerificationOfDetachedSignature(signature) } @@ -121,14 +123,16 @@ class ConsumerOptions { * @param detachedSignature detached signature * @return options */ - fun addVerificationOfDetachedSignature(detachedSignature: PGPSignature): ConsumerOptions = apply { - detachedSignatures.add(detachedSignature) - } + fun addVerificationOfDetachedSignature(detachedSignature: PGPSignature): ConsumerOptions = + apply { + detachedSignatures.add(detachedSignature) + } fun getDetachedSignatures() = detachedSignatures.toList() /** - * Set a callback that's used when a certificate (public key) is missing for signature verification. + * Set a callback that's used when a certificate (public key) is missing for signature + * verification. * * @param callback callback * @return options @@ -152,18 +156,18 @@ class ConsumerOptions { fun getSessionKey() = sessionKey /** - * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] - * is used to decrypt it when needed. + * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] is + * used to decrypt it when needed. * * @param key key * @param keyRingProtector protector for the secret key * @return options */ @JvmOverloads - fun addDecryptionKey(key: PGPSecretKeyRing, - protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { - decryptionKeys[key] = protector - } + fun addDecryptionKey( + key: PGPSecretKeyRing, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() + ) = apply { decryptionKeys[key] = protector } /** * Add the keys in the provided key collection for message decryption. @@ -173,18 +177,21 @@ class ConsumerOptions { * @return options */ @JvmOverloads - fun addDecryptionKeys(keys: PGPSecretKeyRingCollection, - protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { + fun addDecryptionKeys( + keys: PGPSecretKeyRingCollection, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys() + ) = apply { for (key in keys) { addDecryptionKey(key, protector) } } /** - * Add a passphrase for message decryption. - * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. + * Add a passphrase for message decryption. This passphrase will be used to try to decrypt + * messages which were symmetrically encrypted for a passphrase. * - * See [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * See + * [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) * * @param passphrase passphrase * @return options @@ -195,8 +202,8 @@ class ConsumerOptions { /** * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using - * hardware-backed secret keys. - * (See e.g. [org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory]). + * hardware-backed secret keys. (See e.g. + * [org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory]). * * @param factory decryptor factory * @return options @@ -206,9 +213,8 @@ class ConsumerOptions { } /** - * Return the custom [PublicKeyDataDecryptorFactory] that were - * set by the user. - * These factories can be used to decrypt session keys using a custom logic. + * Return the custom [PublicKeyDataDecryptorFactory] that were set by the user. These factories + * can be used to decrypt session keys using a custom logic. * * @return custom decryptor factories */ @@ -236,8 +242,8 @@ class ConsumerOptions { fun getCertificateSource() = certificates /** - * Return the callback that gets called when a certificate for signature verification is missing. - * This method might return `null` if the users hasn't set a callback. + * Return the callback that gets called when a certificate for signature verification is + * missing. This method might return `null` if the users hasn't set a callback. * * @return missing public key callback */ @@ -255,45 +261,46 @@ class ConsumerOptions { /** * By default, PGPainless will require encrypted messages to make use of SEIP data packets. - * Those are Symmetrically Encrypted Integrity Protected Data packets. - * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection - * code of the SEIP packet fails. + * Those are Symmetrically Encrypted Integrity Protected Data packets. Symmetrically Encrypted + * Data Packets without integrity protection are rejected by default. Furthermore, PGPainless + * will throw an exception if verification of the MDC error detection code of the SEIP packet + * fails. * * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an * attack or data corruption. * * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data - * without integrity protection. - * If the flag
ignoreMDCErrors
is set to true, PGPainless will + * without integrity protection. If the flag
ignoreMDCErrors
is set to true, + * PGPainless will + * * not throw exceptions for SEIP packets with tampered ciphertext + * * not throw exceptions for SEIP packets with tampered MDC + * * not throw exceptions for MDCs with bad CTB + * * not throw exceptions for MDCs with bad length * - * * not throw exceptions for SEIP packets with tampered ciphertext - * * not throw exceptions for SEIP packets with tampered MDC - * * not throw exceptions for MDCs with bad CTB - * * not throw exceptions for MDCs with bad length + * It will however still throw an exception if it encounters a SEIP packet with missing or + * truncated MDC * - * - * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC - * - * See [Sym. Encrypted Integrity Protected Data Packet](https://datatracker.ietf.org/doc/html/rfc4880.section-5.13) + * See + * [Sym. Encrypted Integrity Protected Data Packet](https://datatracker.ietf.org/doc/html/rfc4880.section-5.13) * * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. * @return options */ @Deprecated("Ignoring non-integrity-protected packets is discouraged.") - fun setIgnoreMDCErrors(ignoreMDCErrors: Boolean): ConsumerOptions = apply { this.ignoreMDCErrors = ignoreMDCErrors } + fun setIgnoreMDCErrors(ignoreMDCErrors: Boolean): ConsumerOptions = apply { + this.ignoreMDCErrors = ignoreMDCErrors + } fun isIgnoreMDCErrors() = ignoreMDCErrors /** - * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. - * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. + * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. This + * workaround might come in handy if PGPainless accidentally mistakes the data for binary + * OpenPGP data. * * @return options */ - fun forceNonOpenPgpData(): ConsumerOptions = apply { - this.forceNonOpenPgpData = true - } + fun forceNonOpenPgpData(): ConsumerOptions = apply { this.forceNonOpenPgpData = true } /** * Return true, if the ciphertext should be handled as binary non-OpenPGP data. @@ -303,15 +310,15 @@ class ConsumerOptions { fun isForceNonOpenPgpData() = forceNonOpenPgpData /** - * Specify the [MissingKeyPassphraseStrategy]. - * This strategy defines, how missing passphrases for unlocking secret keys are handled. - * In interactive mode ([MissingKeyPassphraseStrategy.INTERACTIVE]) PGPainless will try to obtain missing + * Specify the [MissingKeyPassphraseStrategy]. This strategy defines, how missing passphrases + * for unlocking secret keys are handled. In interactive mode + * ([MissingKeyPassphraseStrategy.INTERACTIVE]) PGPainless will try to obtain missing * passphrases for secret keys via the [SecretKeyRingProtector] * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider] callback. * - * In non-interactice mode ([MissingKeyPassphraseStrategy.THROW_EXCEPTION]), PGPainless will instead - * throw a [org.pgpainless.exception.MissingPassphraseException] containing the ids of all keys for which - * there are missing passphrases. + * In non-interactice mode ([MissingKeyPassphraseStrategy.THROW_EXCEPTION]), PGPainless will + * instead throw a [org.pgpainless.exception.MissingPassphraseException] containing the ids of + * all keys for which there are missing passphrases. * * @param strategy strategy * @return options @@ -331,8 +338,8 @@ class ConsumerOptions { } /** - * Set a custom multi-pass strategy for processing cleartext-signed messages. - * Uses [InMemoryMultiPassStrategy] by default. + * Set a custom multi-pass strategy for processing cleartext-signed messages. Uses + * [InMemoryMultiPassStrategy] by default. * * @param multiPassStrategy multi-pass caching strategy * @return builder @@ -343,8 +350,7 @@ class ConsumerOptions { } /** - * Return the currently configured [MultiPassStrategy]. - * Defaults to [InMemoryMultiPassStrategy]. + * Return the currently configured [MultiPassStrategy]. Defaults to [InMemoryMultiPassStrategy]. * * @return multi-pass strategy */ @@ -353,8 +359,8 @@ class ConsumerOptions { } /** - * Source for OpenPGP certificates. - * When verifying signatures on a message, this object holds available signer certificates. + * Source for OpenPGP certificates. When verifying signatures on a message, this object holds + * available signer certificates. */ class CertificateSource { private val explicitCertificates: MutableSet = mutableSetOf() @@ -370,6 +376,7 @@ class ConsumerOptions { /** * Return the set of explicitly set verification certificates. + * * @return explicitly set verification certs */ fun getExplicitCertificates(): Set { @@ -377,9 +384,9 @@ class ConsumerOptions { } /** - * Return a certificate which contains a subkey with the given keyId. - * This method first checks all explicitly set verification certs and if no cert is found it consults - * the certificate stores. + * Return a certificate which contains a subkey with the given keyId. This method first + * checks all explicitly set verification certs and if no cert is found it consults the + * certificate stores. * * @param keyId key id * @return certificate @@ -389,13 +396,10 @@ class ConsumerOptions { } fun getCertificate(signature: PGPSignature): PGPPublicKeyRing? = - explicitCertificates.firstOrNull { - it.getPublicKeyFor(signature) != null - } + explicitCertificates.firstOrNull { it.getPublicKeyFor(signature) != null } } companion object { - @JvmStatic - fun get() = ConsumerOptions() + @JvmStatic fun get() = ConsumerOptions() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt index b7f57da3..4a0dbeba 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt @@ -8,19 +8,19 @@ import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.pgpainless.key.SubkeyIdentifier /** - * Custom [PublicKeyDataDecryptorFactory] which can enable customized implementations of message decryption - * using public keys. - * This class can for example be used to implement message encryption using hardware tokens like smartcards or - * TPMs. + * Custom [PublicKeyDataDecryptorFactory] which can enable customized implementations of message + * decryption using public keys. This class can for example be used to implement message encryption + * using hardware tokens like smartcards or TPMs. + * * @see [ConsumerOptions.addCustomDecryptorFactory] */ interface CustomPublicKeyDataDecryptorFactory : PublicKeyDataDecryptorFactory { /** - * Identifier for the subkey for which this particular [CustomPublicKeyDataDecryptorFactory] - * is intended. + * Identifier for the subkey for which this particular [CustomPublicKeyDataDecryptorFactory] is + * intended. * * @return subkey identifier */ val subkeyIdentifier: SubkeyIdentifier -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt index 4934f5de..d1d4f8b2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilder.kt @@ -7,20 +7,20 @@ package org.pgpainless.decryption_verification import java.io.InputStream /** - * Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) - * and combines it with a configured [ConsumerOptions] object to form a [DecryptionStream] which - * can be used to decrypt an OpenPGP message or verify signatures. + * Builder class that takes an [InputStream] of ciphertext (or plaintext signed data) and combines + * it with a configured [ConsumerOptions] object to form a [DecryptionStream] which can be used to + * decrypt an OpenPGP message or verify signatures. */ -class DecryptionBuilder: DecryptionBuilderInterface { +class DecryptionBuilder : DecryptionBuilderInterface { override fun onInputStream(inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { return DecryptWithImpl(inputStream) } - class DecryptWithImpl(val inputStream: InputStream): DecryptionBuilderInterface.DecryptWith { + class DecryptWithImpl(val inputStream: InputStream) : DecryptionBuilderInterface.DecryptWith { override fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream { return OpenPgpMessageInputStream.create(inputStream, consumerOptions) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt index c15f301e..18fd4179 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionBuilderInterface.kt @@ -4,14 +4,15 @@ package org.pgpainless.decryption_verification -import org.bouncycastle.openpgp.PGPException import java.io.IOException import java.io.InputStream +import org.bouncycastle.openpgp.PGPException interface DecryptionBuilderInterface { /** - * Create a [DecryptionStream] on an [InputStream] which contains the encrypted and/or signed data. + * Create a [DecryptionStream] on an [InputStream] which contains the encrypted and/or signed + * data. * * @param inputStream encrypted and/or signed data. * @return api handle @@ -31,4 +32,4 @@ interface DecryptionBuilderInterface { @Throws(PGPException::class, IOException::class) fun withOptions(consumerOptions: ConsumerOptions): DecryptionStream } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt index b9499784..86bd490a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/DecryptionStream.kt @@ -9,13 +9,13 @@ import java.io.InputStream /** * Abstract definition of an [InputStream] which can be used to decrypt / verify OpenPGP messages. */ -abstract class DecryptionStream: InputStream() { +abstract class DecryptionStream : InputStream() { /** - * Return [MessageMetadata] about the decrypted / verified message. - * The [DecryptionStream] MUST be closed via [close] before the metadata object can be accessed. + * Return [MessageMetadata] about the decrypted / verified message. The [DecryptionStream] MUST + * be closed via [close] before the metadata object can be accessed. * * @return message metadata */ abstract val metadata: MessageMetadata -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt index 7d0d243a..1974e290 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification +import kotlin.jvm.Throws import org.bouncycastle.bcpg.AEADEncDataPacket import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket import org.bouncycastle.openpgp.PGPException @@ -12,25 +13,22 @@ import org.bouncycastle.openpgp.operator.PGPDataDecryptor import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory import org.pgpainless.key.SubkeyIdentifier -import kotlin.jvm.Throws -/** - * Enable integration of hardware-backed OpenPGP keys. - */ +/** Enable integration of hardware-backed OpenPGP keys. */ class HardwareSecurity { interface DecryptionCallback { /** - * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with - * hardware security modules such as smartcards or TPMs. + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for + * dealing with hardware security modules such as smartcards or TPMs. * - * If decryption fails for some reason, a subclass of the [HardwareSecurityException] is thrown. + * If decryption fails for some reason, a subclass of the [HardwareSecurityException] is + * thrown. * * @param keyId id of the key * @param keyAlgorithm algorithm * @param sessionKeyData encrypted session key - * * @return decrypted session key * @throws HardwareSecurityException exception */ @@ -39,38 +37,51 @@ class HardwareSecurity { } /** - * Implementation of [PublicKeyDataDecryptorFactory] which delegates decryption of encrypted session keys - * to a [DecryptionCallback]. - * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + * Implementation of [PublicKeyDataDecryptorFactory] which delegates decryption of encrypted + * session keys to a [DecryptionCallback]. Users can provide such a callback to delegate + * decryption of messages to hardware security SDKs. */ class HardwareDataDecryptorFactory( - override val subkeyIdentifier: SubkeyIdentifier, - private val callback: DecryptionCallback, + override val subkeyIdentifier: SubkeyIdentifier, + private val callback: DecryptionCallback, ) : CustomPublicKeyDataDecryptorFactory { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private val factory: PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(null) - override fun createDataDecryptor(withIntegrityPacket: Boolean, encAlgorithm: Int, key: ByteArray?): PGPDataDecryptor { + override fun createDataDecryptor( + withIntegrityPacket: Boolean, + encAlgorithm: Int, + key: ByteArray? + ): PGPDataDecryptor { return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key) } - override fun createDataDecryptor(aeadEncDataPacket: AEADEncDataPacket?, sessionKey: PGPSessionKey?): PGPDataDecryptor { + override fun createDataDecryptor( + aeadEncDataPacket: AEADEncDataPacket?, + sessionKey: PGPSessionKey? + ): PGPDataDecryptor { return factory.createDataDecryptor(aeadEncDataPacket, sessionKey) } - override fun createDataDecryptor(seipd: SymmetricEncIntegrityPacket?, sessionKey: PGPSessionKey?): PGPDataDecryptor { + override fun createDataDecryptor( + seipd: SymmetricEncIntegrityPacket?, + sessionKey: PGPSessionKey? + ): PGPDataDecryptor { return factory.createDataDecryptor(seipd, sessionKey) } - override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray { + override fun recoverSessionData( + keyAlgorithm: Int, + secKeyData: Array + ): ByteArray { return try { callback.decryptSessionKey(subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0]) - } catch (e : HardwareSecurityException) { + } catch (e: HardwareSecurityException) { throw PGPException("Hardware-backed decryption failed.", e) } } } class HardwareSecurityException : Exception() -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt index a1e095f8..4618882c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.kt @@ -4,23 +4,25 @@ package org.pgpainless.decryption_verification +import java.io.IOException +import java.io.InputStream import org.bouncycastle.openpgp.PGPEncryptedData import org.bouncycastle.openpgp.PGPException import org.pgpainless.exception.ModificationDetectionException import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.IOException -import java.io.InputStream class IntegrityProtectedInputStream( - private val inputStream: InputStream, - private val encryptedData: PGPEncryptedData, - private val options: ConsumerOptions + private val inputStream: InputStream, + private val encryptedData: PGPEncryptedData, + private val options: ConsumerOptions ) : InputStream() { private var closed: Boolean = false override fun read() = inputStream.read() + override fun read(b: ByteArray, off: Int, len: Int) = inputStream.read(b, off, len) + override fun close() { if (closed) return @@ -29,7 +31,7 @@ class IntegrityProtectedInputStream( try { if (!encryptedData.verify()) throw ModificationDetectionException() LOGGER.debug("Integrity Protection check passed.") - } catch (e : PGPException) { + } catch (e: PGPException) { throw IOException("Data appears to not be integrity protected.", e) } } @@ -39,4 +41,4 @@ class IntegrityProtectedInputStream( @JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(IntegrityProtectedInputStream::class.java) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt index 64b2a5f3..acfcba51 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageInspector.kt @@ -4,14 +4,15 @@ package org.pgpainless.decryption_verification +import java.io.IOException +import java.io.InputStream import org.bouncycastle.openpgp.* import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmorUtils -import java.io.IOException -import java.io.InputStream /** - * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase protected. + * Inspect an OpenPGP message to determine IDs of its encryption keys or whether it is passphrase + * protected. */ class MessageInspector { @@ -23,9 +24,10 @@ class MessageInspector { * @param isSignedOnly true, if the message is not encrypted, but signed using OnePassSignatures */ data class EncryptionInfo( - val keyIds: List, - val isPassphraseEncrypted: Boolean, - val isSignedOnly: Boolean) { + val keyIds: List, + val isPassphraseEncrypted: Boolean, + val isSignedOnly: Boolean + ) { val isEncrypted: Boolean get() = isPassphraseEncrypted || keyIds.isNotEmpty() @@ -34,25 +36,26 @@ class MessageInspector { companion object { /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. + * Parses parts of the provided OpenPGP message in order to determine which keys were used + * to encrypt it. * * @param message OpenPGP message * @return encryption info - * * @throws PGPException in case the message is broken * @throws IOException in case of an IO error */ @JvmStatic @Throws(PGPException::class, IOException::class) - fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = determineEncryptionInfoForMessage(message.byteInputStream()) + fun determineEncryptionInfoForMessage(message: String): EncryptionInfo = + determineEncryptionInfoForMessage(message.byteInputStream()) /** - * Parses parts of the provided OpenPGP message in order to determine which keys were used to encrypt it. - * Note: This method does not rewind the passed in Stream, so you might need to take care of that yourselves. + * Parses parts of the provided OpenPGP message in order to determine which keys were used + * to encrypt it. Note: This method does not rewind the passed in Stream, so you might need + * to take care of that yourselves. * * @param inputStream openpgp message * @return encryption information - * * @throws IOException in case of an IO error * @throws PGPException if the message is broken */ @@ -70,13 +73,12 @@ class MessageInspector { var n: Any? while (objectFactory.nextObject().also { n = it } != null) { when (val next = n!!) { - is PGPOnePassSignatureList -> { if (!next.isEmpty) { - return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = true) + return EncryptionInfo( + listOf(), isPassphraseEncrypted = false, isSignedOnly = true) } } - is PGPEncryptedDataList -> { var isPassphraseEncrypted = false val keyIds = mutableListOf() @@ -90,13 +92,12 @@ class MessageInspector { // Data is encrypted, we cannot go deeper return EncryptionInfo(keyIds, isPassphraseEncrypted, false) } - is PGPCompressedData -> { - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - PGPUtil.getDecoderStream(next.dataStream)) + objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(PGPUtil.getDecoderStream(next.dataStream)) continue } - is PGPLiteralData -> { break } @@ -105,4 +106,4 @@ class MessageInspector { return EncryptionInfo(listOf(), isPassphraseEncrypted = false, isSignedOnly = false) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt index 3eb49e5d..ecdeadf0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -4,6 +4,8 @@ package org.pgpainless.decryption_verification +import java.util.* +import javax.annotation.Nonnull import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPLiteralData @@ -15,116 +17,118 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.SessionKey -import java.util.* -import javax.annotation.Nonnull -/** - * View for extracting metadata about a [Message]. - */ -class MessageMetadata( - val message: Message -) { +/** View for extracting metadata about a [Message]. */ +class MessageMetadata(val message: Message) { // ################################################################################################################ - // ### Encryption ### + // ### Encryption + // ### // ################################################################################################################ /** - * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is unencrypted. + * The [SymmetricKeyAlgorithm] of the outermost encrypted data packet, or null if message is + * unencrypted. */ val encryptionAlgorithm: SymmetricKeyAlgorithm? - get() = encryptionAlgorithms.let { - if (it.hasNext()) it.next() else null - } + get() = encryptionAlgorithms.let { if (it.hasNext()) it.next() else null } /** - * [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost encrypted data packet, the next item - * that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. + * [Iterator] of each [SymmetricKeyAlgorithm] encountered in the message. The first item + * returned by the iterator is the algorithm of the outermost encrypted data packet, the next + * item that of the next nested encrypted data packet and so on. The iterator might also be + * empty, in case of an unencrypted message. */ val encryptionAlgorithms: Iterator get() = encryptionLayers.asSequence().map { it.algorithm }.iterator() val isEncrypted: Boolean - get() = if (encryptionAlgorithm == null) false else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL + get() = + if (encryptionAlgorithm == null) false + else encryptionAlgorithm != SymmetricKeyAlgorithm.NULL fun isEncryptedFor(keys: PGPKeyRing): Boolean { return encryptionLayers.asSequence().any { - it.recipients.any { keyId -> - keys.getPublicKey(keyId) != null - } + it.recipients.any { keyId -> keys.getPublicKey(keyId) != null } } } /** - * [SessionKey] of the outermost encrypted data packet. - * If the message was unencrypted, this method returns `null`. + * [SessionKey] of the outermost encrypted data packet. If the message was unencrypted, this + * method returns `null`. */ val sessionKey: SessionKey? get() = sessionKeys.asSequence().firstOrNull() /** - * [Iterator] of each [SessionKey] for all encrypted data packets in the message. - * The first item returned by the iterator is the session key of the outermost encrypted data packet, - * the next item that of the next nested encrypted data packet and so on. - * The iterator might also be empty, in case of an unencrypted message. + * [Iterator] of each [SessionKey] for all encrypted data packets in the message. The first item + * returned by the iterator is the session key of the outermost encrypted data packet, the next + * item that of the next nested encrypted data packet and so on. The iterator might also be + * empty, in case of an unencrypted message. */ val sessionKeys: Iterator get() = encryptionLayers.asSequence().mapNotNull { it.sessionKey }.iterator() /** * [SubkeyIdentifier] of the decryption key that was used to decrypt the outermost encryption - * layer. - * If the message was unencrypted or was decrypted using a passphrase, this field might be `null`. + * layer. If the message was unencrypted or was decrypted using a passphrase, this field might + * be `null`. */ val decryptionKey: SubkeyIdentifier? - get() = encryptionLayers.asSequence() - .mapNotNull { it.decryptionKey } - .firstOrNull() + get() = encryptionLayers.asSequence().mapNotNull { it.decryptionKey }.firstOrNull() - /** - * List containing all recipient keyIDs. - */ + /** List containing all recipient keyIDs. */ val recipientKeyIds: List - get() = encryptionLayers.asSequence() + get() = + encryptionLayers + .asSequence() .map { it.recipients.toMutableList() } - .reduce { all, keyIds -> all.addAll(keyIds); all } + .reduce { all, keyIds -> + all.addAll(keyIds) + all + } .toList() val encryptionLayers: Iterator - get() = object : LayerIterator(message) { - override fun matches(layer: Packet) = layer is EncryptedData - override fun getProperty(last: Layer) = last as EncryptedData - } + get() = + object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is EncryptedData + + override fun getProperty(last: Layer) = last as EncryptedData + } // ################################################################################################################ - // ### Compression ### + // ### Compression + // ### // ################################################################################################################ /** - * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message - * does not contain any compressed data packets. + * [CompressionAlgorithm] of the outermost compressed data packet, or null, if the message does + * not contain any compressed data packets. */ - val compressionAlgorithm: CompressionAlgorithm? = compressionAlgorithms.asSequence().firstOrNull() + val compressionAlgorithm: CompressionAlgorithm? = + compressionAlgorithms.asSequence().firstOrNull() /** - * [Iterator] of each [CompressionAlgorithm] encountered in the message. - * The first item returned by the iterator is the algorithm of the outermost compressed data packet, the next - * item that of the next nested compressed data packet and so on. - * The iterator might also be empty, in case of a message without any compressed data packets. + * [Iterator] of each [CompressionAlgorithm] encountered in the message. The first item returned + * by the iterator is the algorithm of the outermost compressed data packet, the next item that + * of the next nested compressed data packet and so on. The iterator might also be empty, in + * case of a message without any compressed data packets. */ val compressionAlgorithms: Iterator get() = compressionLayers.asSequence().map { it.algorithm }.iterator() val compressionLayers: Iterator - get() = object : LayerIterator(message) { - override fun matches(layer: Packet) = layer is CompressedData - override fun getProperty(last: Layer) = last as CompressedData - } + get() = + object : LayerIterator(message) { + override fun matches(layer: Packet) = layer is CompressedData + + override fun getProperty(last: Layer) = last as CompressedData + } // ################################################################################################################ - // ### Signatures ### + // ### Signatures + // ### // ################################################################################################################ val isUsingCleartextSignatureFramework: Boolean @@ -133,81 +137,87 @@ class MessageMetadata( val verifiedSignatures: List get() = verifiedInlineSignatures.plus(verifiedDetachedSignatures) - /** - * List of all rejected signatures. - */ + /** List of all rejected signatures. */ val rejectedSignatures: List - get() = mutableListOf() + get() = + mutableListOf() .plus(rejectedInlineSignatures) .plus(rejectedDetachedSignatures) .toList() /** - * List of all verified inline-signatures. - * This list contains all acceptable, correct signatures that were part of the message itself. + * List of all verified inline-signatures. This list contains all acceptable, correct signatures + * that were part of the message itself. */ - val verifiedInlineSignatures: List = verifiedInlineSignaturesByLayer + val verifiedInlineSignatures: List = + verifiedInlineSignaturesByLayer .asSequence() .map { it.toMutableList() } - .reduce { acc, signatureVerifications -> acc.addAll(signatureVerifications); acc } + .reduce { acc, signatureVerifications -> + acc.addAll(signatureVerifications) + acc + } .toList() /** * [Iterator] of each [List] of verified inline-signatures of the message, separated by layer. - * Since signatures might occur in different layers within a message, this method can be used to gain more detailed - * insights into what signatures were encountered at what layers of the message structure. - * Each item of the [Iterator] represents a layer of the message and contains only signatures from - * this layer. - * An empty list means no (or no acceptable) signatures were encountered in that layer. + * Since signatures might occur in different layers within a message, this method can be used to + * gain more detailed insights into what signatures were encountered at what layers of the + * message structure. Each item of the [Iterator] represents a layer of the message and contains + * only signatures from this layer. An empty list means no (or no acceptable) signatures were + * encountered in that layer. */ val verifiedInlineSignaturesByLayer: Iterator> - get() = object : LayerIterator>(message) { - override fun matches(layer: Packet) = layer is Layer + get() = + object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer - override fun getProperty(last: Layer): List { - return listOf() + override fun getProperty(last: Layer): List { + return listOf() .plus(last.verifiedOnePassSignatures) .plus(last.verifiedPrependedSignatures) + } } - } - - /** - * List of all rejected inline-signatures of the message. - */ - val rejectedInlineSignatures: List = rejectedInlineSignaturesByLayer + /** List of all rejected inline-signatures of the message. */ + val rejectedInlineSignatures: List = + rejectedInlineSignaturesByLayer .asSequence() .map { it.toMutableList() } - .reduce { acc, failures -> acc.addAll(failures); acc} + .reduce { acc, failures -> + acc.addAll(failures) + acc + } .toList() /** - * Similar to [verifiedInlineSignaturesByLayer], this field contains all rejected inline-signatures - * of the message, but organized by layer. + * Similar to [verifiedInlineSignaturesByLayer], this field contains all rejected + * inline-signatures of the message, but organized by layer. */ val rejectedInlineSignaturesByLayer: Iterator> - get() = object : LayerIterator>(message) { - override fun matches(layer: Packet) = layer is Layer + get() = + object : LayerIterator>(message) { + override fun matches(layer: Packet) = layer is Layer - override fun getProperty(last: Layer): List = + override fun getProperty(last: Layer): List = mutableListOf() - .plus(last.rejectedOnePassSignatures) - .plus(last.rejectedPrependedSignatures) - } + .plus(last.rejectedOnePassSignatures) + .plus(last.rejectedPrependedSignatures) + } /** - * List of all verified detached signatures. - * This list contains all acceptable, correct detached signatures. + * List of all verified detached signatures. This list contains all acceptable, correct detached + * signatures. */ val verifiedDetachedSignatures: List = message.verifiedDetachedSignatures - /** - * List of all rejected detached signatures. - */ - val rejectedDetachedSignatures: List = message.rejectedDetachedSignatures + /** List of all rejected detached signatures. */ + val rejectedDetachedSignatures: List = + message.rejectedDetachedSignatures /** - * True, if the message contains any (verified or rejected) signature, false if no signatures are present. + * True, if the message contains any (verified or rejected) signature, false if no signatures + * are present. */ val hasSignature: Boolean get() = isVerifiedSigned() || hasRejectedSignatures() @@ -217,110 +227,131 @@ class MessageMetadata( fun hasRejectedSignatures(): Boolean = rejectedSignatures.isNotEmpty() /** - * Return true, if the message was signed by a certificate for which we can authenticate a binding to the given userId. + * Return true, if the message was signed by a certificate for which we can authenticate a + * binding to the given userId. * * @param userId userId - * @param email if true, treat the user-id as an email address and match all userIDs containing this address + * @param email if true, treat the user-id as an email address and match all userIDs containing + * this address * @param certificateAuthority certificate authority - * @param targetAmount targeted trust amount that needs to be reached by the binding to qualify as authenticated. - * defaults to 120. + * @param targetAmount targeted trust amount that needs to be reached by the binding to qualify + * as authenticated. defaults to 120. * @return true, if we can authenticate a binding for a signing key with sufficient evidence */ @JvmOverloads - fun isAuthenticatablySignedBy(userId: String, email: Boolean, certificateAuthority: CertificateAuthority, targetAmount: Int = 120): Boolean { - return verifiedSignatures.any { certificateAuthority - .authenticateBinding(it.signingKey.fingerprint, userId, email, it.signature.creationTime, targetAmount) + fun isAuthenticatablySignedBy( + userId: String, + email: Boolean, + certificateAuthority: CertificateAuthority, + targetAmount: Int = 120 + ): Boolean { + return verifiedSignatures.any { + certificateAuthority + .authenticateBinding( + it.signingKey.fingerprint, + userId, + email, + it.signature.creationTime, + targetAmount) .authenticated } } /** - * Return rue, if the message was verifiable signed by a certificate that either has the given fingerprint - * as primary key, or as the signing subkey. + * Return rue, if the message was verifiable signed by a certificate that either has the given + * fingerprint as primary key, or as the signing subkey. * * @param fingerprint fingerprint * @return true if message was signed by a cert identified by the given fingerprint */ fun isVerifiedSignedBy(fingerprint: OpenPgpFingerprint) = - verifiedSignatures.any { it.signingKey.matches(fingerprint) } + verifiedSignatures.any { it.signingKey.matches(fingerprint) } fun isVerifiedSignedBy(keys: PGPKeyRing) = - verifiedSignatures.any { keys.matches(it.signingKey) } + verifiedSignatures.any { keys.matches(it.signingKey) } fun isVerifiedDetachedSignedBy(fingerprint: OpenPgpFingerprint) = - verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } + verifiedDetachedSignatures.any { it.signingKey.matches(fingerprint) } fun isVerifiedDetachedSignedBy(keys: PGPKeyRing) = - verifiedDetachedSignatures.any { keys.matches(it.signingKey) } + verifiedDetachedSignatures.any { keys.matches(it.signingKey) } fun isVerifiedInlineSignedBy(fingerprint: OpenPgpFingerprint) = - verifiedInlineSignatures.any { it.signingKey.matches(fingerprint) } + verifiedInlineSignatures.any { it.signingKey.matches(fingerprint) } fun isVerifiedInlineSignedBy(keys: PGPKeyRing) = - verifiedInlineSignatures.any { keys.matches(it.signingKey) } + verifiedInlineSignatures.any { keys.matches(it.signingKey) } // ################################################################################################################ - // ### Literal Data ### + // ### Literal Data + // ### // ################################################################################################################ /** - * Value of the literal data packet's filename field. - * This value can be used to store a decrypted file under its original filename, - * but since this field is not necessarily part of the signed data of a message, usage of this field is - * discouraged. + * Value of the literal data packet's filename field. This value can be used to store a + * decrypted file under its original filename, but since this field is not necessarily part of + * the signed data of a message, usage of this field is discouraged. * - * @see RFC4880 §5.9. Literal Data Packet + * @see RFC4880 §5.9. Literal Data + * Packet */ val filename: String? = findLiteralData()?.fileName /** - * True, if the sender signals an increased degree of confidentiality by setting the filename of the literal - * data packet to a special value that indicates that the data is intended for your eyes only. + * True, if the sender signals an increased degree of confidentiality by setting the filename of + * the literal data packet to a special value that indicates that the data is intended for your + * eyes only. */ @Deprecated("Reliance on this signaling mechanism is discouraged.") val isForYourEyesOnly: Boolean = PGPLiteralData.CONSOLE == filename /** - * Value of the literal data packets modification date field. - * This value can be used to restore the modification date of a decrypted file, - * but since this field is not necessarily part of the signed data, its use is discouraged. + * Value of the literal data packets modification date field. This value can be used to restore + * the modification date of a decrypted file, but since this field is not necessarily part of + * the signed data, its use is discouraged. * - * @see RFC4880 §5.9. Literal Data Packet + * @see RFC4880 §5.9. Literal Data + * Packet */ val modificationDate: Date? = findLiteralData()?.modificationDate /** - * Value of the format field of the literal data packet. - * This value indicates what format (text, binary data, ...) the data has. - * Since this field is not necessarily part of the signed data of a message, its usage is discouraged. + * Value of the format field of the literal data packet. This value indicates what format (text, + * binary data, ...) the data has. Since this field is not necessarily part of the signed data + * of a message, its usage is discouraged. * - * @see RFC4880 §5.9. Literal Data Packet + * @see RFC4880 §5.9. Literal Data + * Packet */ val literalDataEncoding: StreamEncoding? = findLiteralData()?.format /** - * Find the [LiteralData] layer of an OpenPGP message. - * This method might return null, for example for a cleartext signed message without OpenPGP packets. + * Find the [LiteralData] layer of an OpenPGP message. This method might return null, for + * example for a cleartext signed message without OpenPGP packets. * * @return literal data */ private fun findLiteralData(): LiteralData? { - // If the message is a non-OpenPGP message with a detached signature, or a Cleartext Signed message, + // If the message is a non-OpenPGP message with a detached signature, or a Cleartext Signed + // message, // we might not have a Literal Data packet. var nested = message.child ?: return null while (nested.hasNestedChild()) { val layer = nested as Layer - nested = checkNotNull(layer.child) { - // Otherwise, we MUST find a Literal Data packet, or else the message is malformed - "Malformed OpenPGP message. Cannot find Literal Data Packet" - } + nested = + checkNotNull(layer.child) { + // Otherwise, we MUST find a Literal Data packet, or else the message is + // malformed + "Malformed OpenPGP message. Cannot find Literal Data Packet" + } } return nested as LiteralData } // ################################################################################################################ - // ### Message Structure ### + // ### Message Structure + // ### // ################################################################################################################ interface Packet @@ -329,13 +360,12 @@ class MessageMetadata( fun hasNestedChild(): Boolean } - abstract class Layer( - val depth: Int - ) : Packet { + abstract class Layer(val depth: Int) : Packet { init { if (depth > MAX_LAYER_DEPTH) { - throw MalformedOpenPgpMessageException("Maximum packet nesting depth ($MAX_LAYER_DEPTH) exceeded.") + throw MalformedOpenPgpMessageException( + "Maximum packet nesting depth ($MAX_LAYER_DEPTH) exceeded.") } } @@ -347,9 +377,8 @@ class MessageMetadata( val rejectedPrependedSignatures: List = mutableListOf() /** - * Nested child element of this layer. - * Might be `null`, if this layer does not have a child element - * (e.g. if this is a [LiteralData] packet). + * Nested child element of this layer. Might be `null`, if this layer does not have a child + * element (e.g. if this is a [LiteralData] packet). */ var child: Nested? = null @@ -386,8 +415,8 @@ class MessageMetadata( * Outermost OpenPGP Message structure. * * @param cleartextSigned whether the message is using the Cleartext Signature Framework - * - * @see RFC4880 §7. Cleartext Signature Framework + * @see RFC4880 §7. Cleartext + * Signature Framework */ class Message(var cleartextSigned: Boolean = false) : Layer(0) { fun setCleartextSigned() = apply { cleartextSigned = true } @@ -397,14 +426,14 @@ class MessageMetadata( * Literal Data Packet. * * @param fileName value of the filename field. An empty String represents no filename. - * @param modificationDate value of the modification date field. The special value `Date(0)` indicates no - * modification date. + * @param modificationDate value of the modification date field. The special value `Date(0)` + * indicates no modification date. * @param format value of the format field. */ class LiteralData( - val fileName: String = "", - val modificationDate: Date = Date(0L), - val format: StreamEncoding = StreamEncoding.BINARY + val fileName: String = "", + val modificationDate: Date = Date(0L), + val format: StreamEncoding = StreamEncoding.BINARY ) : Nested { // A literal data packet MUST NOT have a child element, as its content is the plaintext @@ -417,9 +446,7 @@ class MessageMetadata( * @param algorithm [CompressionAlgorithm] used to compress the packet. * @param depth nesting depth at which this packet was encountered. */ - class CompressedData( - val algorithm: CompressionAlgorithm, - depth: Int) : Layer(depth), Nested { + class CompressedData(val algorithm: CompressionAlgorithm, depth: Int) : Layer(depth), Nested { // A compressed data packet MUST have a child element override fun hasNestedChild() = true @@ -431,38 +458,30 @@ class MessageMetadata( * @param algorithm symmetric key algorithm used to encrypt the packet. * @param depth nesting depth at which this packet was encountered. */ - class EncryptedData( - val algorithm: SymmetricKeyAlgorithm, - depth: Int - ) : Layer(depth), Nested { + class EncryptedData(val algorithm: SymmetricKeyAlgorithm, depth: Int) : Layer(depth), Nested { - /** - * [SessionKey] used to decrypt the packet. - */ + /** [SessionKey] used to decrypt the packet. */ var sessionKey: SessionKey? = null - /** - * List of all recipient key ids to which the packet was encrypted for. - */ + /** List of all recipient key ids to which the packet was encrypted for. */ val recipients: List = mutableListOf() - fun addRecipients(keyIds: List) = apply { - (recipients as MutableList).addAll(keyIds) - } + fun addRecipients(keyIds: List) = apply { (recipients as MutableList).addAll(keyIds) } /** - * Identifier of the subkey that was used to decrypt the packet (in case of a public key encrypted packet). + * Identifier of the subkey that was used to decrypt the packet (in case of a public key + * encrypted packet). */ var decryptionKey: SubkeyIdentifier? = null // An encrypted data packet MUST have a child element override fun hasNestedChild() = true - } /** - * Iterator that iterates the packet structure from outermost to innermost packet, emitting the results of - * a transformation ([getProperty]) on those packets that match ([matches]) a given criterion. + * Iterator that iterates the packet structure from outermost to innermost packet, emitting the + * results of a transformation ([getProperty]) on those packets that match ([matches]) a given + * criterion. * * @param message outermost structure object */ @@ -519,6 +538,7 @@ class MessageMetadata( } abstract fun matches(layer: Packet): Boolean + abstract fun getProperty(last: Layer): O } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt index f8bb448b..c5443ba8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingKeyPassphraseStrategy.kt @@ -4,19 +4,18 @@ package org.pgpainless.decryption_verification -/** - * Strategy defining how missing secret key passphrases are handled. - */ +/** Strategy defining how missing secret key passphrases are handled. */ enum class MissingKeyPassphraseStrategy { /** - * Try to interactively obtain key passphrases one-by-one via callbacks, - * eg [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider]. + * Try to interactively obtain key passphrases one-by-one via callbacks, eg + * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider]. */ INTERACTIVE, /** * Do not try to obtain passphrases interactively and instead throw a - * [org.pgpainless.exception.MissingPassphraseException] listing all keys with missing passphrases. + * [org.pgpainless.exception.MissingPassphraseException] listing all keys with missing + * passphrases. */ THROW_EXCEPTION -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt index 723530b7..eb81847f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MissingPublicKeyCallback.kt @@ -9,20 +9,19 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing fun interface MissingPublicKeyCallback { /** - * This method gets called if we encounter a signature made by a key which was not provided for signature verification. - * If you cannot provide the requested key, it is safe to return null here. - * PGPainless will then continue verification with the next signature. + * This method gets called if we encounter a signature made by a key which was not provided for + * signature verification. If you cannot provide the requested key, it is safe to return null + * here. PGPainless will then continue verification with the next signature. * - * Note: The key-id might belong to a subkey, so be aware that when looking up the [PGPPublicKeyRing], - * you may not only search for the key-id on the key rings primary key! + * Note: The key-id might belong to a subkey, so be aware that when looking up the + * [PGPPublicKeyRing], you may not only search for the key-id on the key rings primary key! * - * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id. + * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately + * one-pass-signatures only contain the key id. * * @param keyId ID of the missing signing (sub)key * @return keyring containing the key or null - * * @see RFC */ fun onMissingPublicKeyEncountered(keyId: Long): PGPPublicKeyRing? -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index b823fdaf..5e8b68f2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -4,6 +4,9 @@ package org.pgpainless.decryption_verification +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException @@ -34,16 +37,14 @@ import org.pgpainless.signature.consumer.SignatureValidator import org.pgpainless.util.ArmoredInputStreamFactory import org.pgpainless.util.SessionKey import org.slf4j.LoggerFactory -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream class OpenPgpMessageInputStream( - type: Type, - inputStream: InputStream, - private val options: ConsumerOptions, - private val layerMetadata: Layer, - private val policy: Policy) : DecryptionStream() { + type: Type, + inputStream: InputStream, + private val options: ConsumerOptions, + private val layerMetadata: Layer, + private val policy: Policy +) : DecryptionStream() { private val signatures: Signatures = Signatures(options) private var packetInputStream: TeeBCPGInputStream? = null @@ -58,19 +59,20 @@ class OpenPgpMessageInputStream( signatures.addDetachedSignatures(options.getDetachedSignatures()) } - when(type) { + when (type) { Type.standard -> { // tee out packet bytes for signature verification - packetInputStream = TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures) + packetInputStream = + TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures) // *omnomnom* consumePackets() } - Type.cleartext_signed -> { val multiPassStrategy = options.getMultiPassStrategy() - val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + val detachedSignatures = + ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.messageOutputStream) for (signature in detachedSignatures) { @@ -78,9 +80,9 @@ class OpenPgpMessageInputStream( } options.isForceNonOpenPgpData() - nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) + nestedInputStream = + TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) } - Type.non_openpgp -> { packetInputStream = null nestedInputStream = TeeInputStream(inputStream, this.signatures) @@ -89,11 +91,17 @@ class OpenPgpMessageInputStream( } enum class Type { - standard, cleartext_signed, non_openpgp + standard, + cleartext_signed, + non_openpgp } - constructor(inputStream: InputStream, options: ConsumerOptions, metadata: Layer, policy: Policy): - this(Type.standard, inputStream, options, metadata, policy) + constructor( + inputStream: InputStream, + options: ConsumerOptions, + metadata: Layer, + policy: Policy + ) : this(Type.standard, inputStream, options, metadata, policy) private fun consumePackets() { val pIn = packetInputStream ?: return @@ -102,50 +110,53 @@ class OpenPgpMessageInputStream( // Comsume packets, potentially stepping into nested layers layer@ while (run { - packet = pIn.nextPacketTag() - packet - } != null) { + packet = pIn.nextPacketTag() + packet + } != null) { signatures.nextPacket(packet!!) // Consume packets in a layer - when(packet) { - + when (packet) { OpenPgpPacket.LIT -> { processLiteralData() break@layer // nest down } - OpenPgpPacket.COMP -> { processCompressedData() break@layer // nest down } - OpenPgpPacket.OPS -> { processOnePassSignature() // OPS is on the same layer, no nest down } - OpenPgpPacket.SIG -> { processSignature() // SIG is on the same layer, no nest down } - - OpenPgpPacket.PKESK, OpenPgpPacket.SKESK, OpenPgpPacket.SED, OpenPgpPacket.SEIPD -> { + OpenPgpPacket.PKESK, + OpenPgpPacket.SKESK, + OpenPgpPacket.SED, + OpenPgpPacket.SEIPD -> { if (processEncryptedData()) { break@layer } throw MissingDecryptionMethodException("No working decryption method found.") } - OpenPgpPacket.MARKER -> { LOGGER.debug("Skipping Marker Packet") pIn.readMarker() } - - OpenPgpPacket.SK, OpenPgpPacket.PK, OpenPgpPacket.SSK, OpenPgpPacket.PSK, OpenPgpPacket.TRUST, OpenPgpPacket.UID, OpenPgpPacket.UATTR -> + OpenPgpPacket.SK, + OpenPgpPacket.PK, + OpenPgpPacket.SSK, + OpenPgpPacket.PSK, + OpenPgpPacket.TRUST, + OpenPgpPacket.UID, + OpenPgpPacket.UATTR -> throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") - - OpenPgpPacket.EXP_1, OpenPgpPacket.EXP_2, OpenPgpPacket.EXP_3, OpenPgpPacket.EXP_4 -> + OpenPgpPacket.EXP_1, + OpenPgpPacket.EXP_2, + OpenPgpPacket.EXP_3, + OpenPgpPacket.EXP_4 -> throw MalformedOpenPgpMessageException("Unsupported Packet in Stream: $packet") - else -> throw MalformedOpenPgpMessageException("Unexpected Packet in Stream: $packet") } @@ -158,8 +169,10 @@ class OpenPgpMessageInputStream( val literalData = packetInputStream!!.readLiteralData() // Extract Metadata - layerMetadata.child = LiteralData( - literalData.fileName, literalData.modificationTime, + layerMetadata.child = + LiteralData( + literalData.fileName, + literalData.modificationTime, StreamEncoding.requireFromCode(literalData.format)) nestedInputStream = literalData.inputStream @@ -171,18 +184,22 @@ class OpenPgpMessageInputStream( val compressedData = packetInputStream!!.readCompressedData() // Extract Metadata - val compressionLayer = CompressedData( + val compressionLayer = + CompressedData( CompressionAlgorithm.requireFromId(compressedData.algorithm), layerMetadata.depth + 1) - LOGGER.debug("Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") - nestedInputStream = OpenPgpMessageInputStream(compressedData.dataStream, options, compressionLayer, policy) + LOGGER.debug( + "Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") + nestedInputStream = + OpenPgpMessageInputStream(compressedData.dataStream, options, compressionLayer, policy) } private fun processOnePassSignature() { syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) val ops = packetInputStream!!.readOnePassSignature() - LOGGER.debug("One-Pass-Signature Packet by key ${ops.keyID.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug( + "One-Pass-Signature Packet by key ${ops.keyID.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addOnePassSignature(ops) } @@ -190,26 +207,33 @@ class OpenPgpMessageInputStream( // true if signature corresponds to OPS val isSigForOps = syntaxVerifier.peekStack() == StackSymbol.OPS syntaxVerifier.next(InputSymbol.SIGNATURE) - val signature = try { - packetInputStream!!.readSignature() - } catch (e : UnsupportedPacketVersionException) { - LOGGER.debug("Unsupported Signature at depth ${layerMetadata.depth} encountered.", e) - return - } + val signature = + try { + packetInputStream!!.readSignature() + } catch (e: UnsupportedPacketVersionException) { + LOGGER.debug( + "Unsupported Signature at depth ${layerMetadata.depth} encountered.", e) + return + } val keyId = signature.issuerKeyId if (isSigForOps) { - LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") - signatures.leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + LOGGER.debug( + "Signature Packet corresponding to One-Pass-Signature by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + signatures + .leaveNesting() // TODO: Only leave nesting if all OPSs of the nesting layer are + // dealt with signatures.addCorrespondingOnePassSignature(signature, layerMetadata, policy) } else { - LOGGER.debug("Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + LOGGER.debug( + "Prepended Signature Packet by key ${keyId.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") signatures.addPrependedSignature(signature) } } private fun processEncryptedData(): Boolean { - LOGGER.debug("Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") + LOGGER.debug( + "Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) val encDataList = packetInputStream!!.readEncryptedDataList() if (!encDataList.isIntegrityProtected) { @@ -220,22 +244,25 @@ class OpenPgpMessageInputStream( } val esks = SortedESKs(encDataList) - LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" + + LOGGER.debug( + "Symmetrically Encrypted Integrity-Protected Data has ${esks.skesks.size} SKESK(s) and" + " ${esks.pkesks.size + esks.anonPkesks.size} PKESK(s) from which ${esks.anonPkesks.size} PKESK(s)" + " have an anonymous recipient.") // try custom decryptor factories for ((key, decryptorFactory) in options.getCustomDecryptorFactories()) { LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.") - esks.pkesks.filter { - // find matching PKESK - it.keyID == key.subkeyId - }.forEach { - // attempt decryption - if (decryptPKESKAndStream(esks, key, decryptorFactory, it)) { - return true + esks.pkesks + .filter { + // find matching PKESK + it.keyID == key.subkeyId + } + .forEach { + // attempt decryption + if (decryptPKESKAndStream(esks, key, decryptorFactory, it)) { + return true + } } - } } // try provided session key @@ -244,20 +271,24 @@ class OpenPgpMessageInputStream( LOGGER.debug("Attempt decryption with provided session key.") throwIfUnacceptable(sk.algorithm) - val decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sk) + val decryptorFactory = + ImplementationFactory.getInstance().getSessionKeyDataDecryptorFactory(sk) val layer = EncryptedData(sk.algorithm, layerMetadata.depth + 1) val skEncData = encDataList.extractSessionKeyEncryptedData() try { val decrypted = skEncData.getDataStream(decryptorFactory) layer.sessionKey = sk - val integrityProtected = IntegrityProtectedInputStream(decrypted, skEncData, options) - nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, layer, policy) + val integrityProtected = + IntegrityProtectedInputStream(decrypted, skEncData, options) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, layer, policy) LOGGER.debug("Successfully decrypted data using provided session key") return true - } catch (e : PGPException) { + } catch (e: PGPException) { // Session key mismatch? - LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e) + LOGGER.debug( + "Decryption using provided session key failed. Mismatched session key and message?", + e) } } @@ -267,19 +298,21 @@ class OpenPgpMessageInputStream( LOGGER.debug("Attempt decryption with provided passphrase") val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) if (!isAcceptable(algorithm)) { - LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm $algorithm") + LOGGER.debug( + "Skipping SKESK with unacceptable encapsulation algorithm $algorithm") continue } - val decryptorFactory = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase) + val decryptorFactory = + ImplementationFactory.getInstance().getPBEDataDecryptorFactory(passphrase) if (decryptSKESKAndStream(esks, skesk, decryptorFactory)) { return true } } } - val postponedDueToMissingPassphrase = mutableListOf>() + val postponedDueToMissingPassphrase = + mutableListOf>() // try (known) secret keys esks.pkesks.forEach { pkesk -> @@ -295,7 +328,8 @@ class OpenPgpMessageInputStream( LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(secretKey.keyID)) { - LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + LOGGER.debug( + "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") postponedDueToMissingPassphrase.add(secretKey to pkesk) continue } @@ -319,7 +353,8 @@ class OpenPgpMessageInputStream( val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(secretKey.keyID)) { - LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") + LOGGER.debug( + "Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") postponedDueToMissingPassphrase.add(secretKey to pkesk) continue } @@ -331,15 +366,14 @@ class OpenPgpMessageInputStream( } } - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + if (options.getMissingKeyPassphraseStrategy() == + MissingKeyPassphraseStrategy.THROW_EXCEPTION) { // Non-interactive mode: Throw an exception with all locked decryption keys - postponedDueToMissingPassphrase.map { - SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) - }.also { - if (it.isNotEmpty()) - throw MissingPassphraseException(it.toSet()) - } - } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { + postponedDueToMissingPassphrase + .map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) } + .also { if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } + } else if (options.getMissingKeyPassphraseStrategy() == + MissingKeyPassphraseStrategy.INTERACTIVE) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { val keyId = secretKey.keyID val decryptionKeys = getDecryptionKey(pkesk)!! @@ -348,7 +382,8 @@ class OpenPgpMessageInputStream( continue } - LOGGER.debug("Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") + LOGGER.debug( + "Attempt decryption with key $decryptionKeyId while interactively requesting its passphrase.") val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue val privateKey = secretKey.unlock(protector) if (decryptWithPrivateKey(esks, privateKey, decryptionKeyId, pkesk)) { @@ -364,29 +399,37 @@ class OpenPgpMessageInputStream( return false } - private fun decryptWithPrivateKey(esks: SortedESKs, - privateKey: PGPPrivateKey, - decryptionKeyId: SubkeyIdentifier, - pkesk: PGPPublicKeyEncryptedData): Boolean { - val decryptorFactory = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(privateKey) + private fun decryptWithPrivateKey( + esks: SortedESKs, + privateKey: PGPPrivateKey, + decryptionKeyId: SubkeyIdentifier, + pkesk: PGPPublicKeyEncryptedData + ): Boolean { + val decryptorFactory = + ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey) return decryptPKESKAndStream(esks, decryptionKeyId, decryptorFactory, pkesk) } - private fun hasUnsupportedS2KSpecifier(secretKey: PGPSecretKey, decryptionKeyId: SubkeyIdentifier): Boolean { + private fun hasUnsupportedS2KSpecifier( + secretKey: PGPSecretKey, + decryptionKeyId: SubkeyIdentifier + ): Boolean { val s2k = secretKey.s2K if (s2k != null) { if (s2k.type in 100..110) { - LOGGER.debug("Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}") + LOGGER.debug( + "Skipping PKESK because key $decryptionKeyId has unsupported private S2K specifier ${s2k.type}") return true } } return false } - private fun decryptSKESKAndStream(esks: SortedESKs, - skesk: PGPPBEEncryptedData, - decryptorFactory: PBEDataDecryptorFactory): Boolean { + private fun decryptSKESKAndStream( + esks: SortedESKs, + skesk: PGPPBEEncryptedData, + decryptorFactory: PBEDataDecryptorFactory + ): Boolean { try { val decrypted = skesk.getDataStream(decryptorFactory) val sessionKey = SessionKey(skesk.getSessionKey(decryptorFactory)) @@ -396,38 +439,45 @@ class OpenPgpMessageInputStream( encryptedData.addRecipients(esks.pkesks.map { it.keyID }) LOGGER.debug("Successfully decrypted data with passphrase") val integrityProtected = IntegrityProtectedInputStream(decrypted, skesk, options) - nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) return true - } catch (e : UnacceptableAlgorithmException) { + } catch (e: UnacceptableAlgorithmException) { throw e - } catch (e : PGPException) { - LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e) + } catch (e: PGPException) { + LOGGER.debug( + "Decryption of encrypted data packet using password failed. Password mismatch?", e) } return false } - private fun decryptPKESKAndStream(esks: SortedESKs, - decryptionKeyId: SubkeyIdentifier, - decryptorFactory: PublicKeyDataDecryptorFactory, - pkesk: PGPPublicKeyEncryptedData): Boolean { + private fun decryptPKESKAndStream( + esks: SortedESKs, + decryptionKeyId: SubkeyIdentifier, + decryptorFactory: PublicKeyDataDecryptorFactory, + pkesk: PGPPublicKeyEncryptedData + ): Boolean { try { val decrypted = pkesk.getDataStream(decryptorFactory) val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) throwIfUnacceptable(sessionKey.algorithm) - val encryptedData = EncryptedData( - SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + val encryptedData = + EncryptedData( + SymmetricKeyAlgorithm.requireFromId( + pkesk.getSymmetricAlgorithm(decryptorFactory)), layerMetadata.depth + 1) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyID }) LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) - nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) + nestedInputStream = + OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy) return true - } catch (e : UnacceptableAlgorithmException) { + } catch (e: UnacceptableAlgorithmException) { throw e - } catch (e : PGPException) { + } catch (e: PGPException) { LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e) } return false @@ -441,11 +491,12 @@ class OpenPgpMessageInputStream( return -1 } - val r: Int = try { - nestedInputStream!!.read() - } catch (e: IOException) { - -1 - } + val r: Int = + try { + nestedInputStream!!.read() + } catch (e: IOException) { + -1 + } if (r != -1) { signatures.updateLiteral(r.toByte()) } else { @@ -532,34 +583,37 @@ class OpenPgpMessageInputStream( return MessageMetadata((layerMetadata as Message)) } - private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { - it.any { - k -> k.keyID == keyId - }.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { - k -> k.keyID == keyId - }) - } - - private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { - it.getSecretKeyFor(pkesk) != null && PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> - when (pkesk.version) { - 3 -> pkesk.keyID == subkey.keyID - else -> throw NotImplementedError("Version 6 PKESK not yet supported.") - } + private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = + options.getDecryptionKeys().firstOrNull { + it.any { k -> k.keyID == keyId } + .and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { k -> k.keyID == keyId }) } - } - private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = - options.getDecryptionKeys().filter { - it.getSecretKeyFor(pkesk) != null && PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + private fun getDecryptionKey(pkesk: PGPPublicKeyEncryptedData): PGPSecretKeyRing? = + options.getDecryptionKeys().firstOrNull { + it.getSecretKeyFor(pkesk) != null && + PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> when (pkesk.version) { 3 -> pkesk.keyID == subkey.keyID else -> throw NotImplementedError("Version 6 PKESK not yet supported.") } } - } + } - private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List> { + private fun getDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List = + options.getDecryptionKeys().filter { + it.getSecretKeyFor(pkesk) != null && + PGPainless.inspectKeyRing(it).decryptionSubkeys.any { subkey -> + when (pkesk.version) { + 3 -> pkesk.keyID == subkey.keyID + else -> throw NotImplementedError("Version 6 PKESK not yet supported.") + } + } + } + + private fun findPotentialDecryptionKeys( + pkesk: PGPPublicKeyEncryptedData + ): List> { val algorithm = pkesk.algorithm val candidates = mutableListOf>() options.getDecryptionKeys().forEach { @@ -574,11 +628,12 @@ class OpenPgpMessageInputStream( } private fun isAcceptable(algorithm: SymmetricKeyAlgorithm): Boolean = - policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) + policy.symmetricKeyDecryptionAlgorithmPolicy.isAcceptable(algorithm) private fun throwIfUnacceptable(algorithm: SymmetricKeyAlgorithm) { if (!isAcceptable(algorithm)) { - throw UnacceptableAlgorithmException("Symmetric-Key algorithm $algorithm is not acceptable for message decryption.") + throw UnacceptableAlgorithmException( + "Symmetric-Key algorithm $algorithm is not acceptable for message decryption.") } } @@ -610,9 +665,7 @@ class OpenPgpMessageInputStream( get() = skesks.plus(pkesks).plus(anonPkesks) } - private class Signatures( - val options: ConsumerOptions - ) : OutputStream() { + private class Signatures(val options: ConsumerOptions) : OutputStream() { val detachedSignatures = mutableListOf() val prependedSignatures = mutableListOf() val onePassSignatures = mutableListOf() @@ -636,8 +689,10 @@ class OpenPgpMessageInputStream( if (check != null) { detachedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") - detachedSignaturesWithMissingCert.add(SignatureVerification.Failure( + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + detachedSignaturesWithMissingCert.add( + SignatureVerification.Failure( signature, null, SignatureValidationException("Missing verification key."))) } } @@ -648,10 +703,11 @@ class OpenPgpMessageInputStream( if (check != null) { prependedSignatures.add(check) } else { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") - prependedSignaturesWithMissingCert.add(SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key") - )) + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + prependedSignaturesWithMissingCert.add( + SignatureVerification.Failure( + signature, null, SignatureValidationException("Missing verification key"))) } } @@ -680,7 +736,11 @@ class OpenPgpMessageInputStream( } } - fun addCorrespondingOnePassSignature(signature: PGPSignature, layer: Layer, policy: Policy) { + fun addCorrespondingOnePassSignature( + signature: PGPSignature, + layer: Layer, + policy: Policy + ) { var found = false val keyId = signature.issuerKeyId for ((i, check) in onePassSignatures.withIndex().reversed()) { @@ -694,27 +754,32 @@ class OpenPgpMessageInputStream( } check.signature = signature - val verification = SignatureVerification(signature, + val verification = + SignatureVerification( + signature, SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(signature) + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature) CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) LOGGER.debug("Acceptable signature by key ${verification.signingKey}") layer.addVerifiedOnePassSignature(verification) } catch (e: SignatureValidationException) { LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) - layer.addRejectedOnePassSignature(SignatureVerification.Failure(verification, e)) + layer.addRejectedOnePassSignature( + SignatureVerification.Failure(verification, e)) } break } if (!found) { - LOGGER.debug("No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") - inbandSignaturesWithMissingCert.add(SignatureVerification.Failure( - signature, null, SignatureValidationException("Missing verification key.") - )) + LOGGER.debug( + "No suitable certificate for verification of signature by key ${keyId.openPgpKeyId()} found.") + inbandSignaturesWithMissingCert.add( + SignatureVerification.Failure( + signature, null, SignatureValidationException("Missing verification key."))) } } @@ -737,7 +802,9 @@ class OpenPgpMessageInputStream( } if (options.getMissingCertificateCallback() != null) { - return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(signature.keyID) + return options + .getMissingCertificateCallback()!! + .onMissingPublicKeyEncountered(signature.keyID) } return null // TODO: Missing cert for sig } @@ -749,7 +816,9 @@ class OpenPgpMessageInputStream( } if (options.getMissingCertificateCallback() != null) { - return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(signature.keyID) + return options + .getMissingCertificateCallback()!! + .onMissingPublicKeyEncountered(signature.keyID) } return null // TODO: Missing cert for sig } @@ -800,32 +869,42 @@ class OpenPgpMessageInputStream( fun finish(layer: Layer, policy: Policy) { for (detached in detachedSignatures) { - val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier) + val verification = + SignatureVerification(detached.signature, detached.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(detached.signature) + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( - detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy) + detached.signature, + KeyRingUtils.publicKeys(detached.signingKeyRing), + policy) LOGGER.debug("Acceptable signature by key ${verification.signingKey}") layer.addVerifiedDetachedSignature(verification) - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) - layer.addRejectedDetachedSignature(SignatureVerification.Failure(verification, e)) + layer.addRejectedDetachedSignature( + SignatureVerification.Failure(verification, e)) } } for (prepended in prependedSignatures) { - val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) + val verification = + SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) - .verify(prepended.signature) + SignatureValidator.signatureWasCreatedInBounds( + options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( - prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy) + prepended.signature, + KeyRingUtils.publicKeys(prepended.signingKeyRing), + policy) LOGGER.debug("Acceptable signature by key ${verification.signingKey}") layer.addVerifiedPrependedSignature(verification) - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { LOGGER.debug("Rejected signature by key ${verification.signingKey}", e) - layer.addRejectedPrependedSignature(SignatureVerification.Failure(verification, e)) + layer.addRejectedPrependedSignature( + SignatureVerification.Failure(verification, e)) } } @@ -864,22 +943,22 @@ class OpenPgpMessageInputStream( companion object { @JvmStatic private fun initialize(signature: PGPSignature, publicKey: PGPPublicKey) { - val verifierProvider = ImplementationFactory.getInstance() - .pgpContentVerifierBuilderProvider + val verifierProvider = + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider try { signature.init(verifierProvider, publicKey) - } catch (e : PGPException) { + } catch (e: PGPException) { throw RuntimeException(e) } } @JvmStatic private fun initialize(ops: PGPOnePassSignature, publicKey: PGPPublicKey) { - val verifierProvider = ImplementationFactory.getInstance() - .pgpContentVerifierBuilderProvider + val verifierProvider = + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider try { ops.init(verifierProvider, publicKey) - } catch (e : PGPException) { + } catch (e: PGPException) { throw RuntimeException(e) } } @@ -891,36 +970,40 @@ class OpenPgpMessageInputStream( private val LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream::class.java) @JvmStatic - fun create(inputStream: InputStream, - options: ConsumerOptions) = create(inputStream, options, PGPainless.getPolicy()) + fun create(inputStream: InputStream, options: ConsumerOptions) = + create(inputStream, options, PGPainless.getPolicy()) @JvmStatic - fun create(inputStream: InputStream, - options: ConsumerOptions, - policy: Policy) = create(inputStream, options, Message(), policy) + fun create(inputStream: InputStream, options: ConsumerOptions, policy: Policy) = + create(inputStream, options, Message(), policy) @JvmStatic - internal fun create(inputStream: InputStream, - options: ConsumerOptions, - metadata: Layer, - policy: Policy): OpenPgpMessageInputStream { + internal fun create( + inputStream: InputStream, + options: ConsumerOptions, + metadata: Layer, + policy: Policy + ): OpenPgpMessageInputStream { val openPgpIn = OpenPgpInputStream(inputStream) openPgpIn.reset() if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { - return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy) + return OpenPgpMessageInputStream( + Type.non_openpgp, openPgpIn, options, metadata, policy) } if (openPgpIn.isBinaryOpenPgp) { // Simply consume OpenPGP message - return OpenPgpMessageInputStream(Type.standard, openPgpIn, options, metadata, policy) + return OpenPgpMessageInputStream( + Type.standard, openPgpIn, options, metadata, policy) } return if (openPgpIn.isAsciiArmored) { val armorIn = ArmoredInputStreamFactory.get(openPgpIn) if (armorIn.isClearText) { (metadata as Message).setCleartextSigned() - OpenPgpMessageInputStream(Type.cleartext_signed, armorIn, options, metadata, policy) + OpenPgpMessageInputStream( + Type.cleartext_signed, armorIn, options, metadata, policy) } else { // Simply consume dearmored OpenPGP message OpenPgpMessageInputStream(Type.standard, armorIn, options, metadata, policy) @@ -930,4 +1013,4 @@ class OpenPgpMessageInputStream( } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt index 8d229fb2..3e00fbb2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/SignatureVerification.kt @@ -11,43 +11,43 @@ import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.signature.SignatureUtils /** - * Tuple of a signature and an identifier of its corresponding verification key. - * Semantic meaning of the signature verification (success, failure) is merely given by context. - * E.g. [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, - * while the class [Failure] contains failed verifications. + * Tuple of a signature and an identifier of its corresponding verification key. Semantic meaning of + * the signature verification (success, failure) is merely given by context. E.g. + * [MessageMetadata.getVerifiedInlineSignatures] contains verified verifications, while the class + * [Failure] contains failed verifications. * * @param signature PGPSignature object * @param signingKey [SubkeyIdentifier] of the (sub-) key that is used for signature verification. - * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. + * Note, that this might be null, e.g. in case of a [Failure] due to missing verification key. */ -data class SignatureVerification( - val signature: PGPSignature, - val signingKey: SubkeyIdentifier -) { +data class SignatureVerification(val signature: PGPSignature, val signingKey: SubkeyIdentifier) { override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)};" + - " Key: $signingKey;" + " Key: $signingKey;" } /** - * Tuple object of a [SignatureVerification] and the corresponding [SignatureValidationException] - * that caused the verification to fail. + * Tuple object of a [SignatureVerification] and the corresponding + * [SignatureValidationException] that caused the verification to fail. * - * @param signatureVerification verification (tuple of [PGPSignature] and corresponding [SubkeyIdentifier]) + * @param signatureVerification verification (tuple of [PGPSignature] and corresponding + * [SubkeyIdentifier]) * @param validationException exception that caused the verification to fail */ data class Failure( - val signature: PGPSignature, - val signingKey: SubkeyIdentifier?, - val validationException: SignatureValidationException + val signature: PGPSignature, + val signingKey: SubkeyIdentifier?, + val validationException: SignatureValidationException ) { - constructor(verification: SignatureVerification, validationException: SignatureValidationException): - this(verification.signature, verification.signingKey, validationException) + constructor( + verification: SignatureVerification, + validationException: SignatureValidationException + ) : this(verification.signature, verification.signingKey, validationException) override fun toString(): String { return "Signature: ${SignatureUtils.getSignatureDigestPrefix(signature)}; Key: ${signingKey?.toString() ?: "null"}; Failure: ${validationException.message}" } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt index f6c5a454..73c10e8a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt @@ -4,6 +4,9 @@ package org.pgpainless.decryption_verification +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.MarkerPacket import org.bouncycastle.bcpg.Packet @@ -13,25 +16,21 @@ import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPOnePassSignature import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.OpenPgpPacket -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream /** - * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. - * Unfortunately we cannot simply override [BCPGInputStream.read] to tee the data out though, since - * [BCPGInputStream.readPacket] inconsistently calls a mix of [BCPGInputStream.read] and - * [InputStream.read] of the underlying stream. This would cause the second length byte to get swallowed up. + * Since we need to update signatures with data from the underlying stream, this class is used to + * tee out the data. Unfortunately we cannot simply override [BCPGInputStream.read] to tee the data + * out though, since [BCPGInputStream.readPacket] inconsistently calls a mix of + * [BCPGInputStream.read] and [InputStream.read] of the underlying stream. This would cause the + * second length byte to get swallowed up. * - * Therefore, this class delegates the teeing to an [DelayedTeeInputStream] which wraps the underlying - * stream. Since calling [BCPGInputStream.nextPacketTag] reads up to and including the next packets tag, - * we need to delay teeing out that byte to signature verifiers. - * Hence, the reading methods of the [TeeBCPGInputStream] handle pushing this byte to the output stream using + * Therefore, this class delegates the teeing to an [DelayedTeeInputStream] which wraps the + * underlying stream. Since calling [BCPGInputStream.nextPacketTag] reads up to and including the + * next packets tag, we need to delay teeing out that byte to signature verifiers. Hence, the + * reading methods of the [TeeBCPGInputStream] handle pushing this byte to the output stream using * [DelayedTeeInputStream.squeeze]. */ -class TeeBCPGInputStream( - inputStream: BCPGInputStream, - outputStream: OutputStream) { +class TeeBCPGInputStream(inputStream: BCPGInputStream, outputStream: OutputStream) { private val delayedTee: DelayedTeeInputStream private val packetInputStream: BCPGInputStream @@ -43,8 +42,7 @@ class TeeBCPGInputStream( fun nextPacketTag(): OpenPgpPacket? { return packetInputStream.nextPacketTag().let { - if (it == -1) null - else OpenPgpPacket.requireFromTag(it) + if (it == -1) null else OpenPgpPacket.requireFromTag(it) } } @@ -82,8 +80,8 @@ class TeeBCPGInputStream( } class DelayedTeeInputStream( - private val inputStream: InputStream, - private val outputStream: OutputStream + private val inputStream: InputStream, + private val outputStream: OutputStream ) : InputStream() { private var last: Int = -1 @@ -94,7 +92,7 @@ class TeeBCPGInputStream( return try { last = inputStream.read() last - } catch (e : IOException) { + } catch (e: IOException) { if (e.message?.contains("crc check failed in armored message") == true) { throw e } @@ -108,19 +106,18 @@ class TeeBCPGInputStream( } inputStream.read(b, off, len).let { r -> - last = if (r > 0) { - outputStream.write(b, off, r - 1) - b[off + r - 1].toInt() - } else { - -1 - } + last = + if (r > 0) { + outputStream.write(b, off, r - 1) + b[off + r - 1].toInt() + } else { + -1 + } return r } } - /** - * Squeeze the last byte out and update the output stream. - */ + /** Squeeze the last byte out and update the output stream. */ fun squeeze() { if (last != -1) { outputStream.write(last) @@ -133,4 +130,4 @@ class TeeBCPGInputStream( outputStream.close() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt index 7a3b93ee..78614a96 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/ClearsignedMessageUtil.kt @@ -4,47 +4,49 @@ package org.pgpainless.decryption_verification.cleartext_signatures +import java.io.* +import kotlin.jvm.Throws import org.bouncycastle.bcpg.ArmoredInputStream import org.bouncycastle.openpgp.PGPSignatureList import org.bouncycastle.util.Strings import org.pgpainless.exception.WrongConsumingMethodException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmoredInputStreamFactory -import java.io.* -import kotlin.jvm.Throws /** - * Utility class to deal with cleartext-signed messages. - * Based on Bouncycastle's [org.bouncycastle.openpgp.examples.ClearSignedFileProcessor]. + * Utility class to deal with cleartext-signed messages. Based on Bouncycastle's + * [org.bouncycastle.openpgp.examples.ClearSignedFileProcessor]. */ class ClearsignedMessageUtil { companion object { /** - * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided - * messageOutputStream. + * Dearmor a clearsigned message, detach the inband signatures and write the plaintext + * message to the provided messageOutputStream. * * @param clearsignedInputStream input stream containing a clearsigned message * @param messageOutputStream output stream to which the dearmored message shall be written * @return signatures - * * @throws IOException if the message is not clearsigned or some other IO error happens * @throws WrongConsumingMethodException in case the armored message is not cleartext signed */ @JvmStatic @Throws(WrongConsumingMethodException::class, IOException::class) fun detachSignaturesFromInbandClearsignedMessage( - clearsignedInputStream: InputStream, - messageOutputStream: OutputStream): PGPSignatureList { - val input: ArmoredInputStream = if (clearsignedInputStream is ArmoredInputStream) { - clearsignedInputStream - } else { - ArmoredInputStreamFactory.get(clearsignedInputStream) - } + clearsignedInputStream: InputStream, + messageOutputStream: OutputStream + ): PGPSignatureList { + val input: ArmoredInputStream = + if (clearsignedInputStream is ArmoredInputStream) { + clearsignedInputStream + } else { + ArmoredInputStreamFactory.get(clearsignedInputStream) + } if (!input.isClearText) { - throw WrongConsumingMethodException("Message isn't using the Cleartext Signature Framework.") + throw WrongConsumingMethodException( + "Message isn't using the Cleartext Signature Framework.") } BufferedOutputStream(messageOutputStream).use { output -> @@ -94,7 +96,11 @@ class ClearsignedMessageUtil { } @JvmStatic - private fun readInputLine(bOut: ByteArrayOutputStream, lookAhead: Int, fIn: InputStream): Int { + private fun readInputLine( + bOut: ByteArrayOutputStream, + lookAhead: Int, + fIn: InputStream + ): Int { var mLookAhead = lookAhead bOut.reset() var ch = mLookAhead @@ -150,4 +156,4 @@ class ClearsignedMessageUtil { return isLineEnding(b) || b == '\t'.code.toByte() || b == ' '.code.toByte() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt index eed6438f..da7c7cec 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/InMemoryMultiPassStrategy.kt @@ -8,12 +8,12 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream /** - * Implementation of the [MultiPassStrategy]. - * This class keeps the read data in memory by caching the data inside a [ByteArrayOutputStream]. + * Implementation of the [MultiPassStrategy]. This class keeps the read data in memory by caching + * the data inside a [ByteArrayOutputStream]. * - * Note, that this class is suitable and efficient for processing small amounts of data. - * For larger data like encrypted files, use of the [WriteToFileMultiPassStrategy] is recommended to - * prevent [OutOfMemoryError] and other issues. + * Note, that this class is suitable and efficient for processing small amounts of data. For larger + * data like encrypted files, use of the [WriteToFileMultiPassStrategy] is recommended to prevent + * [OutOfMemoryError] and other issues. */ class InMemoryMultiPassStrategy : MultiPassStrategy { @@ -26,4 +26,4 @@ class InMemoryMultiPassStrategy : MultiPassStrategy { get() = ByteArrayInputStream(getBytes()) fun getBytes(): ByteArray = messageOutputStream.toByteArray() -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt index ae69b9c5..4ef7ef3a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/MultiPassStrategy.kt @@ -7,16 +7,16 @@ package org.pgpainless.decryption_verification.cleartext_signatures import java.io.* /** - * Since for verification of cleartext signed messages, we need to read the whole data twice in order to verify signatures, - * a strategy for how to cache the read data is required. - * Otherwise, large data kept in memory could cause an [OutOfMemoryError] or other issues. + * Since for verification of cleartext signed messages, we need to read the whole data twice in + * order to verify signatures, a strategy for how to cache the read data is required. Otherwise, + * large data kept in memory could cause an [OutOfMemoryError] or other issues. * - * This is an Interface that describes a strategy to deal with the fact that detached signatures require multiple passes - * to do verification. + * This is an Interface that describes a strategy to deal with the fact that detached signatures + * require multiple passes to do verification. * - * This interface can be used to write the signed data stream out via [messageOutputStream] and later - * get access to the data again via [messageInputStream]. - * Thereby the detail where the data is being stored (memory, file, etc.) can be abstracted away. + * This interface can be used to write the signed data stream out via [messageOutputStream] and + * later get access to the data again via [messageInputStream]. Thereby the detail where the data is + * being stored (memory, file, etc.) can be abstracted away. */ interface MultiPassStrategy { @@ -32,8 +32,8 @@ interface MultiPassStrategy { * Provide an [InputStream] which contains the data that was previously written away in * [messageOutputStream]. * - * As there may be multiple signatures that need to be processed, each call of this method MUST return - * a new [InputStream]. + * As there may be multiple signatures that need to be processed, each call of this method MUST + * return a new [InputStream]. * * @return input stream * @throws IOException io error @@ -43,9 +43,10 @@ interface MultiPassStrategy { companion object { /** - * Write the message content out to a file and re-read it to verify signatures. - * This strategy is best suited for larger messages (e.g. plaintext signed files) which might not fit into memory. - * After the message has been processed completely, the messages content are available at the provided file. + * Write the message content out to a file and re-read it to verify signatures. This + * strategy is best suited for larger messages (e.g. plaintext signed files) which might not + * fit into memory. After the message has been processed completely, the messages content + * are available at the provided file. * * @param file target file * @return strategy @@ -56,10 +57,10 @@ interface MultiPassStrategy { } /** - * Read the message content into memory. - * This strategy is best suited for small messages which fit into memory. - * After the message has been processed completely, the message content can be accessed by calling - * [ByteArrayOutputStream.toByteArray] on [messageOutputStream]. + * Read the message content into memory. This strategy is best suited for small messages + * which fit into memory. After the message has been processed completely, the message + * content can be accessed by calling [ByteArrayOutputStream.toByteArray] on + * [messageOutputStream]. * * @return strategy */ @@ -68,4 +69,4 @@ interface MultiPassStrategy { return InMemoryMultiPassStrategy() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt index 5d6567a4..88fcf6f1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/cleartext_signatures/WriteToFileMultiPassStrategy.kt @@ -7,18 +7,15 @@ package org.pgpainless.decryption_verification.cleartext_signatures import java.io.* /** - * Implementation of the [MultiPassStrategy]. - * When processing signed data the first time, the data is being written out into a file. - * For the second pass, that file is being read again. + * Implementation of the [MultiPassStrategy]. When processing signed data the first time, the data + * is being written out into a file. For the second pass, that file is being read again. * - * This strategy is recommended when larger amounts of data need to be processed. - * For smaller files, [InMemoryMultiPassStrategy] yields higher efficiency. + * This strategy is recommended when larger amounts of data need to be processed. For smaller files, + * [InMemoryMultiPassStrategy] yields higher efficiency. * * @param file file to write the data to and read from */ -class WriteToFileMultiPassStrategy( - private val file: File -) : MultiPassStrategy { +class WriteToFileMultiPassStrategy(private val file: File) : MultiPassStrategy { override val messageOutputStream: OutputStream @Throws(IOException::class) @@ -39,5 +36,4 @@ class WriteToFileMultiPassStrategy( } return FileInputStream(file) } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt index 133bfcb3..2bea3356 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/InputSymbol.kt @@ -5,35 +5,26 @@ package org.pgpainless.decryption_verification.syntax_check enum class InputSymbol { - /** - * A [PGPLiteralData] packet. - */ + /** A [PGPLiteralData] packet. */ LITERAL_DATA, - /** - * A [PGPSignatureList] object. - */ + /** A [PGPSignatureList] object. */ SIGNATURE, - /** - * A [PGPOnePassSignatureList] object. - */ + /** A [PGPOnePassSignatureList] object. */ ONE_PASS_SIGNATURE, /** - * A [PGPCompressedData] packet. - * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify - * its nested packet sequence. + * A [PGPCompressedData] packet. The contents of this packet MUST form a valid OpenPGP message, + * so a nested PDA is opened to verify its nested packet sequence. */ COMPRESSED_DATA, /** - * A [PGPEncryptedDataList] object. - * This object combines multiple ESKs and the corresponding Symmetrically Encrypted - * (possibly Integrity Protected) Data packet. + * A [PGPEncryptedDataList] object. This object combines multiple ESKs and the corresponding + * Symmetrically Encrypted (possibly Integrity Protected) Data packet. */ ENCRYPTED_DATA, /** - * Marks the end of a (sub-) sequence. - * This input is given if the end of an OpenPGP message is reached. - * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents - * (e.g. the end of a Compressed Data packet). + * Marks the end of a (sub-) sequence. This input is given if the end of an OpenPGP message is + * reached. This might be the case for the end of the whole ciphertext, or the end of a packet + * with nested contents (e.g. the end of a Compressed Data packet). */ END_OF_SEQUENCE -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt index 56bd9a77..127d10f8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.kt @@ -9,9 +9,10 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException /** * This class describes the syntax for OpenPGP messages as specified by rfc4880. * - * See [rfc4880 - §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) - * See [Blog post about theoretic background and translation of grammar to PDA syntax](https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/) - * See [Blog post about practically implementing the PDA for packet syntax validation](https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/) + * See [rfc4880 - §11.3. OpenPGP Messages](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) See + * [Blog post about theoretic background and translation of grammar to PDA syntax](https://blog.jabberhead.tk/2022/09/14/using-pushdown-automata-to-verify-packet-sequences/) + * See + * [Blog post about practically implementing the PDA for packet syntax validation](https://blog.jabberhead.tk/2022/10/26/implementing-packet-sequence-validation-using-pushdown-automata/) */ class OpenPgpMessageSyntax : Syntax { @@ -33,10 +34,12 @@ class OpenPgpMessageSyntax : Syntax { return when (input) { InputSymbol.LITERAL_DATA -> Transition(State.LITERAL_MESSAGE) InputSymbol.SIGNATURE -> Transition(State.OPENPGP_MESSAGE, StackSymbol.MSG) - InputSymbol.ONE_PASS_SIGNATURE -> Transition(State.OPENPGP_MESSAGE, StackSymbol.OPS, StackSymbol.MSG) + InputSymbol.ONE_PASS_SIGNATURE -> + Transition(State.OPENPGP_MESSAGE, StackSymbol.OPS, StackSymbol.MSG) InputSymbol.COMPRESSED_DATA -> Transition(State.COMPRESSED_MESSAGE) InputSymbol.ENCRYPTED_DATA -> Transition(State.ENCRYPTED_MESSAGE) - InputSymbol.END_OF_SEQUENCE -> throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) + InputSymbol.END_OF_SEQUENCE -> + throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) else -> throw MalformedOpenPgpMessageException(State.OPENPGP_MESSAGE, input, stackItem) } } @@ -85,4 +88,4 @@ class OpenPgpMessageSyntax : Syntax { } throw MalformedOpenPgpMessageException(State.VALID, input, stackItem) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt index b1949917..7dff6ba2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/PDA.kt @@ -8,37 +8,41 @@ import org.pgpainless.exception.MalformedOpenPgpMessageException import org.slf4j.LoggerFactory /** - * Pushdown Automaton for validating context-free languages. - * In PGPainless, this class is used to validate OpenPGP message packet sequences against the allowed syntax. + * Pushdown Automaton for validating context-free languages. In PGPainless, this class is used to + * validate OpenPGP message packet sequences against the allowed syntax. * * See [OpenPGP Message Syntax](https://www.rfc-editor.org/rfc/rfc4880#section-11.3) */ -class PDA constructor( - private val syntax: Syntax, - private val stack: ArrayDeque, - private val inputs: MutableList, - private var state: State +class PDA +constructor( + private val syntax: Syntax, + private val stack: ArrayDeque, + private val inputs: MutableList, + private var state: State ) { /** - * Construct a PDA with a custom [Syntax], initial [State] and initial [StackSymbols][StackSymbol]. + * Construct a PDA with a custom [Syntax], initial [State] and initial + * [StackSymbols][StackSymbol]. * * @param syntax syntax * @param initialState initial state - * @param initialStack zero or more initial stack items (get pushed onto the stack in order of appearance) + * @param initialStack zero or more initial stack items (get pushed onto the stack in order of + * appearance) */ - constructor(syntax: Syntax, initialState: State, vararg initialStack: StackSymbol): this ( - syntax, ArrayDeque(initialStack.toList().reversed()), mutableListOf(), initialState) + constructor( + syntax: Syntax, + initialState: State, + vararg initialStack: StackSymbol + ) : this(syntax, ArrayDeque(initialStack.toList().reversed()), mutableListOf(), initialState) + + /** Default constructor which initializes the PDA to work with the [OpenPgpMessageSyntax]. */ + constructor() : + this(OpenPgpMessageSyntax(), State.OPENPGP_MESSAGE, StackSymbol.TERMINUS, StackSymbol.MSG) /** - * Default constructor which initializes the PDA to work with the [OpenPgpMessageSyntax]. - */ - constructor(): this(OpenPgpMessageSyntax(), State.OPENPGP_MESSAGE, StackSymbol.TERMINUS, StackSymbol.MSG) - - /** - * Process the next [InputSymbol]. - * This will either leave the PDA in the next state, or throw a [MalformedOpenPgpMessageException] if the - * input symbol is rejected. + * Process the next [InputSymbol]. This will either leave the PDA in the next state, or throw a + * [MalformedOpenPgpMessageException] if the input symbol is rejected. * * @param input input symbol * @throws MalformedOpenPgpMessageException if the input symbol is rejected @@ -53,14 +57,17 @@ class PDA constructor( } inputs.add(input) } catch (e: MalformedOpenPgpMessageException) { - val stackFormat = if (stackSymbol != null) { - "${stack.joinToString()}||$stackSymbol" - } else { - stack.joinToString() - } - val wrapped = MalformedOpenPgpMessageException( + val stackFormat = + if (stackSymbol != null) { + "${stack.joinToString()}||$stackSymbol" + } else { + stack.joinToString() + } + val wrapped = + MalformedOpenPgpMessageException( "Malformed message: After reading packet sequence ${inputs.joinToString()}, token '$input' is not allowed.\n" + - "No transition from state '$state' with stack $stackFormat", e) + "No transition from state '$state' with stack $stackFormat", + e) LOGGER.debug("Invalid input '$input'", wrapped) throw wrapped } @@ -87,7 +94,8 @@ class PDA constructor( */ fun assertValid() { if (!isValid()) { - throw MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: ${toString()}") + throw MalformedOpenPgpMessageException( + "Pushdown Automaton is not in an acceptable state: ${toString()}") } } @@ -114,7 +122,6 @@ class PDA constructor( } companion object { - @JvmStatic - private val LOGGER = LoggerFactory.getLogger(PDA::class.java) + @JvmStatic private val LOGGER = LoggerFactory.getLogger(PDA::class.java) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt index 960e5eba..8f927864 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/StackSymbol.kt @@ -5,16 +5,10 @@ package org.pgpainless.decryption_verification.syntax_check enum class StackSymbol { - /** - * OpenPGP Message. - */ + /** OpenPGP Message. */ MSG, - /** - * OnePassSignature (in case of BC this represents a OnePassSignatureList). - */ + /** OnePassSignature (in case of BC this represents a OnePassSignatureList). */ OPS, - /** - * Special symbol representing the end of the message. - */ + /** Special symbol representing the end of the message. */ TERMINUS -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt index 8e1f682c..8fad2fb7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/State.kt @@ -4,13 +4,11 @@ package org.pgpainless.decryption_verification.syntax_check -/** - * Set of states of the automaton. - */ +/** Set of states of the automaton. */ enum class State { OPENPGP_MESSAGE, LITERAL_MESSAGE, COMPRESSED_MESSAGE, ENCRYPTED_MESSAGE, VALID -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt index 16f0445b..a90e981e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Syntax.kt @@ -6,25 +6,24 @@ package org.pgpainless.decryption_verification.syntax_check import org.pgpainless.exception.MalformedOpenPgpMessageException -/** - * This interface can be used to define a custom syntax for the [PDA]. - */ +/** This interface can be used to define a custom syntax for the [PDA]. */ interface Syntax { /** * Describe a transition rule from [State]
from
for [InputSymbol]
input
- * with [StackSymbol]
stackItem
from the top of the [PDAs][PDA] stack. - * The resulting [Transition] contains the new [State], as well as a list of - * [StackSymbols][StackSymbol] that get pushed onto the stack by the transition rule. - * If there is no applicable rule, a [MalformedOpenPgpMessageException] is thrown, since in this case - * the [InputSymbol] must be considered illegal. + * with [StackSymbol]
stackItem
from the top of the [PDAs][PDA] stack. The resulting + * [Transition] contains the new [State], as well as a list of [StackSymbols][StackSymbol] that + * get pushed onto the stack by the transition rule. If there is no applicable rule, a + * [MalformedOpenPgpMessageException] is thrown, since in this case the [InputSymbol] must be + * considered illegal. * * @param from current state of the PDA * @param input input symbol * @param stackItem item that got popped from the top of the stack * @return applicable transition rule containing the new state and pushed stack symbols - * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input + * symbol is illegal) */ @Throws(MalformedOpenPgpMessageException::class) fun transition(from: State, input: InputSymbol, stackItem: StackSymbol?): Transition -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt index 5bc6dfc6..d0a09992 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/syntax_check/Transition.kt @@ -5,18 +5,18 @@ package org.pgpainless.decryption_verification.syntax_check /** - * Result of applying a transition rule. - * Transition rules can be described by implementing the [Syntax] interface. + * Result of applying a transition rule. Transition rules can be described by implementing the + * [Syntax] interface. * * @param newState new [State] that is reached by applying the transition. - * @param pushedItems list of [StackSymbol] that are pushed onto the stack by applying the transition. - * The list contains items in the order in which they are pushed onto the stack. - * The list may be empty. + * @param pushedItems list of [StackSymbol] that are pushed onto the stack by applying the + * transition. The list contains items in the order in which they are pushed onto the stack. The + * list may be empty. */ -class Transition private constructor( - val pushedItems: List, - val newState: State -) { +class Transition private constructor(val pushedItems: List, val newState: State) { - constructor(newState: State, vararg pushedItems: StackSymbol): this(pushedItems.toList(), newState) -} \ No newline at end of file + constructor( + newState: State, + vararg pushedItems: StackSymbol + ) : this(pushedItems.toList(), newState) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt index 90f275a4..68e41091 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import java.security.MessageDigest import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey @@ -13,20 +14,23 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator import org.pgpainless.PGPainless import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.protection.SecretKeyRingProtector -import java.security.MessageDigest class BcHashContextSigner { companion object { @JvmStatic - fun signHashContext(hashContext: MessageDigest, - signatureType: SignatureType, - secretKey: PGPSecretKeyRing, - protector: SecretKeyRingProtector): PGPSignature { + fun signHashContext( + hashContext: MessageDigest, + signatureType: SignatureType, + secretKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): PGPSignature { val info = PGPainless.inspectKeyRing(secretKey) - return info.signingSubkeys.mapNotNull { info.getSecretKey(it.keyID) }.firstOrNull() - ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } - ?: throw PGPException("Key does not contain suitable signing subkey.") + return info.signingSubkeys + .mapNotNull { info.getSecretKey(it.keyID) } + .firstOrNull() + ?.let { signHashContext(hashContext, signatureType, it.unlock(protector)) } + ?: throw PGPException("Key does not contain suitable signing subkey.") } /** @@ -38,12 +42,14 @@ class BcHashContextSigner { * @throws PGPException in case of an OpenPGP error */ @JvmStatic - internal fun signHashContext(hashContext: MessageDigest, - signatureType: SignatureType, - privateKey: PGPPrivateKey): PGPSignature { + internal fun signHashContext( + hashContext: MessageDigest, + signatureType: SignatureType, + privateKey: PGPPrivateKey + ): PGPSignature { return PGPSignatureGenerator(BcPGPHashContextContentSignerBuilder(hashContext)) - .apply { init(signatureType.code, privateKey) } - .generate() + .apply { init(signatureType.code, privateKey) } + .generate() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt index 381c4716..bf66b6dc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcPGPHashContextContentSignerBuilder.kt @@ -4,6 +4,8 @@ package org.pgpainless.encryption_signing +import java.io.OutputStream +import java.security.MessageDigest import org.bouncycastle.bcpg.PublicKeyAlgorithmTags import org.bouncycastle.crypto.CipherParameters import org.bouncycastle.crypto.CryptoException @@ -23,17 +25,14 @@ import org.bouncycastle.openpgp.operator.PGPContentSigner import org.bouncycastle.openpgp.operator.bc.BcPGPKeyConverter import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm -import java.io.OutputStream -import java.security.MessageDigest /** - * Implementation of [PGPContentSignerBuilder] using the BC API, which can be used to sign hash contexts. - * This can come in handy to sign data, which was already processed to calculate the hash context, without the - * need to process it again to calculate the OpenPGP signature. + * Implementation of [PGPContentSignerBuilder] using the BC API, which can be used to sign hash + * contexts. This can come in handy to sign data, which was already processed to calculate the hash + * context, without the need to process it again to calculate the OpenPGP signature. */ -class BcPGPHashContextContentSignerBuilder( - private val messageDigest: MessageDigest -) : PGPHashContextContentSignerBuilder() { +class BcPGPHashContextContentSignerBuilder(private val messageDigest: MessageDigest) : + PGPHashContextContentSignerBuilder() { private val keyConverter = BcPGPKeyConverter() private val _hashAlgorithm: HashAlgorithm @@ -50,15 +49,22 @@ class BcPGPHashContextContentSignerBuilder( return object : PGPContentSigner { override fun getOutputStream(): OutputStream = SignerOutputStream(signer) - override fun getSignature(): ByteArray = try { - signer.generateSignature() - } catch (e : CryptoException) { - throw IllegalStateException("unable to create signature.", e) - } + + override fun getSignature(): ByteArray = + try { + signer.generateSignature() + } catch (e: CryptoException) { + throw IllegalStateException("unable to create signature.", e) + } + override fun getDigest(): ByteArray = messageDigest.digest() + override fun getType(): Int = signatureType + override fun getHashAlgorithm(): Int = _hashAlgorithm.algorithmId + override fun getKeyAlgorithm(): Int = keyAlgorithm.algorithmId + override fun getKeyID(): Long = privateKey.keyID } } @@ -67,21 +73,25 @@ class BcPGPHashContextContentSignerBuilder( @JvmStatic private fun requireFromName(digestName: String): HashAlgorithm { val algorithm = HashAlgorithm.fromName(digestName) - require(algorithm != null) { "Cannot recognize OpenPGP Hash Algorithm: $digestName"} + require(algorithm != null) { "Cannot recognize OpenPGP Hash Algorithm: $digestName" } return algorithm } @JvmStatic - private fun createSigner(keyAlgorithm: PublicKeyAlgorithm, - messageDigest: MessageDigest, - keyParam: CipherParameters): Signer { + private fun createSigner( + keyAlgorithm: PublicKeyAlgorithm, + messageDigest: MessageDigest, + keyParam: CipherParameters + ): Signer { val staticDigest = ExistingMessageDigest(messageDigest) return when (keyAlgorithm.algorithmId) { - PublicKeyAlgorithmTags.RSA_GENERAL, PublicKeyAlgorithmTags.RSA_SIGN -> RSADigestSigner(staticDigest) + PublicKeyAlgorithmTags.RSA_GENERAL, + PublicKeyAlgorithmTags.RSA_SIGN -> RSADigestSigner(staticDigest) PublicKeyAlgorithmTags.DSA -> DSADigestSigner(DSASigner(), staticDigest) PublicKeyAlgorithmTags.ECDSA -> DSADigestSigner(ECDSASigner(), staticDigest) PublicKeyAlgorithmTags.EDDSA_LEGACY -> { - if (keyParam is Ed25519PrivateKeyParameters || keyParam is Ed25519PublicKeyParameters) + if (keyParam is Ed25519PrivateKeyParameters || + keyParam is Ed25519PublicKeyParameters) EdDsaSigner(Ed25519Signer(), staticDigest) else EdDsaSigner(Ed448Signer(byteArrayOf()), staticDigest) } @@ -91,10 +101,7 @@ class BcPGPHashContextContentSignerBuilder( } // Copied from BCs BcImplProvider - required since BCs class is package visible only :/ - internal class EdDsaSigner( - private val signer: Signer, - private val digest: Digest - ) : Signer { + internal class EdDsaSigner(private val signer: Signer, private val digest: Digest) : Signer { private val digBuf: ByteArray = ByteArray(digest.digestSize) override fun init(forSigning: Boolean, param: CipherParameters) { @@ -128,4 +135,4 @@ class BcPGPHashContextContentSignerBuilder( digest.reset() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt index 8735d7b1..badc8b48 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt @@ -5,17 +5,15 @@ package org.pgpainless.encryption_signing -import org.pgpainless.algorithm.StreamEncoding import java.io.OutputStream +import org.pgpainless.algorithm.StreamEncoding /** - * [OutputStream] which applies CR-LF encoding of its input data, based on the desired [StreamEncoding]. - * This implementation originates from the Bouncy Castle library. + * [OutputStream] which applies CR-LF encoding of its input data, based on the desired + * [StreamEncoding]. This implementation originates from the Bouncy Castle library. */ -class CRLFGeneratorStream( - private val crlfOut: OutputStream, - encoding: StreamEncoding -) : OutputStream() { +class CRLFGeneratorStream(private val crlfOut: OutputStream, encoding: StreamEncoding) : + OutputStream() { private val isBinary: Boolean private var lastB = 0 @@ -26,9 +24,9 @@ class CRLFGeneratorStream( override fun write(b: Int) { if (!isBinary) { - if (b == '\n'.code && lastB != '\r'.code) { // Unix + if (b == '\n'.code && lastB != '\r'.code) { // Unix crlfOut.write('\r'.code) - } else if (lastB == '\r'.code) { // MAC + } else if (lastB == '\r'.code) { // MAC if (b != '\n'.code) { crlfOut.write('\n'.code) } @@ -49,4 +47,4 @@ class CRLFGeneratorStream( super.flush() crlfOut.flush() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt index 324fcf3b..13785df4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt @@ -4,16 +4,18 @@ package org.pgpainless.encryption_signing +import java.io.OutputStream import org.pgpainless.PGPainless.Companion.getPolicy import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.OutputStream class EncryptionBuilder : EncryptionBuilderInterface { - override fun onOutputStream(outputStream: OutputStream): EncryptionBuilderInterface.WithOptions { + override fun onOutputStream( + outputStream: OutputStream + ): EncryptionBuilderInterface.WithOptions { return WithOptionsImpl(outputStream) } @@ -26,8 +28,7 @@ class EncryptionBuilder : EncryptionBuilderInterface { companion object { - @JvmStatic - val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java) + @JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java) /** * Negotiate the [SymmetricKeyAlgorithm] used for message encryption. @@ -36,24 +37,32 @@ class EncryptionBuilder : EncryptionBuilderInterface { * @return negotiated symmetric key algorithm */ @JvmStatic - fun negotiateSymmetricEncryptionAlgorithm(encryptionOptions: EncryptionOptions): SymmetricKeyAlgorithm { - val preferences = encryptionOptions.keyViews.values + fun negotiateSymmetricEncryptionAlgorithm( + encryptionOptions: EncryptionOptions + ): SymmetricKeyAlgorithm { + val preferences = + encryptionOptions.keyViews.values .map { it.preferredSymmetricKeyAlgorithms } .toList() - val algorithm = byPopularity().negotiate( - getPolicy().symmetricKeyEncryptionAlgorithmPolicy, - encryptionOptions.encryptionAlgorithmOverride, - preferences) - LOGGER.debug("Negotiation resulted in {} being the symmetric encryption algorithm of choice.", algorithm) + val algorithm = + byPopularity() + .negotiate( + getPolicy().symmetricKeyEncryptionAlgorithmPolicy, + encryptionOptions.encryptionAlgorithmOverride, + preferences) + LOGGER.debug( + "Negotiation resulted in {} being the symmetric encryption algorithm of choice.", + algorithm) return algorithm } @JvmStatic fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm { val compressionAlgorithmOverride = producerOptions.compressionAlgorithmOverride - return compressionAlgorithmOverride ?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm() + return compressionAlgorithmOverride + ?: getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm() // TODO: Negotiation } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt index 2d42c68a..586fbc6e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt @@ -4,9 +4,9 @@ package org.pgpainless.encryption_signing -import org.bouncycastle.openpgp.PGPException import java.io.IOException import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException fun interface EncryptionBuilderInterface { @@ -26,11 +26,11 @@ fun interface EncryptionBuilderInterface { * * @param options options * @return encryption stream - * * @throws PGPException if something goes wrong during encryption stream preparation - * @throws IOException if something goes wrong during encryption stream preparation (writing headers) + * @throws IOException if something goes wrong during encryption stream preparation (writing + * headers) */ @Throws(PGPException::class, IOException::class) fun withOptions(options: ProducerOptions): EncryptionStream } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index 00b1359a..3e232654 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import java.util.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator @@ -19,12 +20,8 @@ import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.info.KeyAccessor import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.util.Passphrase -import java.util.* - -class EncryptionOptions( - private val purpose: EncryptionPurpose -) { +class EncryptionOptions(private val purpose: EncryptionPurpose) { private val _encryptionMethods: MutableSet = mutableSetOf() private val _encryptionKeyIdentifiers: MutableSet = mutableSetOf() private val _keyRingInfo: MutableMap = mutableMapOf() @@ -37,16 +34,20 @@ class EncryptionOptions( val encryptionMethods get() = _encryptionMethods.toSet() + val encryptionKeyIdentifiers get() = _encryptionKeyIdentifiers.toSet() + val keyRingInfo get() = _keyRingInfo.toMap() + val keyViews get() = _keyViews.toMap() + val encryptionAlgorithmOverride get() = _encryptionAlgorithmOverride - constructor(): this(EncryptionPurpose.ANY) + constructor() : this(EncryptionPurpose.ANY) /** * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys @@ -54,54 +55,56 @@ class EncryptionOptions( * * @return encryption options */ - fun setEvaluationDate(evaluationDate: Date) = apply { - this.evaluationDate = evaluationDate - } + fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate } /** - * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for - * identifiable bindings. - * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. + * Identify authenticatable certificates for the given user-ID by querying the {@link + * CertificateAuthority} for identifiable bindings. Add all acceptable bindings, whose trust + * amount is larger or equal to the target amount to the list of recipients. + * * @param userId userId - * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address + * @param email if true, treat the user-ID as an email address and match all user-IDs containing + * the mail address * @param authority certificate authority - * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) + * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, 60 + * = partially authenticated...) * @return encryption options */ @JvmOverloads - fun addAuthenticatableRecipients(userId: String, email: Boolean, authority: CertificateAuthority, targetAmount: Int = 120) = apply { + fun addAuthenticatableRecipients( + userId: String, + email: Boolean, + authority: CertificateAuthority, + targetAmount: Int = 120 + ) = apply { var foundAcceptable = false - authority.lookupByUserId(userId, email, evaluationDate, targetAmount) - .filter { it.isAuthenticated() } - .forEach { addRecipient(it.certificate) - .also { - foundAcceptable = true - } - } + authority + .lookupByUserId(userId, email, evaluationDate, targetAmount) + .filter { it.isAuthenticated() } + .forEach { addRecipient(it.certificate).also { foundAcceptable = true } } require(foundAcceptable) { "Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount." } } /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) + * as recipients. * * @param keys keys * @return this */ fun addRecipients(keys: Iterable) = apply { keys.toList().let { - require(it.isNotEmpty()) { - "Set of recipient keys cannot be empty." - } + require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } it.forEach { key -> addRecipient(key) } } } /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * Per key ring, the selector is applied to select one or more encryption subkeys. + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) + * as recipients. Per key ring, the selector is applied to select one or more encryption + * subkeys. * * @param keys keys * @param selector encryption key selector @@ -109,9 +112,7 @@ class EncryptionOptions( */ fun addRecipients(keys: Iterable, selector: EncryptionKeySelector) = apply { keys.toList().let { - require(it.isNotEmpty()) { - "Set of recipient keys cannot be empty." - } + require(it.isNotEmpty()) { "Set of recipient keys cannot be empty." } it.forEach { key -> addRecipient(key, selector) } } } @@ -125,19 +126,25 @@ class EncryptionOptions( fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector) /** - * Add a recipient by providing a key and recipient user-id. - * The user-id is used to determine the recipients preferences (algorithms etc.). + * 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 * @return this */ fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = - addRecipient(key, userId, encryptionKeySelector) + addRecipient(key, userId, encryptionKeySelector) - fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence, encryptionKeySelector: EncryptionKeySelector) = apply { + fun addRecipient( + key: PGPPublicKeyRing, + userId: CharSequence, + encryptionKeySelector: EncryptionKeySelector + ) = apply { val info = KeyRingInfo(key, evaluationDate) - val subkeys = encryptionKeySelector.selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)) + val subkeys = + encryptionKeySelector.selectEncryptionSubkeys( + info.getEncryptionSubkeys(userId, purpose)) if (subkeys.isEmpty()) { throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) } @@ -155,17 +162,23 @@ class EncryptionOptions( } @JvmOverloads - fun addHiddenRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector = encryptionKeySelector) = apply { - addAsRecipient(key, selector, true) - } + fun addHiddenRecipient( + key: PGPPublicKeyRing, + selector: EncryptionKeySelector = encryptionKeySelector + ) = apply { addAsRecipient(key, selector, true) } - private fun addAsRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector, wildcardKeyId: Boolean) = apply { + private fun addAsRecipient( + key: PGPPublicKeyRing, + selector: EncryptionKeySelector, + wildcardKeyId: Boolean + ) = apply { val info = KeyRingInfo(key, evaluationDate) - val primaryKeyExpiration = try { - info.primaryKeyExpirationDate - } catch (e: NoSuchElementException) { - throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) - } + val primaryKeyExpiration = + try { + info.primaryKeyExpirationDate + } catch (e: NoSuchElementException) { + throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) + } if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) @@ -174,10 +187,12 @@ class EncryptionOptions( var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) // There are some legacy keys around without key flags. - // If we allow encryption for those keys, we add valid keys without any key flags, if they are + // If we allow encryption for those keys, we add valid keys without any key flags, if they + // are // capable of encryption by means of their algorithm if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { - encryptionSubkeys = info.validSubkeys + encryptionSubkeys = + info.validSubkeys .filter { it.isEncryptionKey } .filter { info.getKeyFlagsOf(it.keyID).isEmpty() } } @@ -194,13 +209,16 @@ class EncryptionOptions( } } - private fun addRecipientKey(certificate: PGPPublicKeyRing, - key: PGPPublicKey, - wildcardKeyId: Boolean) { + private fun addRecipientKey( + certificate: PGPPublicKeyRing, + key: PGPPublicKey, + wildcardKeyId: Boolean + ) { _encryptionKeyIdentifiers.add(SubkeyIdentifier(certificate, key.keyID)) - addEncryptionMethod(ImplementationFactory.getInstance() - .getPublicKeyKeyEncryptionMethodGenerator(key) - .also { it.setUseWildcardKeyID(wildcardKeyId) }) + addEncryptionMethod( + ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(key).also { + it.setUseWildcardKeyID(wildcardKeyId) + }) } /** @@ -210,19 +228,19 @@ class EncryptionOptions( * @return this */ fun addPassphrase(passphrase: Passphrase) = apply { - require(!passphrase.isEmpty) { - "Passphrase MUST NOT be empty." - } - addEncryptionMethod(ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) + require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." } + addEncryptionMethod( + ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) } /** * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. - * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) - * or {@link PGPKeyEncryptionMethodGenerator} (public key). + * 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. + * 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 * @return this @@ -232,10 +250,9 @@ class EncryptionOptions( } /** - * Override the used symmetric encryption algorithm. - * The symmetric encryption algorithm is used to encrypt the message itself, - * while the used symmetric key will be encrypted to all recipients using public key - * cryptography. + * Override the used symmetric encryption algorithm. The symmetric encryption algorithm is used + * to encrypt the message itself, while the used symmetric key will be encrypted to all + * recipients using public key cryptography. * * If the algorithm is not overridden, a suitable algorithm will be negotiated. * @@ -250,10 +267,10 @@ class EncryptionOptions( } /** - * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption - * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. - * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm - * type to convey the subkeys use. + * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will + * allow encryption for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} + * subpacket. This is a workaround for dealing with legacy keys that have no key flags subpacket + * but rely on the key algorithm type to convey the subkeys use. * * @return this */ @@ -263,20 +280,16 @@ class EncryptionOptions( fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() - fun interface EncryptionKeySelector { fun selectEncryptionSubkeys(encryptionCapableKeys: List): List } companion object { - @JvmStatic - fun get() = EncryptionOptions() + @JvmStatic fun get() = EncryptionOptions() - @JvmStatic - fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) + @JvmStatic fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) - @JvmStatic - fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) + @JvmStatic fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) /** * Only encrypt to the first valid encryption capable subkey we stumble upon. @@ -285,7 +298,8 @@ class EncryptionOptions( */ @JvmStatic fun encryptToFirstSubkey() = EncryptionKeySelector { encryptionCapableKeys -> - encryptionCapableKeys.firstOrNull()?.let { listOf(it) } ?: listOf() } + encryptionCapableKeys.firstOrNull()?.let { listOf(it) } ?: listOf() + } /** * Encrypt to any valid, encryption capable subkey on the key ring. @@ -293,6 +307,8 @@ class EncryptionOptions( * @return encryption key selector */ @JvmStatic - fun encryptToAllCapableSubkeys() = EncryptionKeySelector { encryptionCapableKeys -> encryptionCapableKeys } + fun encryptToAllCapableSubkeys() = EncryptionKeySelector { encryptionCapableKeys -> + encryptionCapableKeys + } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index 0e9a40c9..24d89191 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import java.util.* import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing @@ -13,21 +14,20 @@ import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.MultiMap -import java.util.* data class EncryptionResult( - val encryptionAlgorithm: SymmetricKeyAlgorithm, - val compressionAlgorithm: CompressionAlgorithm, - val detachedSignatures: MultiMap, - val recipients: Set, - val fileName: String, - val modificationDate: Date, - val fileEncoding: StreamEncoding + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val compressionAlgorithm: CompressionAlgorithm, + val detachedSignatures: MultiMap, + val recipients: Set, + val fileName: String, + val modificationDate: Date, + val fileEncoding: StreamEncoding ) { /** - * Return true, if the message is marked as for-your-eyes-only. - * This is typically done by setting the filename "_CONSOLE". + * Return true, if the message is marked as for-your-eyes-only. This is typically done by + * setting the filename "_CONSOLE". * * @return is message for your eyes only? */ @@ -48,8 +48,7 @@ data class EncryptionResult( * * @return builder */ - @JvmStatic - fun builder() = Builder() + @JvmStatic fun builder() = Builder() } class Builder { @@ -70,32 +69,35 @@ data class EncryptionResult( _compressionAlgorithm = compressionAlgorithm } - fun setFileName(fileName: String) = apply { - _fileName = fileName - } + fun setFileName(fileName: String) = apply { _fileName = fileName } fun setModificationDate(modificationDate: Date) = apply { _modificationDate = modificationDate } - fun setFileEncoding(encoding: StreamEncoding) = apply { - _encoding = encoding - } + fun setFileEncoding(encoding: StreamEncoding) = apply { _encoding = encoding } fun addRecipient(recipient: SubkeyIdentifier) = apply { (recipients as MutableSet).add(recipient) } - fun addDetachedSignature(signingSubkeyIdentifier: SubkeyIdentifier, detachedSignature: PGPSignature) = apply { - detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) - } + fun addDetachedSignature( + signingSubkeyIdentifier: SubkeyIdentifier, + detachedSignature: PGPSignature + ) = apply { detachedSignatures.put(signingSubkeyIdentifier, detachedSignature) } fun build(): EncryptionResult { checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } - return EncryptionResult(_encryptionAlgorithm!!, _compressionAlgorithm!!, detachedSignatures, recipients, - _fileName, _modificationDate, _encoding) + return EncryptionResult( + _encryptionAlgorithm!!, + _compressionAlgorithm!!, + detachedSignatures, + recipients, + _fileName, + _modificationDate, + _encoding) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt index 50397905..f2617c34 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -4,6 +4,9 @@ package org.pgpainless.encryption_signing +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.OutputStream import org.bouncycastle.bcpg.ArmoredOutputStream import org.bouncycastle.bcpg.BCPGOutputStream import org.bouncycastle.openpgp.PGPCompressedDataGenerator @@ -16,9 +19,6 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmoredOutputStreamFactory import org.slf4j.LoggerFactory -import java.io.BufferedOutputStream -import java.io.IOException -import java.io.OutputStream // 1 << 8 causes wrong partial body length encoding // 1 << 9 fixes this. @@ -30,11 +30,13 @@ const val BUFFER_SIZE = 1 shl 9 * depending on its configuration. * * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. - * @see Source + * + * @see Source */ class EncryptionStream( - private var outermostStream: OutputStream, - private val options: ProducerOptions, + private var outermostStream: OutputStream, + private val options: ProducerOptions, ) : OutputStream() { private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder() @@ -66,8 +68,8 @@ class EncryptionStream( outermostStream = BufferedOutputStream(outermostStream) LOGGER.debug("Wrap encryption output in ASCII armor.") - armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream, options) - .also { outermostStream = it } + armorOutputStream = + ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it } } @Throws(IOException::class, PGPException::class) @@ -84,9 +86,11 @@ class EncryptionStream( EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let { resultBuilder.setEncryptionAlgorithm(it) LOGGER.debug("Encrypt message using symmetric algorithm $it.") - val encryptedDataGenerator = PGPEncryptedDataGenerator( - ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it) - .apply { setWithIntegrityPacket(true) }) + val encryptedDataGenerator = + PGPEncryptedDataGenerator( + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(it).apply { + setWithIntegrityPacket(true) + }) options.encryptionOptions.encryptionMethods.forEach { m -> encryptedDataGenerator.addMethod(m) } @@ -94,8 +98,11 @@ class EncryptionStream( resultBuilder.addRecipient(r) } - publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)) - .also { stream -> outermostStream = stream } + publicKeyEncryptedStream = + encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream + -> + outermostStream = stream + } } } @@ -107,8 +114,10 @@ class EncryptionStream( if (it == CompressionAlgorithm.UNCOMPRESSED) return LOGGER.debug("Compress using $it.") - basicCompressionStream = BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)) - .also { stream -> outermostStream = stream } + basicCompressionStream = + BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream -> + outermostStream = stream + } } } @@ -138,12 +147,17 @@ class EncryptionStream( return } - literalDataGenerator = PGPLiteralDataGenerator().also { gen -> - literalDataStream = gen.open(outermostStream, options.encoding.code, options.fileName, - options.modificationDate, ByteArray(BUFFER_SIZE)).also { stream -> - outermostStream = stream + literalDataGenerator = + PGPLiteralDataGenerator().also { gen -> + literalDataStream = + gen.open( + outermostStream, + options.encoding.code, + options.fileName, + options.modificationDate, + ByteArray(BUFFER_SIZE)) + .also { stream -> outermostStream = stream } } - } resultBuilder.apply { setFileName(options.fileName) setModificationDate(options.modificationDate) @@ -156,39 +170,47 @@ class EncryptionStream( } private fun prepareInputEncoding() { - outermostStream = CRLFGeneratorStream( + outermostStream = + CRLFGeneratorStream( // By buffering here, we drastically improve performance - // Reason is that CRLFGeneratorStream only implements write(int), so we need BufferedOutputStream to + // Reason is that CRLFGeneratorStream only implements write(int), so we need + // BufferedOutputStream to // "convert" to write(buf) calls again BufferedOutputStream(outermostStream), if (options.isApplyCRLFEncoding) StreamEncoding.UTF8 else StreamEncoding.BINARY) } private fun collectHashAlgorithmsForCleartextSigning(): Array { - return options.signingOptions?.signingMethods?.values - ?.map { it.hashAlgorithm }?.toSet() - ?.map { it.algorithmId }?.toTypedArray() - ?: arrayOf() + return options.signingOptions + ?.signingMethods + ?.values + ?.map { it.hashAlgorithm } + ?.toSet() + ?.map { it.algorithmId } + ?.toTypedArray() + ?: arrayOf() } - @Throws(IOException::class) - override fun write(data: Int) = outermostStream.write(data) + @Throws(IOException::class) override fun write(data: Int) = outermostStream.write(data) @Throws(IOException::class) override fun write(buffer: ByteArray) = write(buffer, 0, buffer.size) @Throws(IOException::class) - override fun write(buffer: ByteArray, off: Int, len: Int) = outermostStream.write(buffer, off, len) + override fun write(buffer: ByteArray, off: Int, len: Int) = + outermostStream.write(buffer, off, len) - @Throws(IOException::class) - override fun flush() = outermostStream.flush() + @Throws(IOException::class) override fun flush() = outermostStream.flush() @Throws(IOException::class) override fun close() { if (closed) return outermostStream.close() - literalDataStream?.apply { flush(); close() } + literalDataStream?.apply { + flush() + close() + } literalDataGenerator?.close() if (options.isCleartextSigned) { @@ -201,7 +223,7 @@ class EncryptionStream( try { writeSignatures() - } catch (e : PGPException) { + } catch (e: PGPException) { throw IOException("Exception while writing signatures.", e) } @@ -238,14 +260,14 @@ class EncryptionStream( } val result: EncryptionResult - get() = check(closed) { "EncryptionStream must be closed before accessing the result." } + get() = + check(closed) { "EncryptionStream must be closed before accessing the result." } .let { resultBuilder.build() } val isClosed get() = closed companion object { - @JvmStatic - private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) + @JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt index aa3dd58c..ef8641a7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/PGPHashContextContentSignerBuilder.kt @@ -4,39 +4,43 @@ package org.pgpainless.encryption_signing +import java.io.OutputStream +import java.security.MessageDigest import org.bouncycastle.crypto.Digest import org.bouncycastle.crypto.Signer import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder -import java.io.OutputStream -import java.security.MessageDigest abstract class PGPHashContextContentSignerBuilder : PGPContentSignerBuilder { // Copied from BC, required since BCs class is package visible only - internal class SignerOutputStream( - private val signer: Signer - ) : OutputStream() { + internal class SignerOutputStream(private val signer: Signer) : OutputStream() { override fun write(p0: Int) = signer.update(p0.toByte()) + override fun write(b: ByteArray) = signer.update(b, 0, b.size) + override fun write(b: ByteArray, off: Int, len: Int) = signer.update(b, off, len) } - internal class ExistingMessageDigest( - private val digest: MessageDigest - ) : Digest { + internal class ExistingMessageDigest(private val digest: MessageDigest) : Digest { override fun getAlgorithmName(): String = digest.algorithm + override fun getDigestSize(): Int = digest.digestLength + override fun update(b: Byte) = digest.update(b) + override fun update(buf: ByteArray, inOff: Int, len: Int) = digest.update(buf) + override fun doFinal(out: ByteArray, outOff: Int): Int { digest.digest().copyInto(out, outOff) return digestSize } + override fun reset() { // Nope! - // We cannot reset, since BCs signer classes are resetting in their init() methods, which would also reset + // We cannot reset, since BCs signer classes are resetting in their init() methods, + // which would also reset // the messageDigest, losing its state. This would shatter our intention. } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt index 88345fd9..871f8950 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -4,15 +4,17 @@ package org.pgpainless.encryption_signing +import java.util.* import org.bouncycastle.openpgp.PGPLiteralData import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding -import java.util.* -class ProducerOptions private constructor( - val encryptionOptions: EncryptionOptions?, - val signingOptions: SigningOptions?) { +class ProducerOptions +private constructor( + val encryptionOptions: EncryptionOptions?, + val signingOptions: SigningOptions? +) { private var _fileName: String = "" private var _modificationDate: Date = PGPLiteralData.NOW @@ -22,14 +24,15 @@ class ProducerOptions private constructor( private var _hideArmorHeaders = false var isDisableAsciiArmorCRC = false - private var _compressionAlgorithmOverride: CompressionAlgorithm = PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm + private var _compressionAlgorithmOverride: CompressionAlgorithm = + PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm private var asciiArmor = true private var _comment: String? = null private var _version: String? = null /** - * Specify, whether the result of the encryption/signing operation shall be ascii armored. - * The default value is true. + * Specify, whether the result of the encryption/signing operation shall be ascii armored. The + * default value is true. * * @param asciiArmor ascii armor * @return builder @@ -50,19 +53,15 @@ class ProducerOptions private constructor( get() = asciiArmor /** - * Set the comment header in ASCII armored output. - * The default value is null, which means no comment header is added. - * Multiline comments are possible using '\\n'. - *
- * Note: If a default header comment is set using [org.pgpainless.util.ArmoredOutputStreamFactory.setComment], - * then both comments will be written to the produced ASCII armor. + * Set the comment header in ASCII armored output. The default value is null, which means no + * comment header is added. Multiline comments are possible using '\\n'.
Note: If a default + * header comment is set using [org.pgpainless.util.ArmoredOutputStreamFactory.setComment], then + * both comments will be written to the produced ASCII armor. * * @param comment comment header text * @return builder */ - fun setComment(comment: String?) = apply { - _comment = comment - } + fun setComment(comment: String?) = apply { _comment = comment } /** * Return comment set for header in ascii armored output. @@ -80,18 +79,15 @@ class ProducerOptions private constructor( fun hasComment() = _comment != null /** - * Set the version header in ASCII armored output. - * The default value is null, which means no version header is added. - *
- * Note: If the value is non-null, then this method overrides the default version header set using + * Set the version header in ASCII armored output. The default value is null, which means no + * version header is added.
Note: If the value is non-null, then this method overrides the + * default version header set using * [org.pgpainless.util.ArmoredOutputStreamFactory.setVersionInfo]. * * @param version version header, or null for no version info. * @return builder */ - fun setVersion(version: String?) = apply { - _version = version - } + fun setVersion(version: String?) = apply { _version = version } /** * Return the version info header in ascii armored output. @@ -128,15 +124,13 @@ class ProducerOptions private constructor( get() = cleartextSigned /** - * Set the name of the encrypted file. - * Note: This option cannot be used simultaneously with [setForYourEyesOnly]. + * Set the name of the encrypted file. Note: This option cannot be used simultaneously with + * [setForYourEyesOnly]. * * @param fileName name of the encrypted file * @return this */ - fun setFileName(fileName: String) = apply { - _fileName = fileName - } + fun setFileName(fileName: String) = apply { _fileName = fileName } /** * Return the encrypted files name. @@ -147,17 +141,15 @@ class ProducerOptions private constructor( get() = _fileName /** - * Mark the encrypted message as for-your-eyes-only by setting a special file name. - * Note: Therefore this method cannot be used simultaneously with [setFileName]. + * Mark the encrypted message as for-your-eyes-only by setting a special file name. Note: + * Therefore this method cannot be used simultaneously with [setFileName]. * * @return this - * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this special filename in - * newly generated literal data packets + * @deprecated deprecated since at least crypto-refresh-05. It is not recommended using this + * special filename in newly generated literal data packets */ @Deprecated("Signaling using special file name is discouraged.") - fun setForYourEyesOnly() = apply { - _fileName = PGPLiteralData.CONSOLE - } + fun setForYourEyesOnly() = apply { _fileName = PGPLiteralData.CONSOLE } /** * Set the modification date of the encrypted file. @@ -165,9 +157,7 @@ class ProducerOptions private constructor( * @param modificationDate Modification date of the encrypted file. * @return this */ - fun setModificationDate(modificationDate: Date) = apply { - _modificationDate = modificationDate - } + fun setModificationDate(modificationDate: Date) = apply { _modificationDate = modificationDate } /** * Return the modification date of the encrypted file. @@ -178,40 +168,32 @@ class ProducerOptions private constructor( get() = _modificationDate /** - * Set format metadata field of the literal data packet. - * Defaults to [StreamEncoding.BINARY]. - *
- * This does not change the encoding of the wrapped data itself. - * To apply CR/LF encoding to your input data before processing, use [applyCRLFEncoding] instead. - * - * @see RFC4880 §5.9. Literal Data Packet + * Set format metadata field of the literal data packet. Defaults to [StreamEncoding.BINARY]. + *
This does not change the encoding of the wrapped data itself. To apply CR/LF encoding to + * your input data before processing, use [applyCRLFEncoding] instead. * * @param encoding encoding * @return this - * - * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are discouraged. + * @see RFC4880 §5.9. + * Literal Data Packet + * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are + * discouraged. */ @Deprecated("Options other than BINARY are discouraged.") - fun setEncoding(encoding: StreamEncoding) = apply { - encodingField = encoding - } + fun setEncoding(encoding: StreamEncoding) = apply { encodingField = encoding } val encoding: StreamEncoding get() = encodingField /** - * Apply special encoding of line endings to the input data. - * By default, this is disabled, which means that the data is not altered. - *
- * Enabling it will change the line endings to CR/LF. - * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in - * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". + * Apply special encoding of line endings to the input data. By default, this is disabled, which + * means that the data is not altered.
Enabling it will change the line endings to CR/LF. + * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will + * result in the identity "decrypt(encrypt(data)) == data == verify(sign(data))". * * @return this */ - fun applyCRLFEncoding() = apply { - applyCRLFEncoding = true - } + fun applyCRLFEncoding() = apply { applyCRLFEncoding = true } /** * Return the input encoding that will be applied before signing / encryption. @@ -239,9 +221,8 @@ class ProducerOptions private constructor( /** * If set to `true`, armor headers like version or comments will be omitted from armored output. - * By default, armor headers are not hidden. - * Note: If comments are added via [setComment], those are not omitted, even if - * [hideArmorHeaders] is set to `true`. + * By default, armor headers are not hidden. Note: If comments are added via [setComment], those + * are not omitted, even if [hideArmorHeaders] is set to `true`. * * @param hideArmorHeaders true or false * @return this @@ -260,7 +241,7 @@ class ProducerOptions private constructor( */ @JvmStatic fun signAndEncrypt(encryptionOptions: EncryptionOptions, signingOptions: SigningOptions) = - ProducerOptions(encryptionOptions, signingOptions) + ProducerOptions(encryptionOptions, signingOptions) /** * Sign some data without encryption. @@ -268,8 +249,7 @@ class ProducerOptions private constructor( * @param signingOptions signing options * @return builder */ - @JvmStatic - fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) + @JvmStatic fun sign(signingOptions: SigningOptions) = ProducerOptions(null, signingOptions) /** * Encrypt some data without signing. @@ -281,12 +261,10 @@ class ProducerOptions private constructor( fun encrypt(encryptionOptions: EncryptionOptions) = ProducerOptions(encryptionOptions, null) /** - * Only wrap the data in an OpenPGP packet. - * No encryption or signing will be applied. + * Only wrap the data in an OpenPGP packet. No encryption or signing will be applied. * * @return builder */ - @JvmStatic - fun noEncryptionNoSigning() = ProducerOptions(null, null) + @JvmStatic fun noEncryptionNoSigning() = ProducerOptions(null, null) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt index d0f04851..b2d53dcd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SignatureGenerationStream.kt @@ -6,23 +6,20 @@ package org.pgpainless.encryption_signing import java.io.OutputStream -/** - * OutputStream which has the task of updating signature generators for written data. - */ +/** OutputStream which has the task of updating signature generators for written data. */ class SignatureGenerationStream( - private val wrapped: OutputStream, - private val options: SigningOptions? + private val wrapped: OutputStream, + private val options: SigningOptions? ) : OutputStream() { override fun close() = wrapped.close() + override fun flush() = wrapped.flush() override fun write(b: Int) { wrapped.write(b) options?.run { - signingMethods.values.forEach { - it.signatureGenerator.update((b and 0xff).toByte()) - } + signingMethods.values.forEach { it.signatureGenerator.update((b and 0xff).toByte()) } } } @@ -30,10 +27,6 @@ class SignatureGenerationStream( override fun write(b: ByteArray, off: Int, len: Int) { wrapped.write(b, off, len) - options?.run { - signingMethods.values.forEach { - it.signatureGenerator.update(b, off, len) - } - } + options?.run { signingMethods.values.forEach { it.signatureGenerator.update(b, off, len) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt index 512e074b..089b2d28 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -4,6 +4,7 @@ package org.pgpainless.encryption_signing +import java.util.* import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless.Companion.getPolicy @@ -22,7 +23,6 @@ import org.pgpainless.policy.Policy import org.pgpainless.signature.subpackets.BaseSignatureSubpackets.Callback import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper -import java.util.* class SigningOptions { @@ -34,10 +34,10 @@ class SigningOptions { get() = _hashAlgorithmOverride /** - * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. - * If no override has been set, an acceptable algorithm will be negotiated instead. - * Note: To override the hash algorithm for signing, call this method *before* calling - * [addInlineSignature] or [addDetachedSignature]. + * Override hash algorithm negotiation by dictating which hash algorithm needs to be used. If no + * override has been set, an acceptable algorithm will be negotiated instead. Note: To override + * the hash algorithm for signing, call this method *before* calling [addInlineSignature] or + * [addDetachedSignature]. * * @param hashAlgorithmOverride override hash algorithm * @return this @@ -55,9 +55,7 @@ class SigningOptions { * @param evaluationDate new evaluation date * @return this */ - fun setEvaluationDate(evaluationDate: Date) = apply { - _evaluationDate = evaluationDate - } + fun setEvaluationDate(evaluationDate: Date) = apply { _evaluationDate = evaluationDate } /** * Sign the message using an inline signature made by the provided signing key. @@ -65,14 +63,15 @@ class SigningOptions { * @param signingKeyProtector protector to unlock the signing key * @param signingKey key ring containing the signing key * @return this - * * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or a signing method cannot be created */ @Throws(KeyException::class, PGPException::class) - fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = apply { - addInlineSignature(signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) - } + fun addSignature(signingKeyProtector: SecretKeyRingProtector, signingKey: PGPSecretKeyRing) = + apply { + addInlineSignature( + signingKeyProtector, signingKey, null, DocumentSignatureType.BINARY_DOCUMENT) + } /** * Add inline signatures with all secret key rings in the provided secret key ring collection. @@ -81,44 +80,41 @@ class SigningOptions { * @param signingKeys collection of signing keys * @param signatureType type of signature (binary, canonical text) * @return this - * * @throws KeyException if something is wrong with any of the keys - * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be created + * @throws PGPException if any of the keys cannot be unlocked or a signing method cannot be + * created */ @Throws(KeyException::class, PGPException::class) - fun addInlineSignatures(signingKeyProtector: SecretKeyRingProtector, - signingKeys: Iterable, - signatureType: DocumentSignatureType) = apply { - signingKeys.forEach { - addInlineSignature(signingKeyProtector, it, null, signatureType) - } + fun addInlineSignatures( + signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType + ) = apply { + signingKeys.forEach { addInlineSignature(signingKeyProtector, it, null, 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. + * 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 signingKeyProtector decryptor to unlock the signing secret key * @param signingKey signing key * @param signatureType type of signature (binary, canonical text) * @return this - * * @throws KeyException if something is wrong with the key * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ @Throws(KeyException::class, PGPException::class) - fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - signatureType: DocumentSignatureType) = apply { - addInlineSignature(signingKeyProtector, signingKey, null, signatureType) - } + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType + ) = apply { addInlineSignature(signingKeyProtector, signingKey, null, 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. + * 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. * @@ -126,27 +122,28 @@ class SigningOptions { * @param signingKey signing key * @param userId user-id of the signer * @param signatureType signature type (binary, canonical text) - * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the signature + * @param subpacketsCallback callback to modify the hashed and unhashed subpackets of the + * signature * @return this - * * @throws KeyException if the key is invalid * @throws PGPException if the key cannot be unlocked or the signing method cannot be created */ @Throws(KeyException::class, PGPException::class) @JvmOverloads - fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - userId: CharSequence? = null, - signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, - subpacketsCallback: Callback? = null) = apply { + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: CharSequence? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw UnboundUserIdException( - of(signingKey), - userId.toString(), - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ) + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId)) } val signingPubKeys = keyRingInfo.signingSubkeys @@ -155,14 +152,16 @@ class SigningOptions { } for (signingPubKey in signingPubKeys) { - val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = - if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) - else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) } } @@ -175,17 +174,22 @@ class SigningOptions { * @param signatureType signature type * @param subpacketsCallback callback to modify the signatures subpackets * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be + * created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any + * signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the + * identified secret key */ @Throws(KeyException::class, PGPException::class) @JvmOverloads - fun addInlineSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - keyId: Long, - signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, - subpacketsCallback: Callback? = null) = apply { + fun addInlineSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val signingPubKeys = keyRingInfo.signingSubkeys if (signingPubKeys.isEmpty()) { @@ -197,12 +201,14 @@ class SigningOptions { continue } - val signingSecKey = signingKey.getSecretKey(signingPubKey.keyID) + val signingSecKey = + signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) val signingSubkey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, false, subpacketsCallback) return this } throw MissingSecretKeyException(of(signingKey), keyId) @@ -215,45 +221,44 @@ class SigningOptions { * @param signingKeys collection of signing key rings * @param signatureType type of the signature (binary, canonical text) * @return this - * * @throws KeyException if something is wrong with any of the keys - * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing method cannot be created + * @throws PGPException if any of the keys cannot be validated or unlocked, or if any signing + * method cannot be created */ @Throws(KeyException::class, PGPException::class) - fun addDetachedSignatures(signingKeyProtector: SecretKeyRingProtector, - signingKeys: Iterable, - signatureType: DocumentSignatureType) = apply { - signingKeys.forEach { - addDetachedSignature(signingKeyProtector, it, null, signatureType) - } + fun addDetachedSignatures( + signingKeyProtector: SecretKeyRingProtector, + signingKeys: Iterable, + signatureType: DocumentSignatureType + ) = apply { + signingKeys.forEach { addDetachedSignature(signingKeyProtector, it, null, 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 (e.g. when signing a file). + * 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 (e.g. when signing a file). * * @param signingKeyProtector decryptor to unlock the secret signing key * @param signingKey signing key * @param signatureType type of data that is signed (binary, canonical text) * @return this - * * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method + * can be created */ @Throws(KeyException::class, PGPException::class) - fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - signatureType: DocumentSignatureType) = apply { - addDetachedSignature(signingKeyProtector, signingKey, null, signatureType) - } + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + signatureType: DocumentSignatureType + ) = apply { addDetachedSignature(signingKeyProtector, signingKey, null, 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 (e.g. when signing a file). + * 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 (e.g. when signing a file). + * *

* This method uses the passed in user-id to select user-specific hash algorithms. * @@ -263,25 +268,26 @@ class SigningOptions { * @param signatureType type of data that is signed (binary, canonical text) * @param subpacketCallback callback to modify hashed and unhashed subpackets of the signature * @return this - * * @throws KeyException if something is wrong with the key - * @throws PGPException if the key cannot be validated or unlocked, or if no signature method can be created + * @throws PGPException if the key cannot be validated or unlocked, or if no signature method + * can be created */ @JvmOverloads @Throws(KeyException::class, PGPException::class) - fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - userId: String? = null, - signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, - subpacketCallback: Callback? = null) = apply { + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + userId: String? = null, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketCallback: Callback? = null + ) = apply { val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) if (userId != null && !keyRingInfo.isUserIdValid(userId)) { throw UnboundUserIdException( - of(signingKey), - userId.toString(), - keyRingInfo.getLatestUserIdCertification(userId), - keyRingInfo.getUserIdRevocation(userId) - ) + of(signingKey), + userId.toString(), + keyRingInfo.getLatestUserIdCertification(userId), + keyRingInfo.getUserIdRevocation(userId)) } val signingPubKeys = keyRingInfo.signingSubkeys @@ -290,14 +296,16 @@ class SigningOptions { } for (signingPubKey in signingPubKeys) { - val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = - if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) - else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) + if (userId != null) keyRingInfo.getPreferredHashAlgorithms(userId) + else keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback) + addSigningMethod( + signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketCallback) } } @@ -310,17 +318,22 @@ class SigningOptions { * @param signatureType signature type * @param subpacketsCallback callback to modify the signatures subpackets * @return builder - * @throws PGPException if the secret key cannot be unlocked or if no signing method can be created. - * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any signing-capable subkeys - * @throws KeyException.MissingSecretKeyException if the key ring does not contain the identified secret key + * @throws PGPException if the secret key cannot be unlocked or if no signing method can be + * created. + * @throws KeyException.UnacceptableSigningKeyException if the key ring does not carry any + * signing-capable subkeys + * @throws KeyException.MissingSecretKeyException if the key ring does not contain the + * identified secret key */ @Throws(KeyException::class, PGPException::class) @JvmOverloads - fun addDetachedSignature(signingKeyProtector: SecretKeyRingProtector, - signingKey: PGPSecretKeyRing, - keyId: Long, - signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, - subpacketsCallback: Callback? = null) = apply { + fun addDetachedSignature( + signingKeyProtector: SecretKeyRingProtector, + signingKey: PGPSecretKeyRing, + keyId: Long, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = apply { val keyRingInfo = inspectKeyRing(signingKey, evaluationDate) val signingPubKeys = keyRingInfo.signingSubkeys @@ -330,12 +343,20 @@ class SigningOptions { for (signingPubKey in signingPubKeys) { if (signingPubKey.keyID == keyId) { - val signingSecKey: PGPSecretKey = signingKey.getSecretKey(signingPubKey.keyID) + val signingSecKey: PGPSecretKey = + signingKey.getSecretKey(signingPubKey.keyID) ?: throw MissingSecretKeyException(of(signingKey), signingPubKey.keyID) val signingSubkey: PGPPrivateKey = signingSecKey.unlock(signingKeyProtector) val hashAlgorithms = keyRingInfo.getPreferredHashAlgorithms(signingPubKey.keyID) - val hashAlgorithm: HashAlgorithm = negotiateHashAlgorithm(hashAlgorithms, getPolicy()) - addSigningMethod(signingKey, signingSubkey, hashAlgorithm, signatureType, true, subpacketsCallback) + val hashAlgorithm: HashAlgorithm = + negotiateHashAlgorithm(hashAlgorithms, getPolicy()) + addSigningMethod( + signingKey, + signingSubkey, + hashAlgorithm, + signatureType, + true, + subpacketsCallback) return this } } @@ -343,26 +364,30 @@ class SigningOptions { throw MissingSecretKeyException(of(signingKey), keyId) } - private fun addSigningMethod(signingKey: PGPSecretKeyRing, - signingSubkey: PGPPrivateKey, - hashAlgorithm: HashAlgorithm, - signatureType: DocumentSignatureType, - detached: Boolean, - subpacketCallback: Callback? = null) { + private fun addSigningMethod( + signingKey: PGPSecretKeyRing, + signingSubkey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType, + detached: Boolean, + subpacketCallback: Callback? = null + ) { val signingKeyIdentifier = SubkeyIdentifier(signingKey, signingSubkey.keyID) val signingSecretKey: PGPSecretKey = signingKey.getSecretKey(signingSubkey.keyID) val publicKeyAlgorithm = requireFromId(signingSecretKey.publicKey.algorithm) val bitStrength = signingSecretKey.publicKey.bitStrength if (!getPolicy().publicKeyAlgorithmPolicy.isAcceptable(publicKeyAlgorithm, bitStrength)) { throw UnacceptableSigningKeyException( - PublicKeyAlgorithmPolicyException( - of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) + PublicKeyAlgorithmPolicyException( + of(signingKey), signingSecretKey.keyID, publicKeyAlgorithm, bitStrength)) } - val generator: PGPSignatureGenerator = createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) + val generator: PGPSignatureGenerator = + createSignatureGenerator(signingSubkey, hashAlgorithm, signatureType) // Subpackets - val hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingSecretKey.publicKey) + val hashedSubpackets = + SignatureSubpackets.createHashedSubpackets(signingSecretKey.publicKey) val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets() if (subpacketCallback != null) { subpacketCallback.modifyHashedSubpackets(hashedSubpackets) @@ -372,80 +397,87 @@ class SigningOptions { generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)) val signingMethod = - if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) - else SigningMethod.inlineSignature(generator, hashAlgorithm) + if (detached) SigningMethod.detachedSignature(generator, hashAlgorithm) + else SigningMethod.inlineSignature(generator, hashAlgorithm) (signingMethods as MutableMap)[signingKeyIdentifier] = signingMethod } /** * Negotiate, which hash algorithm to use. * - * - * This method gives the highest priority to the algorithm override, which can be set via [.overrideHashAlgorithm]. - * After that, the signing keys hash algorithm preferences are iterated to find the first acceptable algorithm. - * Lastly, should no acceptable algorithm be found, the [Policies][Policy] default signature hash algorithm is - * used as a fallback. + * This method gives the highest priority to the algorithm override, which can be set via + * [.overrideHashAlgorithm]. After that, the signing keys hash algorithm preferences are + * iterated to find the first acceptable algorithm. Lastly, should no acceptable algorithm be + * found, the [Policies][Policy] default signature hash algorithm is used as a fallback. * * @param preferences preferences * @param policy policy * @return selected hash algorithm */ - private fun negotiateHashAlgorithm(preferences: Set, - policy: Policy): HashAlgorithm { - return _hashAlgorithmOverride ?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences) + private fun negotiateHashAlgorithm( + preferences: Set, + policy: Policy + ): HashAlgorithm { + return _hashAlgorithmOverride + ?: negotiateSignatureHashAlgorithm(policy).negotiateHashAlgorithm(preferences) } @Throws(PGPException::class) - private fun createSignatureGenerator(privateKey: PGPPrivateKey, - hashAlgorithm: HashAlgorithm, - signatureType: DocumentSignatureType): PGPSignatureGenerator { + private fun createSignatureGenerator( + privateKey: PGPPrivateKey, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType + ): PGPSignatureGenerator { return ImplementationFactory.getInstance() - .getPGPContentSignerBuilder(privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId) - .let { csb -> - PGPSignatureGenerator(csb).also { it.init(signatureType.signatureType.code, privateKey) } + .getPGPContentSignerBuilder( + privateKey.publicKeyPacket.algorithm, hashAlgorithm.algorithmId) + .let { csb -> + PGPSignatureGenerator(csb).also { + it.init(signatureType.signatureType.code, privateKey) } + } } - companion object { - @JvmStatic - fun get() = SigningOptions() + @JvmStatic fun get() = SigningOptions() } - /** - * A method of signing. - */ - class SigningMethod private constructor( - val signatureGenerator: PGPSignatureGenerator, - val isDetached: Boolean, - val hashAlgorithm: HashAlgorithm + /** A method of signing. */ + class SigningMethod + private constructor( + val signatureGenerator: PGPSignatureGenerator, + val isDetached: Boolean, + val hashAlgorithm: HashAlgorithm ) { companion object { /** - * Inline-signature method. - * The resulting signature will be written into the message itself, together with a one-pass-signature packet. + * Inline-signature method. The resulting signature will be written into the message + * itself, together with a one-pass-signature packet. * * @param signatureGenerator signature generator * @param hashAlgorithm hash algorithm used to generate the signature * @return inline signing method */ @JvmStatic - fun inlineSignature(signatureGenerator: PGPSignatureGenerator, hashAlgorithm: HashAlgorithm) = - SigningMethod(signatureGenerator, false, hashAlgorithm) + fun inlineSignature( + signatureGenerator: PGPSignatureGenerator, + hashAlgorithm: HashAlgorithm + ) = SigningMethod(signatureGenerator, false, hashAlgorithm) /** - * Detached signing method. - * The resulting signature will not be added to the message, and instead can be distributed separately - * to the signed message. + * 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 * @param hashAlgorithm hash algorithm used to generate the signature * @return detached signing method */ @JvmStatic - fun detachedSignature(signatureGenerator: PGPSignatureGenerator, hashAlgorithm: HashAlgorithm) = - SigningMethod(signatureGenerator, true, hashAlgorithm) + fun detachedSignature( + signatureGenerator: PGPSignatureGenerator, + hashAlgorithm: HashAlgorithm + ) = SigningMethod(signatureGenerator, true, hashAlgorithm) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt index fec3a550..dcf594ea 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/BcImplementationFactory.kt @@ -4,6 +4,9 @@ package org.pgpainless.implementation +import java.io.InputStream +import java.security.KeyPair +import java.util.* import org.bouncycastle.crypto.AsymmetricCipherKeyPair import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.bc.BcPGPObjectFactory @@ -27,70 +30,85 @@ import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.util.Passphrase -import java.io.InputStream -import java.security.KeyPair -import java.util.* class BcImplementationFactory : ImplementationFactory() { - override val pgpDigestCalculatorProvider: BcPGPDigestCalculatorProvider = BcPGPDigestCalculatorProvider() - override val pgpContentVerifierBuilderProvider: BcPGPContentVerifierBuilderProvider = BcPGPContentVerifierBuilderProvider() + override val pgpDigestCalculatorProvider: BcPGPDigestCalculatorProvider = + BcPGPDigestCalculatorProvider() + override val pgpContentVerifierBuilderProvider: BcPGPContentVerifierBuilderProvider = + BcPGPContentVerifierBuilderProvider() override val keyFingerprintCalculator: BcKeyFingerprintCalculator = BcKeyFingerprintCalculator() - override fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase): PBESecretKeyEncryptor = - BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) - .build(passphrase.getChars()) + override fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .build(passphrase.getChars()) - override fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, - hashAlgorithm: HashAlgorithm, - s2kCount: Int, - passphrase: Passphrase): PBESecretKeyEncryptor = - BcPBESecretKeyEncryptorBuilder( - encryptionAlgorithm.algorithmId, - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .build(passphrase.getChars()) + override fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + BcPBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) + .build(passphrase.getChars()) override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = - BcPBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) - .build(passphrase.getChars()) + BcPBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider).build(passphrase.getChars()) - override fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder = - BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + override fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder = BcPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = - BcPBEDataDecryptorFactory(passphrase.getChars(), pgpDigestCalculatorProvider) + BcPBEDataDecryptorFactory(passphrase.getChars(), pgpDigestCalculatorProvider) - override fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory = - BcPublicKeyDataDecryptorFactory(privateKey) + override fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(privateKey) - override fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory = - BcSessionKeyDataDecryptorFactory(sessionKey) + override fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory = BcSessionKeyDataDecryptorFactory(sessionKey) - override fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator = - BcPublicKeyKeyEncryptionMethodGenerator(key) + override fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator = BcPublicKeyKeyEncryptionMethodGenerator(key) - override fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator = - BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()) + override fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator = BcPBEKeyEncryptionMethodGenerator(passphrase.getChars()) override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = - BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm) + BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm) - override fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair = - BcPGPKeyPair( - publicKeyAlgorithm.algorithmId, - jceToBcKeyPair(publicKeyAlgorithm, keyPair, creationDate), - creationDate) + override fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair = + BcPGPKeyPair( + publicKeyAlgorithm.algorithmId, + jceToBcKeyPair(publicKeyAlgorithm, keyPair, creationDate), + creationDate) - override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = BcPGPObjectFactory(inputStream) + override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = + BcPGPObjectFactory(inputStream) - private fun jceToBcKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, - keyPair: KeyPair, - creationDate: Date): AsymmetricCipherKeyPair = - BcPGPKeyConverter().let { converter -> - JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate).let { pair -> - AsymmetricCipherKeyPair(converter.getPublicKey(pair.publicKey), converter.getPrivateKey(pair.privateKey)) - } + private fun jceToBcKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): AsymmetricCipherKeyPair = + BcPGPKeyConverter().let { converter -> + JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate).let { pair -> + AsymmetricCipherKeyPair( + converter.getPublicKey(pair.publicKey), + converter.getPrivateKey(pair.privateKey)) } -} \ No newline at end of file + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt index 5ae653b4..58478379 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/ImplementationFactory.kt @@ -4,6 +4,9 @@ package org.pgpainless.implementation +import java.io.InputStream +import java.security.KeyPair +import java.util.* import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.* import org.pgpainless.algorithm.HashAlgorithm @@ -11,18 +14,13 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.util.Passphrase import org.pgpainless.util.SessionKey -import java.io.InputStream -import java.security.KeyPair -import java.util.* abstract class ImplementationFactory { companion object { - @JvmStatic - private var instance: ImplementationFactory = BcImplementationFactory() + @JvmStatic private var instance: ImplementationFactory = BcImplementationFactory() - @JvmStatic - fun getInstance() = instance + @JvmStatic fun getInstance() = instance @JvmStatic fun setFactoryImplementation(implementation: ImplementationFactory) = apply { @@ -38,56 +36,82 @@ abstract class ImplementationFactory { get() = getPGPDigestCalculator(HashAlgorithm.SHA1) @Throws(PGPException::class) - abstract fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase): PBESecretKeyEncryptor + abstract fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor @Throws(PGPException::class) abstract fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor @Throws(PGPException::class) - abstract fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, hashAlgorithm: HashAlgorithm, - s2kCount: Int, passphrase: Passphrase): PBESecretKeyEncryptor + abstract fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor fun getPGPDigestCalculator(hashAlgorithm: HashAlgorithm): PGPDigestCalculator = - getPGPDigestCalculator(hashAlgorithm.algorithmId) + getPGPDigestCalculator(hashAlgorithm.algorithmId) fun getPGPDigestCalculator(hashAlgorithm: Int): PGPDigestCalculator = - pgpDigestCalculatorProvider.get(hashAlgorithm) + pgpDigestCalculatorProvider.get(hashAlgorithm) - fun getPGPContentSignerBuilder(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm): PGPContentSignerBuilder = - getPGPContentSignerBuilder(keyAlgorithm.algorithmId, hashAlgorithm.algorithmId) + fun getPGPContentSignerBuilder( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm + ): PGPContentSignerBuilder = + getPGPContentSignerBuilder(keyAlgorithm.algorithmId, hashAlgorithm.algorithmId) - abstract fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder + abstract fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder @Throws(PGPException::class) abstract fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory - abstract fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory + abstract fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory fun getSessionKeyDataDecryptorFactory(sessionKey: SessionKey): SessionKeyDataDecryptorFactory = - getSessionKeyDataDecryptorFactory(PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)) + getSessionKeyDataDecryptorFactory( + PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key)) - abstract fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory + abstract fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory - abstract fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator + abstract fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator - abstract fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator + abstract fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator - fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: SymmetricKeyAlgorithm): PGPDataEncryptorBuilder = - getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.algorithmId) + fun getPGPDataEncryptorBuilder( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm + ): PGPDataEncryptorBuilder = getPGPDataEncryptorBuilder(symmetricKeyAlgorithm.algorithmId) abstract fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder @Throws(PGPException::class) - abstract fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair + abstract fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair fun getPGPObjectFactory(bytes: ByteArray): PGPObjectFactory = - getPGPObjectFactory(bytes.inputStream()) + getPGPObjectFactory(bytes.inputStream()) abstract fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory override fun toString(): String { return javaClass.simpleName } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt index 9a4d8e41..865f1e0d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/implementation/JceImplementationFactory.kt @@ -4,6 +4,9 @@ package org.pgpainless.implementation +import java.io.InputStream +import java.security.KeyPair +import java.util.* import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.* import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator @@ -24,79 +27,86 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.provider.ProviderFactory import org.pgpainless.util.Passphrase -import java.io.InputStream -import java.security.KeyPair -import java.util.* class JceImplementationFactory : ImplementationFactory() { override val pgpDigestCalculatorProvider: PGPDigestCalculatorProvider = - JcaPGPDigestCalculatorProviderBuilder() - .setProvider(ProviderFactory.provider) - .build() + JcaPGPDigestCalculatorProviderBuilder().setProvider(ProviderFactory.provider).build() override val pgpContentVerifierBuilderProvider: PGPContentVerifierBuilderProvider = - JcaPGPContentVerifierBuilderProvider() - .setProvider(ProviderFactory.provider) + JcaPGPContentVerifierBuilderProvider().setProvider(ProviderFactory.provider) override val keyFingerprintCalculator: KeyFingerPrintCalculator = - JcaKeyFingerprintCalculator() - .setProvider(ProviderFactory.provider) + JcaKeyFingerprintCalculator().setProvider(ProviderFactory.provider) - override fun getPBESecretKeyEncryptor(symmetricKeyAlgorithm: SymmetricKeyAlgorithm, - digestCalculator: PGPDigestCalculator, - passphrase: Passphrase): PBESecretKeyEncryptor = - JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) + override fun getPBESecretKeyEncryptor( + symmetricKeyAlgorithm: SymmetricKeyAlgorithm, + digestCalculator: PGPDigestCalculator, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder(symmetricKeyAlgorithm.algorithmId, digestCalculator) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) - override fun getPBESecretKeyEncryptor(encryptionAlgorithm: SymmetricKeyAlgorithm, - hashAlgorithm: HashAlgorithm, - s2kCount: Int, - passphrase: Passphrase): PBESecretKeyEncryptor = - JcePBESecretKeyEncryptorBuilder( - encryptionAlgorithm.algorithmId, - getPGPDigestCalculator(hashAlgorithm), - s2kCount) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) + override fun getPBESecretKeyEncryptor( + encryptionAlgorithm: SymmetricKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + s2kCount: Int, + passphrase: Passphrase + ): PBESecretKeyEncryptor = + JcePBESecretKeyEncryptorBuilder( + encryptionAlgorithm.algorithmId, getPGPDigestCalculator(hashAlgorithm), s2kCount) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) override fun getPBESecretKeyDecryptor(passphrase: Passphrase): PBESecretKeyDecryptor = - JcePBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) + JcePBESecretKeyDecryptorBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) - override fun getPGPContentSignerBuilder(keyAlgorithm: Int, hashAlgorithm: Int): PGPContentSignerBuilder = - JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) - .setProvider(ProviderFactory.provider) + override fun getPGPContentSignerBuilder( + keyAlgorithm: Int, + hashAlgorithm: Int + ): PGPContentSignerBuilder = + JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm) + .setProvider(ProviderFactory.provider) override fun getPBEDataDecryptorFactory(passphrase: Passphrase): PBEDataDecryptorFactory = - JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider) - .setProvider(ProviderFactory.provider) - .build(passphrase.getChars()) + JcePBEDataDecryptorFactoryBuilder(pgpDigestCalculatorProvider) + .setProvider(ProviderFactory.provider) + .build(passphrase.getChars()) - override fun getPublicKeyDataDecryptorFactory(privateKey: PGPPrivateKey): PublicKeyDataDecryptorFactory = - JcePublicKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.provider) - .build(privateKey) + override fun getPublicKeyDataDecryptorFactory( + privateKey: PGPPrivateKey + ): PublicKeyDataDecryptorFactory = + JcePublicKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.provider) + .build(privateKey) - override fun getSessionKeyDataDecryptorFactory(sessionKey: PGPSessionKey): SessionKeyDataDecryptorFactory = - JceSessionKeyDataDecryptorFactoryBuilder() - .setProvider(ProviderFactory.provider) - .build(sessionKey) + override fun getSessionKeyDataDecryptorFactory( + sessionKey: PGPSessionKey + ): SessionKeyDataDecryptorFactory = + JceSessionKeyDataDecryptorFactoryBuilder() + .setProvider(ProviderFactory.provider) + .build(sessionKey) - override fun getPublicKeyKeyEncryptionMethodGenerator(key: PGPPublicKey): PublicKeyKeyEncryptionMethodGenerator = - JcePublicKeyKeyEncryptionMethodGenerator(key) - .setProvider(ProviderFactory.provider) + override fun getPublicKeyKeyEncryptionMethodGenerator( + key: PGPPublicKey + ): PublicKeyKeyEncryptionMethodGenerator = + JcePublicKeyKeyEncryptionMethodGenerator(key).setProvider(ProviderFactory.provider) - override fun getPBEKeyEncryptionMethodGenerator(passphrase: Passphrase): PBEKeyEncryptionMethodGenerator = - JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) - .setProvider(ProviderFactory.provider) + override fun getPBEKeyEncryptionMethodGenerator( + passphrase: Passphrase + ): PBEKeyEncryptionMethodGenerator = + JcePBEKeyEncryptionMethodGenerator(passphrase.getChars()) + .setProvider(ProviderFactory.provider) override fun getPGPDataEncryptorBuilder(symmetricKeyAlgorithm: Int): PGPDataEncryptorBuilder = - JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm) - .setProvider(ProviderFactory.provider) + JcePGPDataEncryptorBuilder(symmetricKeyAlgorithm).setProvider(ProviderFactory.provider) - override fun getPGPKeyPair(publicKeyAlgorithm: PublicKeyAlgorithm, keyPair: KeyPair, creationDate: Date): PGPKeyPair = - JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate) + override fun getPGPKeyPair( + publicKeyAlgorithm: PublicKeyAlgorithm, + keyPair: KeyPair, + creationDate: Date + ): PGPKeyPair = JcaPGPKeyPair(publicKeyAlgorithm.algorithmId, keyPair, creationDate) override fun getPGPObjectFactory(inputStream: InputStream): PGPObjectFactory = - PGPObjectFactory(inputStream, keyFingerprintCalculator) -} \ No newline at end of file + PGPObjectFactory(inputStream, keyFingerprintCalculator) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt index fdcad306..c67c3983 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -4,16 +4,13 @@ package org.pgpainless.key +import java.nio.charset.Charset import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.util.encoders.Hex -import java.nio.charset.Charset -/** - * Abstract super class of different version OpenPGP fingerprints. - * - */ +/** Abstract super class of different version OpenPGP fingerprints. */ abstract class OpenPgpFingerprint : CharSequence, Comparable { val fingerprint: String val bytes: ByteArray @@ -26,39 +23,41 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable abstract fun getVersion(): Int /** - * Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to. - * This method can be implemented for V4 and V5 fingerprints. - * V3 key-IDs cannot be derived from the fingerprint, but we don't care, since V3 is deprecated. + * Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to. This + * method can be implemented for V4 and V5 fingerprints. V3 key-IDs cannot be derived from the + * fingerprint, but we don't care, since V3 is deprecated. * - * @see - * RFC-4880 §12.2: Key IDs and Fingerprints * @return key id + * @see RFC-4880 §12.2: Key IDs and + * Fingerprints */ abstract val keyId: Long constructor(fingerprint: String) { val prep = fingerprint.replace(" ", "").trim().uppercase() if (!isValid(prep)) { - throw IllegalArgumentException("Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.") + throw IllegalArgumentException( + "Fingerprint '$fingerprint' does not appear to be a valid OpenPGP V${getVersion()} fingerprint.") } this.fingerprint = prep this.bytes = Hex.decode(prep) } - constructor(bytes: ByteArray): this(Hex.toHexString(bytes)) + constructor(bytes: ByteArray) : this(Hex.toHexString(bytes)) - constructor(key: PGPPublicKey): this(key.fingerprint) { + constructor(key: PGPPublicKey) : this(key.fingerprint) { if (key.version != getVersion()) { throw IllegalArgumentException("Key is not a v${getVersion()} OpenPgp key.") } } - constructor(key: PGPSecretKey): this(key.publicKey) + constructor(key: PGPSecretKey) : this(key.publicKey) - constructor(keys: PGPKeyRing): this(keys.publicKey) + constructor(keys: PGPKeyRing) : this(keys.publicKey) /** * Check, whether the fingerprint consists of 40 valid hexadecimal characters. + * * @param fp fingerprint to check. * @return true if fingerprint is valid. */ @@ -69,7 +68,9 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable override fun get(index: Int) = fingerprint.get(index) - override fun subSequence(startIndex: Int, endIndex: Int) = fingerprint.subSequence(startIndex, endIndex) + override fun subSequence(startIndex: Int, endIndex: Int) = + fingerprint.subSequence(startIndex, endIndex) + override fun compareTo(other: OpenPgpFingerprint): Int { return fingerprint.compareTo(other.fingerprint) } @@ -87,49 +88,49 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable abstract fun prettyPrint(): String companion object { - @JvmStatic - val utf8: Charset = Charset.forName("UTF-8") + @JvmStatic val utf8: Charset = Charset.forName("UTF-8") /** - * Return the fingerprint of the given key. - * This method automatically matches key versions to fingerprint implementations. + * Return the fingerprint of the given key. This method automatically matches key versions + * to fingerprint implementations. + * + * @param key key + * @return fingerprint + */ + @JvmStatic fun of(key: PGPSecretKey): OpenPgpFingerprint = of(key.publicKey) + + /** + * Return the fingerprint of the given key. This method automatically matches key versions + * to fingerprint implementations. * * @param key key * @return fingerprint */ @JvmStatic - fun of(key: PGPSecretKey): OpenPgpFingerprint = of(key.publicKey) + fun of(key: PGPPublicKey): OpenPgpFingerprint = + when (key.version) { + 4 -> OpenPgpV4Fingerprint(key) + 5 -> OpenPgpV5Fingerprint(key) + 6 -> OpenPgpV6Fingerprint(key) + else -> + throw IllegalArgumentException( + "OpenPGP keys of version ${key.version} are not supported.") + } /** - * Return the fingerprint of the given key. - * This method automatically matches key versions to fingerprint implementations. - * - * @param key key - * @return fingerprint - */ - @JvmStatic - fun of (key: PGPPublicKey): OpenPgpFingerprint = when(key.version) { - 4 -> OpenPgpV4Fingerprint(key) - 5 -> OpenPgpV5Fingerprint(key) - 6 -> OpenPgpV6Fingerprint(key) - else -> throw IllegalArgumentException("OpenPGP keys of version ${key.version} are not supported.") - } - - /** - * Return the fingerprint of the primary key of the given key ring. - * This method automatically matches key versions to fingerprint implementations. + * Return the fingerprint of the primary key of the given key ring. This method + * automatically matches key versions to fingerprint implementations. * * @param ring key ring * @return fingerprint */ - @JvmStatic - fun of (keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) + @JvmStatic fun of(keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) /** - * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. - * If the trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 fingerprint. - * In this case, we return a {@link _64DigitFingerprint}. Since this is ambiguous, it is generally recommended - * to know the version of the key beforehand. + * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. If the + * trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 + * fingerprint. In this case, we return a {@link _64DigitFingerprint}. Since this is + * ambiguous, it is generally recommended to know the version of the key beforehand. * * @param fingerprint fingerprint * @return parsed fingerprint @@ -146,7 +147,8 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable // Might be v5 or v6 :/ return _64DigitFingerprint(prep) } - throw IllegalArgumentException("Fingerprint does not appear to match any known fingerprint pattern.") + throw IllegalArgumentException( + "Fingerprint does not appear to match any known fingerprint pattern.") } /** @@ -159,6 +161,6 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable @JvmStatic @Deprecated("use the parse() methods of the versioned fingerprint subclasses instead.") fun parseFromBinary(binaryFingerprint: ByteArray): OpenPgpFingerprint = - parse(Hex.toHexString(binaryFingerprint)) + parse(Hex.toHexString(binaryFingerprint)) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt index fe0c4364..e02f0ae7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV4Fingerprint.kt @@ -4,22 +4,26 @@ package org.pgpainless.key -import org.bouncycastle.openpgp.PGPKeyRing -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPSecretKey -import org.bouncycastle.util.encoders.Hex import java.net.URI import java.nio.Buffer import java.nio.ByteBuffer import java.nio.charset.Charset +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex -class OpenPgpV4Fingerprint: OpenPgpFingerprint { +class OpenPgpV4Fingerprint : OpenPgpFingerprint { - constructor(fingerprint: String): super(fingerprint) - constructor(bytes: ByteArray): super(bytes) - constructor(key: PGPPublicKey): super(key) - constructor(key: PGPSecretKey): super(key) - constructor(keys: PGPKeyRing): super(keys) + constructor(fingerprint: String) : super(fingerprint) + + constructor(bytes: ByteArray) : super(bytes) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) override fun getVersion() = 4 @@ -47,7 +51,7 @@ class OpenPgpV4Fingerprint: OpenPgpFingerprint { append(fingerprint, i * 4, (i + 1) * 4).append(' ') } append(' ') - for (i in 5 .. 8) { + for (i in 5..8) { append(fingerprint, i * 4, (i + 1) * 4).append(' ') } append(fingerprint, 36, 40) @@ -55,8 +59,7 @@ class OpenPgpV4Fingerprint: OpenPgpFingerprint { } companion object { - @JvmStatic - val SCHEME = "openpgp4fpr" + @JvmStatic val SCHEME = "openpgp4fpr" @JvmStatic fun fromUri(uri: URI): OpenPgpV4Fingerprint { @@ -66,4 +69,4 @@ class OpenPgpV4Fingerprint: OpenPgpFingerprint { return OpenPgpV4Fingerprint(uri.schemeSpecificPart) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt index b82a3482..7bc36cc9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV5Fingerprint.kt @@ -8,18 +8,20 @@ import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey -/** - * This class represents a hex encoded uppercase OpenPGP v5 fingerprint. - */ -class OpenPgpV5Fingerprint: _64DigitFingerprint { +/** This class represents a hex encoded uppercase OpenPGP v5 fingerprint. */ +class OpenPgpV5Fingerprint : _64DigitFingerprint { - constructor(fingerprint: String): super(fingerprint) - constructor(key: PGPPublicKey): super(key) - constructor(key: PGPSecretKey): super(key) - constructor(keys: PGPKeyRing): super(keys) - constructor(bytes: ByteArray): super(bytes) + constructor(fingerprint: String) : super(fingerprint) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + constructor(bytes: ByteArray) : super(bytes) override fun getVersion(): Int { return 5 } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt index 11cf05b0..0b843ed2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpV6Fingerprint.kt @@ -8,18 +8,20 @@ import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey -/** - * This class represents a hex encoded, uppercase OpenPGP v6 fingerprint. - */ -class OpenPgpV6Fingerprint: _64DigitFingerprint { +/** This class represents a hex encoded, uppercase OpenPGP v6 fingerprint. */ +class OpenPgpV6Fingerprint : _64DigitFingerprint { - constructor(fingerprint: String): super(fingerprint) - constructor(key: PGPPublicKey): super(key) - constructor(key: PGPSecretKey): super(key) - constructor(keys: PGPKeyRing): super(keys) - constructor(bytes: ByteArray): super(bytes) + constructor(fingerprint: String) : super(fingerprint) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) + + constructor(bytes: ByteArray) : super(bytes) override fun getVersion(): Int { return 6 } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt index a793fc99..2aec7976 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/SubkeyIdentifier.kt @@ -13,17 +13,30 @@ import org.bouncycastle.openpgp.PGPPublicKey * as well as the subkeys fingerprint. */ class SubkeyIdentifier( - val primaryKeyFingerprint: OpenPgpFingerprint, - val subkeyFingerprint: OpenPgpFingerprint) { + val primaryKeyFingerprint: OpenPgpFingerprint, + val subkeyFingerprint: OpenPgpFingerprint +) { - constructor(fingerprint: OpenPgpFingerprint): this(fingerprint, fingerprint) - constructor(keys: PGPKeyRing): this(keys.publicKey) - constructor(key: PGPPublicKey): this(OpenPgpFingerprint.of(key)) - constructor(keys: PGPKeyRing, keyId: Long): this( - OpenPgpFingerprint.of(keys.publicKey), - OpenPgpFingerprint.of(keys.getPublicKey(keyId) ?: - throw NoSuchElementException("OpenPGP key does not contain subkey ${keyId.openPgpKeyId()}"))) - constructor(keys: PGPKeyRing, subkeyFingerprint: OpenPgpFingerprint): this(OpenPgpFingerprint.of(keys), subkeyFingerprint) + constructor(fingerprint: OpenPgpFingerprint) : this(fingerprint, fingerprint) + + constructor(keys: PGPKeyRing) : this(keys.publicKey) + + constructor(key: PGPPublicKey) : this(OpenPgpFingerprint.of(key)) + + constructor( + keys: PGPKeyRing, + keyId: Long + ) : this( + OpenPgpFingerprint.of(keys.publicKey), + OpenPgpFingerprint.of( + keys.getPublicKey(keyId) + ?: throw NoSuchElementException( + "OpenPGP key does not contain subkey ${keyId.openPgpKeyId()}"))) + + constructor( + keys: PGPKeyRing, + subkeyFingerprint: OpenPgpFingerprint + ) : this(OpenPgpFingerprint.of(keys), subkeyFingerprint) val keyId = subkeyFingerprint.keyId val fingerprint = subkeyFingerprint @@ -34,7 +47,7 @@ class SubkeyIdentifier( val isPrimaryKey = primaryKeyId == subkeyId fun matches(fingerprint: OpenPgpFingerprint) = - primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint + primaryKeyFingerprint == fingerprint || subkeyFingerprint == fingerprint override fun equals(other: Any?): Boolean { if (other == null) { @@ -47,7 +60,8 @@ class SubkeyIdentifier( return false } - return primaryKeyFingerprint == other.primaryKeyFingerprint && subkeyFingerprint == other.subkeyFingerprint + return primaryKeyFingerprint == other.primaryKeyFingerprint && + subkeyFingerprint == other.subkeyFingerprint } override fun hashCode(): Int { @@ -55,4 +69,4 @@ class SubkeyIdentifier( } override fun toString(): String = "$subkeyFingerprint $primaryKeyFingerprint" -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt index 0f7dd705..279815bd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt @@ -2,47 +2,47 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key; +package org.pgpainless.key -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.encoders.Hex; +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.charset.Charset +import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.util.encoders.Hex /** - * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. - * Since both fingerprints use the same format, this class is used when parsing the fingerprint without knowing the - * key version. + * This class represents a hex encoded, upper case OpenPGP v5 or v6 fingerprint. Since both + * fingerprints use the same format, this class is used when parsing the fingerprint without knowing + * the key version. */ -open class _64DigitFingerprint: OpenPgpFingerprint { +open class _64DigitFingerprint : OpenPgpFingerprint { /** * Create an {@link _64DigitFingerprint}. * * @param fingerprint uppercase hexadecimal fingerprint of length 64 */ - constructor(fingerprint: String): super(fingerprint) - constructor(bytes: ByteArray): super(bytes) - constructor(key: PGPPublicKey): super(key) - constructor(key: PGPSecretKey): super(key) - constructor(keys: PGPKeyRing): super(keys) + constructor(fingerprint: String) : super(fingerprint) + + constructor(bytes: ByteArray) : super(bytes) + + constructor(key: PGPPublicKey) : super(key) + + constructor(key: PGPSecretKey) : super(key) + + constructor(keys: PGPKeyRing) : super(keys) override val keyId: Long get() { val bytes = Hex.decode(fingerprint.toByteArray(Charset.forName("UTF-8"))) - val buf = ByteBuffer.wrap(bytes); + val buf = ByteBuffer.wrap(bytes) // The key id is the left-most 8 bytes (conveniently a long). // We have to cast here in order to be compatible with java 8 // https://github.com/eclipse/jetty.project/issues/3244 - (buf as Buffer).position(0); + (buf as Buffer).position(0) return buf.getLong() } @@ -62,13 +62,13 @@ open class _64DigitFingerprint: OpenPgpFingerprint { override fun prettyPrint(): String { return buildString { for (i in 0 until 4) { - append(fingerprint, i * 8, (i + 1) * 8).append(' '); + append(fingerprint, i * 8, (i + 1) * 8).append(' ') } - append(' '); + append(' ') for (i in 4 until 7) { - append(fingerprint, i * 8, (i + 1) * 8).append(' '); + append(fingerprint, i * 8, (i + 1) * 8).append(' ') } - append(fingerprint, 56, 64); + append(fingerprint, 56, 64) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt index a924e0f3..9499355c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/certification/CertifyCertificate.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.certification +import java.util.* import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSecretKey @@ -22,28 +23,27 @@ import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.signature.builder.ThirdPartyCertificationSignatureBuilder import org.pgpainless.signature.builder.ThirdPartyDirectKeySignatureBuilder import org.pgpainless.signature.subpackets.CertificationSubpackets -import java.util.* /** - * API for creating certifications and delegations (Signatures) on keys. - * This API can be used to sign another persons OpenPGP key. + * API for creating certifications and delegations (Signatures) on keys. This API can be used to + * sign another persons OpenPGP key. * - * A certification over a user-id is thereby used to attest, that the user believes that the user-id really belongs - * to the owner of the certificate. - * A delegation over a key can be used to delegate trust by marking the certificate as a trusted introducer. + * A certification over a user-id is thereby used to attest, that the user believes that the user-id + * really belongs to the owner of the certificate. A delegation over a key can be used to delegate + * trust by marking the certificate as a trusted introducer. */ class CertifyCertificate { /** - * Create a certification over a User-Id. - * By default, this method will use [CertificationType.GENERIC] to create the signature. + * Create a certification over a User-Id. By default, this method will use + * [CertificationType.GENERIC] to create the signature. * * @param userId user-id to certify * @param certificate certificate * @return API */ fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing): CertificationOnUserId = - userIdOnCertificate(userId, certificate, CertificationType.GENERIC) + userIdOnCertificate(userId, certificate, CertificationType.GENERIC) /** * Create a certification of the given [CertificationType] over a User-Id. @@ -53,36 +53,40 @@ class CertifyCertificate { * @param certificationType type of signature * @return API */ - fun userIdOnCertificate(userId: String, certificate: PGPPublicKeyRing, certificationType: CertificationType) = - CertificationOnUserId(userId, certificate, certificationType) + fun userIdOnCertificate( + userId: String, + certificate: PGPPublicKeyRing, + certificationType: CertificationType + ) = CertificationOnUserId(userId, certificate, certificationType) /** - * Create a delegation (direct key signature) over a certificate. - * This can be used to mark a certificate as a trusted introducer - * (see [certificate] method with [Trustworthiness] argument). + * Create a delegation (direct key signature) over a certificate. This can be used to mark a + * certificate as a trusted introducer (see [certificate] method with [Trustworthiness] + * argument). * * @param certificate certificate * @return API */ fun certificate(certificate: PGPPublicKeyRing): DelegationOnCertificate = - certificate(certificate, null) + certificate(certificate, null) /** - * Create a delegation (direct key signature) containing a [org.bouncycastle.bcpg.sig.TrustSignature] packet - * over a certificate. - * This can be used to mark a certificate as a trusted introducer. + * Create a delegation (direct key signature) containing a + * [org.bouncycastle.bcpg.sig.TrustSignature] packet over a certificate. This can be used to + * mark a certificate as a trusted introducer. * * @param certificate certificate * @param trustworthiness trustworthiness of the certificate * @return API */ fun certificate(certificate: PGPPublicKeyRing, trustworthiness: Trustworthiness?) = - DelegationOnCertificate(certificate, trustworthiness) + DelegationOnCertificate(certificate, trustworthiness) class CertificationOnUserId( - val userId: String, - val certificate: PGPPublicKeyRing, - val certificationType: CertificationType) { + val userId: String, + val certificate: PGPPublicKeyRing, + val certificationType: CertificationType + ) { /** * Create the certification using the given key. @@ -92,11 +96,14 @@ class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ - fun withKey(certificationKey: PGPSecretKeyRing, - protector: SecretKeyRingProtector): CertificationOnUserIdWithSubpackets { + fun withKey( + certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): CertificationOnUserIdWithSubpackets { val secretKey = getCertifyingSecretKey(certificationKey) - val sigBuilder = ThirdPartyCertificationSignatureBuilder( + val sigBuilder = + ThirdPartyCertificationSignatureBuilder( certificationType.asSignatureType(), secretKey, protector) return CertificationOnUserIdWithSubpackets(certificate, userId, sigBuilder) @@ -104,9 +111,9 @@ class CertifyCertificate { } class CertificationOnUserIdWithSubpackets( - val certificate: PGPPublicKeyRing, - val userId: String, - val sigBuilder: ThirdPartyCertificationSignatureBuilder + val certificate: PGPPublicKeyRing, + val userId: String, + val sigBuilder: ThirdPartyCertificationSignatureBuilder ) { /** @@ -116,7 +123,9 @@ class CertifyCertificate { * @return result * @throws PGPException in case of an OpenPGP related error */ - fun buildWithSubpackets(subpacketCallback: CertificationSubpackets.Callback): CertificationResult { + fun buildWithSubpackets( + subpacketCallback: CertificationSubpackets.Callback + ): CertificationResult { sigBuilder.applyCallback(subpacketCallback) return build() } @@ -129,14 +138,16 @@ class CertifyCertificate { */ fun build(): CertificationResult { val signature = sigBuilder.build(certificate, userId) - val certifiedCertificate = KeyRingUtils.injectCertification(certificate, userId, signature) + val certifiedCertificate = + KeyRingUtils.injectCertification(certificate, userId, signature) return CertificationResult(certifiedCertificate, signature) } } class DelegationOnCertificate( - val certificate: PGPPublicKeyRing, - val trustworthiness: Trustworthiness?) { + val certificate: PGPPublicKeyRing, + val trustworthiness: Trustworthiness? + ) { /** * Build the delegation using the given certification key. @@ -146,20 +157,24 @@ class CertifyCertificate { * @return API * @throws PGPException in case of an OpenPGP related error */ - fun withKey(certificationKey: PGPSecretKeyRing, - protector: SecretKeyRingProtector): DelegationOnCertificateWithSubpackets { + fun withKey( + certificationKey: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): DelegationOnCertificateWithSubpackets { val secretKey = getCertifyingSecretKey(certificationKey) val sigBuilder = ThirdPartyDirectKeySignatureBuilder(secretKey, protector) if (trustworthiness != null) { - sigBuilder.hashedSubpackets.setTrust(true, trustworthiness.depth, trustworthiness.amount) + sigBuilder.hashedSubpackets.setTrust( + true, trustworthiness.depth, trustworthiness.amount) } return DelegationOnCertificateWithSubpackets(certificate, sigBuilder) } } class DelegationOnCertificateWithSubpackets( - val certificate: PGPPublicKeyRing, - val sigBuilder: ThirdPartyDirectKeySignatureBuilder) { + val certificate: PGPPublicKeyRing, + val sigBuilder: ThirdPartyDirectKeySignatureBuilder + ) { /** * Apply the given signature subpackets and build the delegation signature. @@ -168,7 +183,9 @@ class CertifyCertificate { * @return result * @throws PGPException in case of an OpenPGP related error */ - fun buildWithSubpackets(subpacketsCallback: CertificationSubpackets.Callback): CertificationResult { + fun buildWithSubpackets( + subpacketsCallback: CertificationSubpackets.Callback + ): CertificationResult { sigBuilder.applyCallback(subpacketsCallback) return build() } @@ -182,7 +199,8 @@ class CertifyCertificate { fun build(): CertificationResult { val delegatedKey = certificate.publicKey val delegation = sigBuilder.build(delegatedKey) - val delegatedCertificate = KeyRingUtils.injectCertification(certificate, delegatedKey, delegation) + val delegatedCertificate = + KeyRingUtils.injectCertification(certificate, delegatedKey, delegation) return CertificationResult(delegatedCertificate, delegation) } } @@ -194,8 +212,9 @@ class CertifyCertificate { * @param certification the newly created signature */ data class CertificationResult( - val certifiedCertificate: PGPPublicKeyRing, - val certification: PGPSignature) + val certifiedCertificate: PGPPublicKeyRing, + val certification: PGPSignature + ) companion object { @JvmStatic @@ -205,9 +224,7 @@ class CertifyCertificate { val fingerprint = info.fingerprint val certificationPubKey = info.getPublicKey(fingerprint) - requireNotNull(certificationPubKey) { - "Primary key cannot be null." - } + requireNotNull(certificationPubKey) { "Primary key cannot be null." } if (!info.isKeyValidlyBound(certificationPubKey.keyID)) { throw RevokedKeyException(fingerprint) } @@ -222,7 +239,7 @@ class CertifyCertificate { } return certificationKey.getSecretKey(certificationPubKey.keyID) - ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyID) + ?: throw MissingSecretKeyException(fingerprint, certificationPubKey.keyID) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt index 63151a4c..f69d4a08 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/collection/PGPKeyRingCollection.kt @@ -4,32 +4,38 @@ package org.pgpainless.key.collection +import java.io.InputStream import org.bouncycastle.openpgp.* import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.util.ArmorUtils -import java.io.InputStream /** - * This class describes a logic of handling a collection of different [PGPKeyRing]. The logic was inspired by - * [PGPSecretKeyRingCollection] and [PGPPublicKeyRingCollection]. + * This class describes a logic of handling a collection of different [PGPKeyRing]. The logic was + * inspired by [PGPSecretKeyRingCollection] and [PGPPublicKeyRingCollection]. */ class PGPKeyRingCollection( - val pgpSecretKeyRingCollection: PGPSecretKeyRingCollection, - val pgpPublicKeyRingCollection: PGPPublicKeyRingCollection + val pgpSecretKeyRingCollection: PGPSecretKeyRingCollection, + val pgpPublicKeyRingCollection: PGPPublicKeyRingCollection ) { - constructor(encoding: ByteArray, isSilent: Boolean): this(encoding.inputStream(), isSilent) + constructor(encoding: ByteArray, isSilent: Boolean) : this(encoding.inputStream(), isSilent) - constructor(inputStream: InputStream, isSilent: Boolean): this(parse(inputStream, isSilent)) + constructor(inputStream: InputStream, isSilent: Boolean) : this(parse(inputStream, isSilent)) - constructor(collection: Collection, isSilent: Boolean): this(segment(collection, isSilent)) + constructor( + collection: Collection, + isSilent: Boolean + ) : this(segment(collection, isSilent)) - private constructor(arguments: Pair): this(arguments.first, arguments.second) + private constructor( + arguments: Pair + ) : this(arguments.first, arguments.second) /** * The number of rings in this collection. * - * @return total size of [PGPSecretKeyRingCollection] plus [PGPPublicKeyRingCollection] in this collection + * @return total size of [PGPSecretKeyRingCollection] plus [PGPPublicKeyRingCollection] in this + * collection */ val size: Int get() = pgpSecretKeyRingCollection.size() + pgpPublicKeyRingCollection.size() @@ -41,11 +47,16 @@ class PGPKeyRingCollection( companion object { @JvmStatic - private fun parse(inputStream: InputStream, isSilent: Boolean): Pair { + private fun parse( + inputStream: InputStream, + isSilent: Boolean + ): Pair { val secretKeyRings = mutableListOf() val certificates = mutableListOf() // Double getDecoderStream because of #96 - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) for (obj in objectFactory) { if (obj == null) { @@ -68,16 +79,21 @@ class PGPKeyRingCollection( } if (!isSilent) { - throw PGPException("${obj.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + throw PGPException( + "${obj.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + " or ${PGPPublicKeyRing::class.java.simpleName} expected") } } - return PGPSecretKeyRingCollection(secretKeyRings) to PGPPublicKeyRingCollection(certificates) + return PGPSecretKeyRingCollection(secretKeyRings) to + PGPPublicKeyRingCollection(certificates) } @JvmStatic - private fun segment(collection: Collection, isSilent: Boolean): Pair { + private fun segment( + collection: Collection, + isSilent: Boolean + ): Pair { val secretKeyRings = mutableListOf() val certificates = mutableListOf() @@ -87,12 +103,14 @@ class PGPKeyRingCollection( } else if (keyRing is PGPPublicKeyRing) { certificates.add(keyRing) } else if (!isSilent) { - throw PGPException("${keyRing.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + + throw PGPException( + "${keyRing.javaClass.name} found where ${PGPSecretKeyRing::class.java.simpleName}" + " or ${PGPPublicKeyRing::class.java.simpleName} expected") } } - return PGPSecretKeyRingCollection(secretKeyRings) to PGPPublicKeyRingCollection(certificates) + return PGPSecretKeyRingCollection(secretKeyRings) to + PGPPublicKeyRingCollection(certificates) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index f194638b..8404b652 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -4,6 +4,9 @@ package org.pgpainless.key.generation +import java.io.IOException +import java.security.KeyPairGenerator +import java.util.* import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor @@ -21,10 +24,6 @@ import org.pgpainless.signature.subpackets.SelfSignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper import org.pgpainless.util.Passphrase -import java.io.IOException -import java.security.KeyPairGenerator -import java.util.* - class KeyRingBuilder : KeyRingBuilderInterface { @@ -49,19 +48,19 @@ class KeyRingBuilder : KeyRingBuilderInterface { userIds[userId.toString().trim()] = null } - override fun addUserId(userId: ByteArray): KeyRingBuilder = addUserId(Strings.fromUTF8ByteArray(userId)) + override fun addUserId(userId: ByteArray): KeyRingBuilder = + addUserId(Strings.fromUTF8ByteArray(userId)) override fun setExpirationDate(expirationDate: Date?): KeyRingBuilder = apply { if (expirationDate == null) { this.expirationDate = null return@apply } - this.expirationDate = expirationDate.let { - require(Date() < expirationDate) { - "Expiration date must be in the future." + this.expirationDate = + expirationDate.let { + require(Date() < expirationDate) { "Expiration date must be in the future." } + expirationDate } - expirationDate - } } override fun setPassphrase(passphrase: Passphrase): KeyRingBuilder = apply { @@ -85,17 +84,14 @@ class KeyRingBuilder : KeyRingBuilderInterface { private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify override fun build(): PGPSecretKeyRing { - val keyFingerprintCalculator = ImplementationFactory.getInstance() - .v4FingerprintCalculator + val keyFingerprintCalculator = ImplementationFactory.getInstance().v4FingerprintCalculator val secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator) val secretKeyDecryptor = buildSecretKeyDecryptor() passphrase.clear() // Passphrase was used above, so we can get rid of it // generate primary key - requireNotNull(primaryKeySpec) { - "Primary Key spec required." - } + requireNotNull(primaryKeySpec) { "Primary Key spec required." } val certKey = generateKeyPair(primaryKeySpec!!) val signer = buildContentSigner(certKey) val signatureGenerator = PGPSignatureGenerator(signer) @@ -110,16 +106,28 @@ class KeyRingBuilder : KeyRingBuilderInterface { val generator = PGPSignatureSubpacketGenerator() SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator) val hashedSubPackets = generator.generate() - val ringGenerator = if (userIds.isEmpty()) { - PGPKeyRingGenerator(certKey, keyFingerprintCalculator, hashedSubPackets, null, signer, + val ringGenerator = + if (userIds.isEmpty()) { + PGPKeyRingGenerator( + certKey, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, secretKeyEncryptor) - } else { - userIds.keys.first().let { primaryUserId -> - PGPKeyRingGenerator(SignatureType.POSITIVE_CERTIFICATION.code, certKey, primaryUserId, - keyFingerprintCalculator, hashedSubPackets, null, signer, + } else { + userIds.keys.first().let { primaryUserId -> + PGPKeyRingGenerator( + SignatureType.POSITIVE_CERTIFICATION.code, + certKey, + primaryUserId, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, secretKeyEncryptor) + } } - } addSubKeys(certKey, ringGenerator) @@ -138,20 +146,26 @@ class KeyRingBuilder : KeyRingBuilderInterface { val additionalUserId = userIdIterator.next() val userIdString = additionalUserId.key val callback = additionalUserId.value - val subpackets = if (callback == null) { - hashedSubPacketGenerator.also { it.setPrimaryUserId(null) } - } else { - SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { callback.modifyHashedSubpackets(it) } - } + val subpackets = + if (callback == null) { + hashedSubPacketGenerator.also { it.setPrimaryUserId(null) } + } else { + SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { + callback.modifyHashedSubpackets(it) + } + } signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.code, privateKey) - signatureGenerator.setHashedSubpackets( - SignatureSubpacketsHelper.toVector(subpackets)) - val additionalUserIdSignature = signatureGenerator.generateCertification(userIdString, primaryPubKey) - primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, userIdString, additionalUserIdSignature) + signatureGenerator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(subpackets)) + val additionalUserIdSignature = + signatureGenerator.generateCertification(userIdString, primaryPubKey) + primaryPubKey = + PGPPublicKey.addCertification( + primaryPubKey, userIdString, additionalUserIdSignature) } // Reassemble secret key ring with modified primary key - val primarySecretKey = PGPSecretKey( + val primarySecretKey = + PGPSecretKey( privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor) val secretKeyList = mutableListOf(primarySecretKey) while (secretKeys.hasNext()) { @@ -168,25 +182,34 @@ class KeyRingBuilder : KeyRingBuilderInterface { } else { var hashedSubpackets = subKeySpec.subpackets try { - hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(primaryKey, subKey, hashedSubpackets) + hashedSubpackets = + addPrimaryKeyBindingSignatureIfNecessary( + primaryKey, subKey, hashedSubpackets) } catch (e: IOException) { - throw PGPException("Exception while adding primary key binding signature to signing subkey.", e) + throw PGPException( + "Exception while adding primary key binding signature to signing subkey.", + e) } ringGenerator.addSubKey(subKey, hashedSubpackets, null) } } } - private fun addPrimaryKeyBindingSignatureIfNecessary(primaryKey: PGPKeyPair, subKey: PGPKeyPair, hashedSubpackets: PGPSignatureSubpacketVector): PGPSignatureSubpacketVector { + private fun addPrimaryKeyBindingSignatureIfNecessary( + primaryKey: PGPKeyPair, + subKey: PGPKeyPair, + hashedSubpackets: PGPSignatureSubpacketVector + ): PGPSignatureSubpacketVector { val keyFlagMask = hashedSubpackets.keyFlags if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) && - !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { + !KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) { return hashedSubpackets } val bindingSignatureGenerator = PGPSignatureGenerator(buildContentSigner(subKey)) bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.code, subKey.privateKey) - val primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey) + val primaryKeyBindingSig = + bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey) val subpacketGenerator = PGPSignatureSubpacketGenerator(hashedSubpackets) subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig) return subpacketGenerator.generate() @@ -194,25 +217,29 @@ class KeyRingBuilder : KeyRingBuilderInterface { private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder { val hashAlgorithm = PGPainless.getPolicy().signatureHashAlgorithmPolicy.defaultHashAlgorithm - return ImplementationFactory.getInstance().getPGPContentSignerBuilder( - certKey.publicKey.algorithm, hashAlgorithm.algorithmId) + return ImplementationFactory.getInstance() + .getPGPContentSignerBuilder(certKey.publicKey.algorithm, hashAlgorithm.algorithmId) } - private fun buildSecretKeyEncryptor(keyFingerprintCalculator: PGPDigestCalculator): PBESecretKeyEncryptor? { - val keyEncryptionAlgorithm = PGPainless.getPolicy().symmetricKeyEncryptionAlgorithmPolicy.defaultSymmetricKeyAlgorithm - check(passphrase.isValid) { - "Passphrase was cleared." - } - return if (passphrase.isEmpty) null else ImplementationFactory.getInstance() - .getPBESecretKeyEncryptor(keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase) + private fun buildSecretKeyEncryptor( + keyFingerprintCalculator: PGPDigestCalculator + ): PBESecretKeyEncryptor? { + val keyEncryptionAlgorithm = + PGPainless.getPolicy() + .symmetricKeyEncryptionAlgorithmPolicy + .defaultSymmetricKeyAlgorithm + check(passphrase.isValid) { "Passphrase was cleared." } + return if (passphrase.isEmpty) null + else + ImplementationFactory.getInstance() + .getPBESecretKeyEncryptor( + keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase) } private fun buildSecretKeyDecryptor(): PBESecretKeyDecryptor? { - check(passphrase.isValid) { - "Passphrase was cleared." - } - return if (passphrase.isEmpty) null else ImplementationFactory.getInstance() - .getPBESecretKeyDecryptor(passphrase) + check(passphrase.isValid) { "Passphrase was cleared." } + return if (passphrase.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase) } companion object { @@ -222,16 +249,16 @@ class KeyRingBuilder : KeyRingBuilderInterface { fun generateKeyPair(spec: KeySpec): PGPKeyPair { spec.keyType.let { type -> // Create raw Key Pair - val keyPair = KeyPairGenerator.getInstance(type.name, ProviderFactory.provider) - .also { it.initialize(type.algorithmSpec) } - .generateKeyPair() + val keyPair = + KeyPairGenerator.getInstance(type.name, ProviderFactory.provider) + .also { it.initialize(type.algorithmSpec) } + .generateKeyPair() val keyCreationDate = spec.keyCreationDate ?: Date() // Form PGP Key Pair return ImplementationFactory.getInstance() - .getPGPKeyPair(type.algorithm, keyPair, keyCreationDate) + .getPGPKeyPair(type.algorithm, keyPair, keyCreationDate) } } } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt index 0ed301fe..ecc818b6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilderInterface.kt @@ -4,12 +4,12 @@ package org.pgpainless.key.generation -import org.bouncycastle.openpgp.PGPException -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.pgpainless.util.Passphrase import java.security.InvalidAlgorithmParameterException import java.security.NoSuchAlgorithmException import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.util.Passphrase interface KeyRingBuilderInterface> { @@ -29,6 +29,9 @@ interface KeyRingBuilderInterface> { fun setPassphrase(passphrase: Passphrase): B - @Throws(NoSuchAlgorithmException::class, PGPException::class, InvalidAlgorithmParameterException::class) + @Throws( + NoSuchAlgorithmException::class, + PGPException::class, + InvalidAlgorithmParameterException::class) fun build(): PGPSecretKeyRing -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt index 82215971..5e9fa7fd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -17,8 +17,8 @@ import org.pgpainless.util.Passphrase class KeyRingTemplates { /** - * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, - * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, a + * dedicated RSA subkey used for signing and a third RSA subkey used for encryption. * * @param userId userId or null * @param length length of the RSA keys @@ -26,160 +26,187 @@ class KeyRingTemplates { * @return key */ @JvmOverloads - fun rsaKeyRing(userId: CharSequence?, - length: RsaLength, - passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = buildKeyRing().apply { - setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) - addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) - addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - setPassphrase(passphrase) - if (userId != null) { - addUserId(userId) - } - }.build() + fun rsaKeyRing( + userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER)) + addSubkey(getBuilder(KeyType.RSA(length), KeyFlag.SIGN_DATA)) + addSubkey( + getBuilder(KeyType.RSA(length), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + } + .build() /** - * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, - * a dedicated RSA subkey used for signing and a third RSA subkey used for encryption. + * Generate an RSA OpenPGP key consisting of an RSA primary key used for certification, a + * dedicated RSA subkey used for signing and a third RSA subkey used for encryption. * * @param userId userId or null * @param length length of the RSA keys - * @param password passphrase to encrypt the key with. Can be null or blank for unencrypted keys. + * @param password passphrase to encrypt the key with. Can be null or blank for unencrypted + * keys. * @return key */ - fun rsaKeyRing(userId: CharSequence?, - length: RsaLength, - password: String? - ): PGPSecretKeyRing = password.let { - if (it.isNullOrBlank()) { - rsaKeyRing(userId, length, Passphrase.emptyPassphrase()) - } else { - rsaKeyRing(userId, length, Passphrase.fromPassword(it)) + fun rsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + rsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + rsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } } - } /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. + * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. The + * KeyPair consists of a single RSA master key which is used for signing, encryption and + * certification. * * @param userId user id. * @param length length in bits. * @param password Password of the key. Can be empty for unencrypted keys. - * * @return {@link PGPSecretKeyRing} containing the KeyPair. */ @JvmOverloads - fun simpleRsaKeyRing(userId: CharSequence?, - length: RsaLength, - passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = buildKeyRing().apply { - setPrimaryKey(getBuilder(KeyType.RSA(length), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA, KeyFlag.ENCRYPT_COMMS)) - setPassphrase(passphrase) - if (userId != null) { - addUserId(userId.toString()) - } - }.build() + fun simpleRsaKeyRing( + userId: CharSequence?, + length: RsaLength, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey( + getBuilder( + KeyType.RSA(length), + KeyFlag.CERTIFY_OTHER, + KeyFlag.SIGN_DATA, + KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + } + .build() /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. - * The KeyPair consists of a single RSA master key which is used for signing, encryption and certification. + * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. The + * KeyPair consists of a single RSA master key which is used for signing, encryption and + * certification. * * @param userId user id. * @param length length in bits. * @param password Password of the key. Can be null or blank for unencrypted keys. - * * @return {@link PGPSecretKeyRing} containing the KeyPair. */ - fun simpleRsaKeyRing(userId: CharSequence?, - length: RsaLength, - password: String? - ) = password.let { - if (it.isNullOrBlank()) { - simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()) - } else { - simpleRsaKeyRing(userId, length, Passphrase.fromPassword(it)) + fun simpleRsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?) = + password.let { + if (it.isNullOrBlank()) { + simpleRsaKeyRing(userId, length, Passphrase.emptyPassphrase()) + } else { + simpleRsaKeyRing(userId, length, Passphrase.fromPassword(it)) + } } - } /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. The + * EdDSA primary key is used for signing messages and certifying the sub key. The XDH subkey is + * used for encryption and decryption of messages. * * @param userId user-id * @param passphrase Password of the private key. Can be empty for an unencrypted key. - * * @return {@link PGPSecretKeyRing} containing the key pairs. */ @JvmOverloads - fun simpleEcKeyRing(userId: CharSequence?, - passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = buildKeyRing().apply { - setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - addSubkey(getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) - setPassphrase(passphrase) - if (userId != null) { - addUserId(userId.toString()) - } - }.build() + fun simpleEcKeyRing( + userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey( + getBuilder( + KeyType.EDDSA(EdDSACurve._Ed25519), + KeyFlag.CERTIFY_OTHER, + KeyFlag.SIGN_DATA)) + addSubkey( + getBuilder( + KeyType.XDH(XDHSpec._X25519), + KeyFlag.ENCRYPT_STORAGE, + KeyFlag.ENCRYPT_COMMS)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId.toString()) + } + } + .build() /** - * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. - * The EdDSA primary key is used for signing messages and certifying the sub key. - * The XDH subkey is used for encryption and decryption of messages. + * Creates a key ring consisting of an ed25519 EdDSA primary key and a X25519 XDH subkey. The + * EdDSA primary key is used for signing messages and certifying the sub key. The XDH subkey is + * used for encryption and decryption of messages. * * @param userId user-id * @param passphrase Password of the private key. Can be null or blank for an unencrypted key. - * * @return {@link PGPSecretKeyRing} containing the key pairs. */ - fun simpleEcKeyRing(userId: CharSequence?, - password: String? - ): PGPSecretKeyRing = password.let { - if (it.isNullOrBlank()) { - simpleEcKeyRing(userId, Passphrase.emptyPassphrase()) - } else { - simpleEcKeyRing(userId, Passphrase.fromPassword(it)) + fun simpleEcKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + simpleEcKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + simpleEcKeyRing(userId, Passphrase.fromPassword(it)) + } } - } /** - * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify - * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to + * certify an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. * * @param userId primary user id * @param passphrase passphrase for the private key. Can be empty for an unencrypted key. * @return key ring */ @JvmOverloads - fun modernKeyRing(userId: CharSequence?, - passphrase: Passphrase = Passphrase.emptyPassphrase() - ): PGPSecretKeyRing = buildKeyRing().apply { - setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - addSubkey(getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - addSubkey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - setPassphrase(passphrase) - if (userId != null) { - addUserId(userId) - } - }.build() + fun modernKeyRing( + userId: CharSequence?, + passphrase: Passphrase = Passphrase.emptyPassphrase() + ): PGPSecretKeyRing = + buildKeyRing() + .apply { + setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + addSubkey( + getBuilder( + KeyType.XDH(XDHSpec._X25519), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + addSubkey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + setPassphrase(passphrase) + if (userId != null) { + addUserId(userId) + } + } + .build() /** - * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to certify - * an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. + * Generate a modern PGP key ring consisting of an ed25519 EdDSA primary key which is used to + * certify an X25519 XDH encryption subkey and an ed25519 EdDSA signing key. * * @param userId primary user id * @param password passphrase for the private key. Can be null or blank for an unencrypted key. * @return key ring */ - fun modernKeyRing(userId: CharSequence?, - password: String? - ): PGPSecretKeyRing = password.let { - if (it.isNullOrBlank()) { - modernKeyRing(userId, Passphrase.emptyPassphrase()) - } else { - modernKeyRing(userId, Passphrase.fromPassword(it)) + fun modernKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = + password.let { + if (it.isNullOrBlank()) { + modernKeyRing(userId, Passphrase.emptyPassphrase()) + } else { + modernKeyRing(userId, Passphrase.fromPassword(it)) + } } - } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt index fd0b8635..bc8d5599 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt @@ -4,18 +4,18 @@ package org.pgpainless.key.generation +import java.util.* import org.bouncycastle.openpgp.PGPSignatureSubpacketVector import org.pgpainless.algorithm.KeyFlag import org.pgpainless.key.generation.type.KeyType import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper -import java.util.* data class KeySpec( - val keyType: KeyType, - val subpacketGenerator: SignatureSubpackets, - val isInheritedSubPackets: Boolean, - val keyCreationDate: Date + val keyType: KeyType, + val subpacketGenerator: SignatureSubpackets, + val isInheritedSubPackets: Boolean, + val keyCreationDate: Date ) { val subpackets: PGPSignatureSubpacketVector @@ -25,4 +25,4 @@ data class KeySpec( @JvmStatic fun getBuilder(type: KeyType, vararg flags: KeyFlag) = KeySpecBuilder(type, *flags) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt index a430e796..03291f2d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt @@ -4,41 +4,47 @@ package org.pgpainless.key.generation +import java.util.* import org.pgpainless.PGPainless import org.pgpainless.algorithm.* import org.pgpainless.key.generation.type.KeyType import org.pgpainless.signature.subpackets.SelfSignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpackets import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -import java.util.* -class KeySpecBuilder constructor( - private val type: KeyType, - private val keyFlags: List, +class KeySpecBuilder +constructor( + private val type: KeyType, + private val keyFlags: List, ) : KeySpecBuilderInterface { private val hashedSubpackets: SelfSignatureSubpackets = SignatureSubpackets() private val algorithmSuite: AlgorithmSuite = PGPainless.getPolicy().keyGenerationAlgorithmSuite - private var preferredCompressionAlgorithms: Set = algorithmSuite.compressionAlgorithms + private var preferredCompressionAlgorithms: Set = + algorithmSuite.compressionAlgorithms private var preferredHashAlgorithms: Set = algorithmSuite.hashAlgorithms - private var preferredSymmetricAlgorithms: Set = algorithmSuite.symmetricKeyAlgorithms + private var preferredSymmetricAlgorithms: Set = + algorithmSuite.symmetricKeyAlgorithms private var keyCreationDate = Date() - constructor(type: KeyType, vararg keyFlags: KeyFlag): this(type, listOf(*keyFlags)) + constructor(type: KeyType, vararg keyFlags: KeyFlag) : this(type, listOf(*keyFlags)) init { SignatureSubpacketsUtil.assureKeyCanCarryFlags(type, *keyFlags.toTypedArray()) } - override fun overridePreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): KeySpecBuilder = apply { - this.preferredCompressionAlgorithms = algorithms.toSet() - } + override fun overridePreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): KeySpecBuilder = apply { this.preferredCompressionAlgorithms = algorithms.toSet() } - override fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder = apply { - this.preferredHashAlgorithms = algorithms.toSet() - } + override fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder = + apply { + this.preferredHashAlgorithms = algorithms.toSet() + } - override fun overridePreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): KeySpecBuilder = apply { + override fun overridePreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): KeySpecBuilder = apply { require(!algorithms.contains(SymmetricKeyAlgorithm.NULL)) { "NULL (unencrypted) is an invalid symmetric key algorithm preference." } @@ -50,14 +56,14 @@ class KeySpecBuilder constructor( } override fun build(): KeySpec { - return hashedSubpackets.apply { - setKeyFlags(keyFlags) - setPreferredCompressionAlgorithms(preferredCompressionAlgorithms) - setPreferredHashAlgorithms(preferredHashAlgorithms) - setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms) - setFeatures(Feature.MODIFICATION_DETECTION) - }.let { - KeySpec(type, hashedSubpackets as SignatureSubpackets, false, keyCreationDate) - } + return hashedSubpackets + .apply { + setKeyFlags(keyFlags) + setPreferredCompressionAlgorithms(preferredCompressionAlgorithms) + setPreferredHashAlgorithms(preferredHashAlgorithms) + setPreferredSymmetricKeyAlgorithms(preferredSymmetricAlgorithms) + setFeatures(Feature.MODIFICATION_DETECTION) + } + .let { KeySpec(type, hashedSubpackets as SignatureSubpackets, false, keyCreationDate) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt index fcafb9c1..7fb767e4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilderInterface.kt @@ -4,20 +4,24 @@ package org.pgpainless.key.generation +import java.util.* import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm -import java.util.* interface KeySpecBuilderInterface { - fun overridePreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): KeySpecBuilder + fun overridePreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): KeySpecBuilder fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder - fun overridePreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): KeySpecBuilder + fun overridePreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): KeySpecBuilder fun setKeyCreationDate(creationDate: Date): KeySpecBuilder fun build(): KeySpec -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt index 377fbb94..1ff63604 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyLength.kt @@ -7,4 +7,4 @@ package org.pgpainless.key.generation.type interface KeyLength { val length: Int -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt index 105962b9..bc1497f9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.generation.type +import java.security.spec.AlgorithmParameterSpec import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.ecc.EllipticCurve import org.pgpainless.key.generation.type.ecc.ecdh.ECDH @@ -14,7 +15,6 @@ import org.pgpainless.key.generation.type.rsa.RSA import org.pgpainless.key.generation.type.rsa.RsaLength import org.pgpainless.key.generation.type.xdh.XDH import org.pgpainless.key.generation.type.xdh.XDHSpec -import java.security.spec.AlgorithmParameterSpec @Suppress("INAPPLICABLE_JVM_NAME") // https://youtrack.jetbrains.com/issue/KT-31420 interface KeyType { @@ -35,20 +35,22 @@ interface KeyType { /** * Return the strength of the key in bits. + * * @return strength of the key in bits */ val bitStrength: Int /** - * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the key. + * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the + * key. * * @return algorithm parameter spec */ val algorithmSpec: AlgorithmParameterSpec /** - * Return true if the key that is generated from this type is able to carry the SIGN_DATA key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. + * Return true if the key that is generated from this type is able to carry the SIGN_DATA key + * flag. See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. * * @return true if the key can sign. */ @@ -56,8 +58,8 @@ interface KeyType { @JvmName("canSign") get() = algorithm.signingCapable /** - * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. + * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER + * key flag. See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. * * @return true if the key is able to certify other keys */ @@ -65,8 +67,8 @@ interface KeyType { @JvmName("canCertify") get() = canSign /** - * Return true if the key that is generated from this type is able to carry the AUTHENTICATION key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. + * Return true if the key that is generated from this type is able to carry the AUTHENTICATION + * key flag. See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. * * @return true if the key can be used for authentication purposes. */ @@ -74,8 +76,8 @@ interface KeyType { @JvmName("canAuthenticate") get() = canSign /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS + * key flag. See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. * * @return true if the key can encrypt communication */ @@ -83,8 +85,8 @@ interface KeyType { @JvmName("canEncryptCommunication") get() = algorithm.encryptionCapable /** - * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE key flag. - * See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. + * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE + * key flag. See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. * * @return true if the key can encrypt for storage */ @@ -92,19 +94,14 @@ interface KeyType { @JvmName("canEncryptStorage") get() = algorithm.encryptionCapable companion object { - @JvmStatic - fun RSA(length: RsaLength): RSA = RSA.withLength(length) + @JvmStatic fun RSA(length: RsaLength): RSA = RSA.withLength(length) - @JvmStatic - fun ECDH(curve: EllipticCurve): ECDH = ECDH.fromCurve(curve) + @JvmStatic fun ECDH(curve: EllipticCurve): ECDH = ECDH.fromCurve(curve) - @JvmStatic - fun ECDSA(curve: EllipticCurve): ECDSA = ECDSA.fromCurve(curve) + @JvmStatic fun ECDSA(curve: EllipticCurve): ECDSA = ECDSA.fromCurve(curve) - @JvmStatic - fun EDDSA(curve: EdDSACurve): EdDSA = EdDSA.fromCurve(curve) + @JvmStatic fun EDDSA(curve: EdDSACurve): EdDSA = EdDSA.fromCurve(curve) - @JvmStatic - fun XDH(curve: XDHSpec): XDH = XDH.fromSpec(curve) + @JvmStatic fun XDH(curve: XDHSpec): XDH = XDH.fromSpec(curve) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt index 287df67f..d9b51cb3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt @@ -4,19 +4,20 @@ package org.pgpainless.key.generation.type.ecc - /** * Elliptic curves for use with [org.pgpainless.key.generation.type.ecc.ecdh.ECDH] and - * [org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA]. - * For curve25519 related curve definitions see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa.EdDSACurve]. + * [org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA]. For curve25519 related curve definitions + * see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa.EdDSACurve]. */ -enum class EllipticCurve( - val curveName: String, - val bitStrength: Int -) { - _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see https://tools.ietf.org/search/rfc4492#page-32 - _P384("secp384r1", 384), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 - _P521("secp521r1", 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 +enum class EllipticCurve(val curveName: String, val bitStrength: Int) { + _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see + // https://tools.ietf.org/search/rfc4492#page-32 + _P384( + "secp384r1", + 384), // secp384r1 is equivalent to P-384, see https://tools.ietf.org/search/rfc4492#page-32 + _P521( + "secp521r1", + 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 _SECP256K1("secp256k1", 256), _BRAINPOOLP256R1("brainpoolP256r1", 256), _BRAINPOOLP384R1("brainpoolP384r1", 384), @@ -24,4 +25,4 @@ enum class EllipticCurve( ; fun getName(): String = curveName -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt index 6bab2fcc..04e196e0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdh/ECDH.kt @@ -16,7 +16,6 @@ class ECDH private constructor(val curve: EllipticCurve) : KeyType { override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) companion object { - @JvmStatic - fun fromCurve(curve: EllipticCurve) = ECDH(curve) + @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDH(curve) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt index b7a0b94f..1784b49d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/ecdsa/ECDSA.kt @@ -16,7 +16,6 @@ class ECDSA private constructor(val curve: EllipticCurve) : KeyType { override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) companion object { - @JvmStatic - fun fromCurve(curve: EllipticCurve) = ECDSA(curve) + @JvmStatic fun fromCurve(curve: EllipticCurve) = ECDSA(curve) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt index d1e51a8e..6130328a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt @@ -15,7 +15,6 @@ class EdDSA private constructor(val curve: EdDSACurve) : KeyType { override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) companion object { - @JvmStatic - fun fromCurve(curve: EdDSACurve) = EdDSA(curve) + @JvmStatic fun fromCurve(curve: EdDSACurve) = EdDSA(curve) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt index 52b6949b..943c8237 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt @@ -4,11 +4,9 @@ package org.pgpainless.key.generation.type.eddsa -enum class EdDSACurve( - val curveName: String, - val bitStrength: Int) { +enum class EdDSACurve(val curveName: String, val bitStrength: Int) { _Ed25519("ed25519", 256), ; fun getName() = curveName -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt index 6cfbc8a7..d925fc3d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamal.kt @@ -22,7 +22,6 @@ class ElGamal private constructor(length: ElGamalLength) : KeyType { override val algorithmSpec = ElGamalParameterSpec(length.p, length.g) companion object { - @JvmStatic - fun withLength(length: ElGamalLength) = ElGamal(length) + @JvmStatic fun withLength(length: ElGamalLength) = ElGamal(length) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt index 9eae195c..2d29b88d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/elgamal/ElGamalLength.kt @@ -4,61 +4,54 @@ package org.pgpainless.key.generation.type.elgamal -import org.pgpainless.key.generation.type.KeyLength import java.math.BigInteger +import org.pgpainless.key.generation.type.KeyLength /** * The following primes are taken from RFC-3526. * - * @see - * RFC-3526: More Modular Exponential (MODP) Diffie-Hellman groups for Internet Key Exchange (IKE) - * + * @see RFC-3526: More Modular Exponential (MODP) + * Diffie-Hellman groups for Internet Key Exchange (IKE) * @deprecated the use of ElGamal keys is no longer recommended. */ - @Deprecated("The use of ElGamal keys is no longer recommended.") -enum class ElGamalLength( - override val length: Int, - p: String, - g: String -) : KeyLength { +enum class ElGamalLength(override val length: Int, p: String, g: String) : KeyLength { - /** - * prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. - * generator: 2 - */ - _1536(1536, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", "2"), + /** prime: 2^1536 - 2^1472 - 1 + 2^64 * { [2^1406 pi] + 741804 }. generator: 2 */ + _1536( + 1536, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF", + "2"), - /** - * prime: 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }. - * generator: 2 - */ - _2048(2048, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", "2"), + /** prime: 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }. generator: 2 */ + _2048( + 2048, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", + "2"), - /** - * prime: 2^3072 - 2^3008 - 1 + 2^64 * { [2^2942 pi] + 1690314 }. - * generator: 2 - */ - _3072(3072, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", "2"), + /** prime: 2^3072 - 2^3008 - 1 + 2^64 * { [2^2942 pi] + 1690314 }. generator: 2 */ + _3072( + 3072, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF", + "2"), - /** - * prime: 2^4096 - 2^4032 - 1 + 2^64 * { [2^3966 pi] + 240904 }. - * generator: 2 - */ - _4096(4096, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF", "2"), + /** prime: 2^4096 - 2^4032 - 1 + 2^64 * { [2^3966 pi] + 240904 }. generator: 2 */ + _4096( + 4096, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF", + "2"), - /** - * prime: 2^6144 - 2^6080 - 1 + 2^64 * { [2^6014 pi] + 929484 }. - * generator: 2 - */ - _6144(6144, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF", "2"), + /** prime: 2^6144 - 2^6080 - 1 + 2^64 * { [2^6014 pi] + 929484 }. generator: 2 */ + _6144( + 6144, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF", + "2"), - /** - * prime: 2^8192 - 2^8128 - 1 + 2^64 * { [2^8062 pi] + 4743158 }. - * generator: 2 - */ - _8192(8192, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", "2") - ; + /** prime: 2^8192 - 2^8128 - 1 + 2^64 * { [2^8062 pi] + 4743158 }. generator: 2 */ + _8192( + 8192, + "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF", + "2"); val p: BigInteger val g: BigInteger @@ -67,6 +60,4 @@ enum class ElGamalLength( this.p = BigInteger(p, 16) this.g = BigInteger(g, 16) } - - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt index 1f8c0509..39ddbbbb 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RSA.kt @@ -4,14 +4,12 @@ package org.pgpainless.key.generation.type.rsa +import java.security.spec.RSAKeyGenParameterSpec import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType -import java.security.spec.RSAKeyGenParameterSpec -/** - * Key type that specifies the RSA_GENERAL algorithm. - */ -class RSA private constructor(length: RsaLength): KeyType { +/** Key type that specifies the RSA_GENERAL algorithm. */ +class RSA private constructor(length: RsaLength) : KeyType { override val name = "RSA" override val algorithm = PublicKeyAlgorithm.RSA_GENERAL @@ -19,7 +17,6 @@ class RSA private constructor(length: RsaLength): KeyType { override val algorithmSpec = RSAKeyGenParameterSpec(length.length, RSAKeyGenParameterSpec.F4) companion object { - @JvmStatic - fun withLength(length: RsaLength) = RSA(length) + @JvmStatic fun withLength(length: RsaLength) = RSA(length) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt index ae8bb804..7837a1f5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/rsa/RsaLength.kt @@ -14,4 +14,4 @@ enum class RsaLength(override val length: Int) : KeyLength { _3072(3072), _4096(4096), _8192(8192) -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt index 06888237..8a95fc3b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt @@ -8,14 +8,13 @@ import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType -class XDH private constructor(spec: XDHSpec): KeyType { +class XDH private constructor(spec: XDHSpec) : KeyType { override val name = "XDH" override val algorithm = PublicKeyAlgorithm.ECDH override val bitStrength = spec.bitStrength override val algorithmSpec = ECNamedCurveGenParameterSpec(spec.algorithmName) companion object { - @JvmStatic - fun fromSpec(spec: XDHSpec) = XDH(spec) + @JvmStatic fun fromSpec(spec: XDHSpec) = XDH(spec) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt index 9486365f..36fcfcaa 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt @@ -4,12 +4,9 @@ package org.pgpainless.key.generation.type.xdh -enum class XDHSpec( - val algorithmName: String, - val curveName: String, - val bitStrength: Int) { +enum class XDHSpec(val algorithmName: String, val curveName: String, val bitStrength: Int) { _X25519("X25519", "curve25519", 256), ; fun getName() = algorithmName -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index f49a6ca4..a6891f0f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -11,78 +11,79 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil -abstract class KeyAccessor( - protected val info: KeyRingInfo, - protected val key: SubkeyIdentifier -) { +abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: SubkeyIdentifier) { /** - * Depending on the way we address the key (key-id or user-id), return the respective [PGPSignature] - * which contains the algorithm preferences we are going to use. + * Depending on the way we address the key (key-id or user-id), return the respective + * [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. + * 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 */ abstract val signatureWithPreferences: PGPSignature - /** - * Preferred symmetric key encryption algorithms. - */ + /** Preferred symmetric key encryption algorithms. */ val preferredSymmetricKeyAlgorithms: Set - get() = SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signatureWithPreferences) + get() = + SignatureSubpacketsUtil.parsePreferredSymmetricKeyAlgorithms(signatureWithPreferences) - /** - * Preferred hash algorithms. - */ + /** Preferred hash algorithms. */ val preferredHashAlgorithms: Set get() = SignatureSubpacketsUtil.parsePreferredHashAlgorithms(signatureWithPreferences) - /** - * Preferred compression algorithms. - */ + /** Preferred compression algorithms. */ val preferredCompressionAlgorithms: Set - get() = SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signatureWithPreferences) + get() = + SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signatureWithPreferences) /** - * Address the key via a user-id (e.g. `Alice `). - * In this case we are sourcing preferred algorithms from the user-id certification first. + * Address the key via a user-id (e.g. `Alice `). In this case we are + * sourcing preferred algorithms from the user-id certification first. */ - class ViaUserId(info: KeyRingInfo, key: SubkeyIdentifier, private val userId: CharSequence): KeyAccessor(info, key) { + class ViaUserId(info: KeyRingInfo, key: SubkeyIdentifier, private val userId: CharSequence) : + KeyAccessor(info, key) { override val signatureWithPreferences: PGPSignature - get() = checkNotNull(info.getLatestUserIdCertification(userId.toString())) { - "No valid user-id certification signature found for '$userId'." - } + get() = + checkNotNull(info.getLatestUserIdCertification(userId.toString())) { + "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. + * Address the key via key-id. In this case we are sourcing preferred algorithms from the keys + * direct-key signature first. */ class ViaKeyId(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { override val signatureWithPreferences: PGPSignature get() { - // If the key is located by Key ID, the algorithm of the primary User ID of the key provides the + // If the key is located by Key ID, the algorithm of the primary User ID of the key + // provides the // preferred symmetric algorithm. - info.primaryUserId?.let { - userId -> info.getLatestUserIdCertification(userId).let { if (it != null) return it } + info.primaryUserId?.let { userId -> + info.getLatestUserIdCertification(userId).let { if (it != null) return it } } - return checkNotNull(info.latestDirectKeySelfSignature) { "No valid signature found." } + return checkNotNull(info.latestDirectKeySelfSignature) { + "No valid signature found." + } } } - class SubKey(info: KeyRingInfo, key: SubkeyIdentifier): KeyAccessor(info, key) { + class SubKey(info: KeyRingInfo, key: SubkeyIdentifier) : KeyAccessor(info, key) { override val signatureWithPreferences: PGPSignature - get() = checkNotNull( + get() = + checkNotNull( if (key.isPrimaryKey) { - info.latestDirectKeySelfSignature ?: - info.primaryUserId?.let { info.getLatestUserIdCertification(it) } + info.latestDirectKeySelfSignature + ?: info.primaryUserId?.let { info.getLatestUserIdCertification(it) } } else { info.getCurrentSubkeyBindingSignature(key.subkeyId) + }) { + "No valid signature found." } - ) { "No valid signature found." } - } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt index 652cf22d..f510af3e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt @@ -12,64 +12,70 @@ import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey @Deprecated("Deprecated in favor of extension functions to PGPSecretKey and PGPPublicKey.") -class KeyInfo private constructor( - val secretKey: PGPSecretKey?, - val publicKey: PGPPublicKey) { +class KeyInfo private constructor(val secretKey: PGPSecretKey?, val publicKey: PGPPublicKey) { - constructor(secretKey: PGPSecretKey): this(secretKey, secretKey.publicKey) - constructor(publicKey: PGPPublicKey): this(null, publicKey) + constructor(secretKey: PGPSecretKey) : this(secretKey, secretKey.publicKey) + + constructor(publicKey: PGPPublicKey) : this(null, publicKey) /** - * Return the name of the elliptic curve used by this key, or throw an [IllegalArgumentException] if the key - * is not based on elliptic curves, or on an unknown curve. + * Return the name of the elliptic curve used by this key, or throw an + * [IllegalArgumentException] if the key is not based on elliptic curves, or on an unknown + * curve. */ - @Deprecated("Deprecated in favor of calling getCurveName() on the PGPPublicKey itself.", - ReplaceWith("publicKey.getCurveName()")) + @Deprecated( + "Deprecated in favor of calling getCurveName() on the PGPPublicKey itself.", + ReplaceWith("publicKey.getCurveName()")) val curveName: String get() = publicKey.getCurveName() /** - * Return true, if the secret key is encrypted. - * This method returns false, if the secret key is null. + * Return true, if the secret key is encrypted. This method returns false, if the secret key is + * null. */ - @Deprecated("Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isEncrypted()")) + @Deprecated( + "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) val isEncrypted: Boolean get() = secretKey?.isEncrypted() ?: false /** - * Return true, if the secret key is decrypted. - * This method returns true, if the secret key is null. + * Return true, if the secret key is decrypted. This method returns true, if the secret key is + * null. */ - @Deprecated("Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isDecrypted()")) + @Deprecated( + "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) val isDecrypted: Boolean get() = secretKey?.isDecrypted() ?: true /** - * Return true, if the secret key is using the GNU_DUMMY_S2K s2k type. - * This method returns false, if the secret key is null. + * Return true, if the secret key is using the GNU_DUMMY_S2K s2k type. This method returns + * false, if the secret key is null. */ - @Deprecated("Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", - ReplaceWith("secretKey.hasDummyS2K()")) + @Deprecated( + "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) val hasDummyS2K: Boolean - @JvmName("hasDummyS2K") - get() = secretKey?.hasDummyS2K() ?: false + @JvmName("hasDummyS2K") get() = secretKey?.hasDummyS2K() ?: false companion object { @JvmStatic - @Deprecated("Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isEncrypted()")) + @Deprecated( + "Deprecated in favor of calling isEncrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isEncrypted()")) fun isEncrypted(secretKey: PGPSecretKey?) = secretKey.isEncrypted() @JvmStatic - @Deprecated("Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", - ReplaceWith("secretKey.isDecrypted()")) + @Deprecated( + "Deprecated in favor of calling isDecrypted() on the PGPSecretKey itself.", + ReplaceWith("secretKey.isDecrypted()")) fun isDecrypted(secretKey: PGPSecretKey?) = secretKey.isDecrypted() @JvmStatic - @Deprecated("Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", - ReplaceWith("secretKey.hasDummyS2K()")) + @Deprecated( + "Deprecated in favor of calling hasDummyS2K() on the PGPSecretKey itself.", + ReplaceWith("secretKey.hasDummyS2K()")) fun hasDummyS2K(secretKey: PGPSecretKey?) = secretKey.hasDummyS2K() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 208d8060..9f0bbc87 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.info +import java.util.* import openpgp.openPgpKeyId import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* @@ -19,240 +20,210 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate import org.pgpainless.util.DateUtil import org.slf4j.LoggerFactory -import java.util.* class KeyRingInfo( - val keys: PGPKeyRing, - val policy: Policy = PGPainless.getPolicy(), - val referenceDate: Date = Date()) { + val keys: PGPKeyRing, + val policy: Policy = PGPainless.getPolicy(), + val referenceDate: Date = Date() +) { @JvmOverloads - constructor(keys: PGPKeyRing, referenceDate: Date = Date()): this(keys, PGPainless.getPolicy(), referenceDate) + constructor( + keys: PGPKeyRing, + referenceDate: Date = Date() + ) : this(keys, PGPainless.getPolicy(), referenceDate) private val signatures: Signatures = Signatures(keys, referenceDate, policy) - /** - * Primary {@link PGPPublicKey}.´ - */ + /** Primary {@link PGPPublicKey}.´ */ val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) - /** - * Primary key ID. - */ + /** Primary key ID. */ val keyId: Long = publicKey.keyID - /** - * Primary key fingerprint. - */ + /** Primary key fingerprint. */ val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys) - /** - * All User-IDs (valid, expired, revoked). - */ + /** All User-IDs (valid, expired, revoked). */ val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) - /** - * Primary User-ID. - */ + /** Primary User-ID. */ val primaryUserId = findPrimaryUserId() - /** - * Revocation State. - */ + /** Revocation State. */ val revocationState = signatures.primaryKeyRevocation.toRevocationState() /** * Return the date on which the primary key was revoked, or null if it has not yet been revoked. * * @return revocation date or null */ - val revocationDate: Date? = if (revocationState.isSoftRevocation()) revocationState.date else null + val revocationDate: Date? = + if (revocationState.isSoftRevocation()) revocationState.date else null /** * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing]. */ - val secretKey: PGPSecretKey? = when(keys) { - is PGPSecretKeyRing -> keys.secretKey!! - else -> null - } + val secretKey: PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.secretKey!! + else -> null + } - /** - * OpenPGP key version. - */ + /** OpenPGP key version. */ val version: Int = publicKey.version /** - * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. - * The first key in the list being the primary key. - * Note that the list is unmodifiable. + * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. The first key in the list + * being the primary key. Note that the list is unmodifiable. * * @return list of public keys */ val publicKeys: List = keys.publicKeys.asSequence().toList() - /** - * All secret keys. - * If the key ring is a [PGPPublicKeyRing], then return an empty list. - */ - val secretKeys: List = when(keys) { - is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() - else -> listOf() - } + /** All secret keys. If the key ring is a [PGPPublicKeyRing], then return an empty list. */ + val secretKeys: List = + when (keys) { + is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() + else -> listOf() + } - /** - * List of valid public subkeys. - */ - val validSubkeys: List = keys.publicKeys.asSequence() - .filter { isKeyValidlyBound(it.keyID) } - .toList() + /** List of valid public subkeys. */ + val validSubkeys: List = + keys.publicKeys.asSequence().filter { isKeyValidlyBound(it.keyID) }.toList() - /** - * List of valid user-IDs. - */ + /** List of valid user-IDs. */ val validUserIds: List = userIds.filter { isUserIdBound(it) } - /** - * List of valid and expired user-IDs. - */ - val validAndExpiredUserIds: List = userIds.filter { - val certification = signatures.userIdCertifications[it] ?: return@filter false - val revocation = signatures.userIdRevocations[it] ?: return@filter true - return@filter !revocation.isHardRevocation && certification.creationTime > revocation.creationTime - } + /** List of valid and expired user-IDs. */ + val validAndExpiredUserIds: List = + userIds.filter { + val certification = signatures.userIdCertifications[it] ?: return@filter false + val revocation = signatures.userIdRevocations[it] ?: return@filter true + return@filter !revocation.isHardRevocation && + certification.creationTime > revocation.creationTime + } - /** - * List of email addresses that can be extracted from the user-IDs. - */ - val emailAddresses: List = userIds.mapNotNull { - PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 -> - if (m1.find()) m1.group(1) - else PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 -> - if(m2.find()) m2.group(1) else null + /** List of email addresses that can be extracted from the user-IDs. */ + val emailAddresses: List = + userIds.mapNotNull { + PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 -> + if (m1.find()) m1.group(1) + else + PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 -> + if (m2.find()) m2.group(1) else null + } } } - } - /** - * Newest direct-key self-signature on the primary key. - */ + /** Newest direct-key self-signature on the primary key. */ val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature - /** - * Newest primary-key revocation self-signature. - */ + /** Newest primary-key revocation self-signature. */ val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation - /** - * Public-key encryption-algorithm of the primary key. - */ + /** Public-key encryption-algorithm of the primary key. */ val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm) - /** - * Creation date of the primary key. - */ + /** Creation date of the primary key. */ val creationDate: Date = publicKey.creationTime!! - /** - * Latest date at which the key was modified (either by adding a subkey or self-signature). - */ + /** Latest date at which the key was modified (either by adding a subkey or self-signature). */ val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate() - /** - * True, if the underlying keyring is a [PGPSecretKeyRing]. - */ + /** True, if the underlying keyring is a [PGPSecretKeyRing]. */ val isSecretKey: Boolean = keys is PGPSecretKeyRing - /** - * True, if there are no encrypted secret keys. - */ - val isFullyDecrypted: Boolean = !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } + /** True, if there are no encrypted secret keys. */ + val isFullyDecrypted: Boolean = + !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } - /** - * True, if there are only encrypted secret keys. - */ - val isFullyEncrypted: Boolean = isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } + /** True, if there are only encrypted secret keys. */ + val isFullyEncrypted: Boolean = + isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } - /** - * List of public keys, whose secret key counterparts can be used to decrypt messages. - */ - val decryptionSubkeys: List = keys.publicKeys.asSequence().filter { - if (it.keyID != keyId) { - if (signatures.subkeyBindings[it.keyID] == null) { - LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") - return@filter false + /** List of public keys, whose secret key counterparts can be used to decrypt messages. */ + val decryptionSubkeys: List = + keys.publicKeys + .asSequence() + .filter { + if (it.keyID != keyId) { + if (signatures.subkeyBindings[it.keyID] == null) { + LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") + return@filter false + } + } + if (!it.isEncryptionKey) { + LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") + return@filter false + } + return@filter true } - } - if (!it.isEncryptionKey) { - LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") - return@filter false - } - return@filter true - }.toList() + .toList() - /** - * Expiration date of the primary key. - */ + /** Expiration date of the primary key. */ val primaryKeyExpirationDate: Date? get() { - val directKeyExpirationDate: Date? = latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } - val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() - val primaryUserIdCertification = possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } - val userIdExpirationDate: Date? = primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } + val directKeyExpirationDate: Date? = + latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } + val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() + val primaryUserIdCertification = + possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } + val userIdExpirationDate: Date? = + primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } - if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { - throw NoSuchElementException("No direct-key signature and no user-id signature found.") + if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { + throw NoSuchElementException( + "No direct-key signature and no user-id signature found.") + } + if (directKeyExpirationDate != null && userIdExpirationDate == null) { + return directKeyExpirationDate + } + if (directKeyExpirationDate == null) { + return userIdExpirationDate + } + return if (directKeyExpirationDate < userIdExpirationDate) directKeyExpirationDate + else userIdExpirationDate } - if (directKeyExpirationDate != null && userIdExpirationDate == null) { - return directKeyExpirationDate - } - if (directKeyExpirationDate == null) { - return userIdExpirationDate - } - return if (directKeyExpirationDate < userIdExpirationDate) - directKeyExpirationDate - else userIdExpirationDate - } - /** - * List of all subkeys that can be used to sign a message. - */ - val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } + /** List of all subkeys that can be used to sign a message. */ + val signingSubkeys: List = + validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } - /** - * Whether the key is usable for encryption. - */ + /** Whether the key is usable for encryption. */ val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) /** - * Whether the key is capable of signing messages. - * This field is also true, if the key contains a subkey that is capable of signing messages, but where the secret - * key is unavailable, e.g. because it was moved to a smart-card. + * Whether the key is capable of signing messages. This field is also true, if the key contains + * a subkey that is capable of signing messages, but where the secret key is unavailable, e.g. + * because it was moved to a smart-card. * * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. */ val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() - /** - * Whether the key is actually usable to sign messages. - */ - val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } + /** Whether the key is actually usable to sign messages. */ + val isUsableForSigning: Boolean = + isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } - /** - * [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. - */ + /** [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. */ val preferredHashAlgorithms: Set - get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) + get() = + primaryUserId?.let { getPreferredHashAlgorithms(it) } + ?: getPreferredHashAlgorithms(keyId) /** * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. */ val preferredSymmetricKeyAlgorithms: Set - get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) + get() = + primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } + ?: getPreferredSymmetricKeyAlgorithms(keyId) - /** - * [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. - */ + /** [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. */ val preferredCompressionAlgorithms: Set - get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) + get() = + primaryUserId?.let { getPreferredCompressionAlgorithms(it) } + ?: getPreferredCompressionAlgorithms(keyId) /** * Return the expiration date of the subkey with the provided fingerprint. @@ -272,13 +243,19 @@ class KeyRingInfo( */ fun getSubkeyExpirationDate(keyId: Long): Date? { if (publicKey.keyID == keyId) return primaryKeyExpirationDate - val subkey = getPublicKey(keyId) ?: throw NoSuchElementException("No subkey with key-ID ${keyId.openPgpKeyId()} found.") - val bindingSig = getCurrentSubkeyBindingSignature(keyId) ?: throw AssertionError("Subkey has no valid binding signature.") + val subkey = + getPublicKey(keyId) + ?: throw NoSuchElementException( + "No subkey with key-ID ${keyId.openPgpKeyId()} found.") + val bindingSig = + getCurrentSubkeyBindingSignature(keyId) + ?: throw AssertionError("Subkey has no valid binding signature.") return bindingSig.getKeyExpirationDate(subkey.creationTime) } /** - * Return the date after which the key can no longer be used to perform the given use-case, caused by expiration. + * Return the date after which the key can no longer be used to perform the given use-case, + * caused by expiration. * * @return expiration date for the given use-case */ @@ -289,17 +266,21 @@ class KeyRingInfo( val primaryKeyExpiration = primaryKeyExpirationDate val keysWithFlag: List = getKeysWithKeyFlag(use) - if (keysWithFlag.isEmpty()) throw NoSuchElementException("No key with the required key flag found.") + if (keysWithFlag.isEmpty()) + throw NoSuchElementException("No key with the required key flag found.") var nonExpiring = false - val latestSubkeyExpiration = keysWithFlag.map { key -> - getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } - }.filterNotNull().maxByOrNull { it } + val latestSubkeyExpiration = + keysWithFlag + .map { key -> + getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } + } + .filterNotNull() + .maxByOrNull { it } if (nonExpiring) return primaryKeyExpiration return if (primaryKeyExpiration == null) latestSubkeyExpiration - else if (latestSubkeyExpiration == null) - primaryKeyExpiration + else if (latestSubkeyExpiration == null) primaryKeyExpiration else minOf(primaryKeyExpiration, latestSubkeyExpiration) } @@ -318,17 +299,24 @@ class KeyRingInfo( * @param flag flag * @return keys with flag */ - fun getKeysWithKeyFlag(flag: KeyFlag): List = publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } + fun getKeysWithKeyFlag(flag: KeyFlag): List = + publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } /** * Return a list of all subkeys which can be used to encrypt a message for the given user-ID. * * @return encryption subkeys */ - fun getEncryptionSubkeys(userId: CharSequence?, purpose: EncryptionPurpose): List { + fun getEncryptionSubkeys( + userId: CharSequence?, + purpose: EncryptionPurpose + ): List { if (userId != null && !isUserIdValid(userId)) { - throw UnboundUserIdException(OpenPgpFingerprint.of(keys), userId.toString(), - getLatestUserIdCertification(userId), getUserIdRevocation(userId)) + throw UnboundUserIdException( + OpenPgpFingerprint.of(keys), + userId.toString(), + getLatestUserIdCertification(userId), + getUserIdRevocation(userId)) } return getEncryptionSubkeys(purpose) } @@ -341,42 +329,53 @@ class KeyRingInfo( fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { primaryKeyExpirationDate?.let { if (it < referenceDate) { - LOGGER.debug("Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}") + LOGGER.debug( + "Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}") return listOf() } } - return keys.publicKeys.asSequence().filter { - if (!isKeyValidlyBound(it.keyID)) { - LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") - return@filter false - } - - getSubkeyExpirationDate(it.keyID)?.let { exp -> - if (exp < referenceDate) { - LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") + return keys.publicKeys + .asSequence() + .filter { + if (!isKeyValidlyBound(it.keyID)) { + LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") return@filter false } - } - if (!it.isEncryptionKey) { - LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") - return@filter false - } + getSubkeyExpirationDate(it.keyID)?.let { exp -> + if (exp < referenceDate) { + LOGGER.debug( + "(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") + return@filter false + } + } - val keyFlags = getKeyFlagsOf(it.keyID) - when (purpose) { - EncryptionPurpose.COMMUNICATIONS -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) - EncryptionPurpose.STORAGE -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) - EncryptionPurpose.ANY -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + if (!it.isEncryptionKey) { + LOGGER.debug( + "(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") + return@filter false + } + + val keyFlags = getKeyFlagsOf(it.keyID) + when (purpose) { + EncryptionPurpose.COMMUNICATIONS -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) + EncryptionPurpose.STORAGE -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + EncryptionPurpose.ANY -> + return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || + keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) + } } - }.toList() + .toList() } /** * Return, whether the key is usable for encryption, given the purpose. * - * @return true, if the key can be used to encrypt a message according to the encryption-purpose. + * @return true, if the key can be used to encrypt a message according to the + * encryption-purpose. */ fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean { return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() @@ -387,29 +386,30 @@ class KeyRingInfo( * * @return possibly expired primary user-ID */ - fun getPossiblyExpiredPrimaryUserId(): String? = primaryUserId ?: userIds - .mapNotNull { userId -> - getLatestUserIdCertification(userId)?.let { userId to it } - } - .sortedByDescending { it.second.creationTime } - .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID }?.first + fun getPossiblyExpiredPrimaryUserId(): String? = + primaryUserId + ?: userIds + .mapNotNull { userId -> getLatestUserIdCertification(userId)?.let { userId to it } } + .sortedByDescending { it.second.creationTime } + .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID } + ?.first - /** - * Return the most-recently created self-signature on the key. - */ + /** Return the most-recently created self-signature on the key. */ private fun getMostRecentSignature(): PGPSignature? = - setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature).asSequence() - .plus(signatures.userIdCertifications.values) - .plus(signatures.userIdRevocations.values) - .plus(signatures.subkeyBindings.values) - .plus(signatures.subkeyRevocations.values) - .maxByOrNull { creationDate } + setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature) + .asSequence() + .plus(signatures.userIdCertifications.values) + .plus(signatures.userIdRevocations.values) + .plus(signatures.subkeyBindings.values) + .plus(signatures.subkeyRevocations.values) + .maxByOrNull { creationDate } /** * Return the creation time of the latest added subkey. * * @return latest key creation time */ - fun getLatestKeyCreationDate(): Date = validSubkeys.maxByOrNull { creationDate }?.creationTime + fun getLatestKeyCreationDate(): Date = + validSubkeys.maxByOrNull { creationDate }?.creationTime ?: throw AssertionError("Apparently there is no validly bound key in this key ring.") /** @@ -417,57 +417,63 @@ class KeyRingInfo( * * @return latest self-certification for the given user-ID. */ - fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = signatures.userIdCertifications[userId] + fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = + signatures.userIdCertifications[userId] /** * Return the latest revocation self-signature for the given user-ID * * @return latest user-ID revocation for the given user-ID */ - fun getUserIdRevocation(userId: CharSequence): PGPSignature? = signatures.userIdRevocations[userId] + fun getUserIdRevocation(userId: CharSequence): PGPSignature? = + signatures.userIdRevocations[userId] /** * Return the current binding signature for the subkey with the given key-ID. * * @return current subkey binding signature */ - fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = signatures.subkeyBindings[keyId] + fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = + signatures.subkeyBindings[keyId] /** * Return the current revocation signature for the subkey with the given key-ID. * * @return current subkey revocation signature */ - fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = signatures.subkeyRevocations[keyId] + fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = + signatures.subkeyRevocations[keyId] /** * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. + * * @param keyId key-id * @return list of key flags */ fun getKeyFlagsOf(keyId: Long): List = - if (keyId == publicKey.keyID) { - latestDirectKeySelfSignature?.let { sig -> - SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> - return flags - } + if (keyId == publicKey.keyID) { + latestDirectKeySelfSignature?.let { sig -> + SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> + return flags } - - primaryUserId?.let { - SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags -> - return flags - } - } - listOf() - } else { - getCurrentSubkeyBindingSignature(keyId)?.let { - SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> - return flags - } - } - listOf() } + primaryUserId?.let { + SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags + -> + return flags + } + } + listOf() + } else { + getCurrentSubkeyBindingSignature(keyId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> + return flags + } + } + listOf() + } + /** * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. * @@ -475,13 +481,15 @@ class KeyRingInfo( * @return key flags */ fun getKeyFlagsOf(userId: CharSequence): List = - if (!isUserIdValid(userId)) { - listOf() - } else { - getLatestUserIdCertification(userId)?.let { - SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf() - } ?: throw AssertionError("While user-id '$userId' was reported as valid, there appears to be no certification for it.") + if (!isUserIdValid(userId)) { + listOf() + } else { + getLatestUserIdCertification(userId)?.let { + SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf() } + ?: throw AssertionError( + "While user-id '$userId' was reported as valid, there appears to be no certification for it.") + } /** * Return the public key with the given key id from the provided key ring. @@ -497,13 +505,15 @@ class KeyRingInfo( * @param keyId key id * @return secret key or null */ - fun getSecretKey(keyId: Long): PGPSecretKey? = when(keys) { - is PGPSecretKeyRing -> keys.getSecretKey(keyId) - else -> null - } + fun getSecretKey(keyId: Long): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.getSecretKey(keyId) + else -> null + } /** - * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a smart-card). + * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a + * smart-card). * * @return availability of the secret key */ @@ -511,7 +521,8 @@ class KeyRingInfo( return getSecretKey(keyId)?.let { return if (it.s2K == null) true // Unencrypted key else it.s2K.type !in 100..110 // Secret key on smart-card - } ?: false // Missing secret key + } + ?: false // Missing secret key } /** @@ -520,7 +531,8 @@ class KeyRingInfo( * @param fingerprint fingerprint * @return public key or null */ - fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = keys.getPublicKey(fingerprint.keyId) + fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = + keys.getPublicKey(fingerprint.keyId) /** * Return the secret key with the given fingerprint. @@ -528,21 +540,21 @@ class KeyRingInfo( * @param fingerprint fingerprint * @return secret key or null */ - fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = when(keys) { - is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) - else -> null - } + fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) + else -> null + } /** * Return the public key matching the given [SubkeyIdentifier]. * * @return public key - * @throws IllegalArgumentException if the identifier's primary key does not match the primary key of the key. + * @throws IllegalArgumentException if the identifier's primary key does not match the primary + * key of the key. */ fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? { - require(identifier.primaryKeyId == publicKey.keyID) { - "Mismatching primary key ID." - } + require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." } return getPublicKey(identifier.subkeyId) } @@ -550,17 +562,19 @@ class KeyRingInfo( * Return the secret key matching the given [SubkeyIdentifier]. * * @return secret key - * @throws IllegalArgumentException if the identifier's primary key does not match the primary key of the key. + * @throws IllegalArgumentException if the identifier's primary key does not match the primary + * key of the key. */ - fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = when(keys) { - is PGPSecretKeyRing -> { - require(identifier.primaryKeyId == publicKey.keyID) { - "Mismatching primary key ID." + fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = + when (keys) { + is PGPSecretKeyRing -> { + require(identifier.primaryKeyId == publicKey.keyID) { + "Mismatching primary key ID." + } + keys.getSecretKey(identifier.subkeyId) } - keys.getSecretKey(identifier.subkeyId) + else -> null } - else -> null - } /** * Return true if the public key with the given key id is bound to the key ring properly. @@ -573,7 +587,8 @@ class KeyRingInfo( // Primary key -> Check Primary Key Revocation if (publicKey.keyID == this.publicKey.keyID) { - return if (signatures.primaryKeyRevocation != null && signatures.primaryKeyRevocation.isHardRevocation) { + return if (signatures.primaryKeyRevocation != null && + signatures.primaryKeyRevocation.isHardRevocation) { false } else signatures.primaryKeyRevocation == null } @@ -594,16 +609,18 @@ class KeyRingInfo( false } else { // Key is soft-revoked, not yet re-bound - (revocation.isExpired(referenceDate) || !revocation.creationTime.after(binding.creationTime)) + (revocation.isExpired(referenceDate) || + !revocation.creationTime.after(binding.creationTime)) } } else true } /** * Return the current primary user-id of the key ring. + * *

- * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, - * this method returns the first user-id on the key, otherwise null. + * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, this + * method returns the first user-id on the key, otherwise null. * * @return primary user-id or null */ @@ -612,94 +629,85 @@ class KeyRingInfo( return null } - return signatures.userIdCertifications.filter { (_, certification) -> - certification.hashedSubPackets.isPrimaryUserID - }.entries.maxByOrNull { (_, certification) -> - certification.creationTime - }?.key ?: signatures.userIdCertifications.keys.firstOrNull() + return signatures.userIdCertifications + .filter { (_, certification) -> certification.hashedSubPackets.isPrimaryUserID } + .entries + .maxByOrNull { (_, certification) -> certification.creationTime } + ?.key + ?: signatures.userIdCertifications.keys.firstOrNull() } - /** - * Return true, if the primary user-ID, as well as the given user-ID are valid and bound. - */ + /** Return true, if the primary user-ID, as well as the given user-ID are valid and bound. */ fun isUserIdValid(userId: CharSequence) = - if (primaryUserId == null) { - false - } else { - isUserIdBound(primaryUserId) && (if (userId == primaryUserId) true else isUserIdBound(userId)) - } + if (primaryUserId == null) { + false + } else { + isUserIdBound(primaryUserId) && + (if (userId == primaryUserId) true else isUserIdBound(userId)) + } - /** - * Return true, if the given user-ID is validly bound. - */ + /** Return true, if the given user-ID is validly bound. */ fun isUserIdBound(userId: CharSequence) = - signatures.userIdCertifications[userId]?.let { sig -> - if (sig.isExpired(referenceDate)) { - // certification expired - return false + signatures.userIdCertifications[userId]?.let { sig -> + if (sig.isExpired(referenceDate)) { + // certification expired + return false + } + if (sig.hashedSubPackets.isPrimaryUserID) { + getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> + // key expired? + if (expirationDate < referenceDate) return false } - if (sig.hashedSubPackets.isPrimaryUserID) { - getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> - // key expired? - if (expirationDate < referenceDate) return false - } + } + signatures.userIdRevocations[userId]?.let { rev -> + if (rev.isHardRevocation) { + return false // hard revoked -> invalid } - signatures.userIdRevocations[userId]?.let { rev -> - if (rev.isHardRevocation) { - return false // hard revoked -> invalid - } - sig.creationTime > rev.creationTime// re-certification after soft revocation? - } ?: true // certification, but no revocation - } ?: false // no certification + sig.creationTime > rev.creationTime // re-certification after soft revocation? + } + ?: true // certification, but no revocation + } + ?: false // no certification - /** - * [HashAlgorithm] preferences of the given user-ID. - */ + /** [HashAlgorithm] preferences of the given user-ID. */ fun getPreferredHashAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredHashAlgorithms } - /** - * [HashAlgorithm] preferences of the given key. - */ + /** [HashAlgorithm] preferences of the given key. */ fun getPreferredHashAlgorithms(keyId: Long): Set { return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms } - /** - * [SymmetricKeyAlgorithm] preferences of the given user-ID. - */ + /** [SymmetricKeyAlgorithm] preferences of the given user-ID. */ fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms } - /** - * [SymmetricKeyAlgorithm] preferences of the given key. - */ + /** [SymmetricKeyAlgorithm] preferences of the given key. */ fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set { - return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredSymmetricKeyAlgorithms + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) + .preferredSymmetricKeyAlgorithms } - /** - * [CompressionAlgorithm] preferences of the given user-ID. - */ + /** [CompressionAlgorithm] preferences of the given user-ID. */ fun getPreferredCompressionAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms } - /** - * [CompressionAlgorithm] preferences of the given key. - */ + /** [CompressionAlgorithm] preferences of the given key. */ fun getPreferredCompressionAlgorithms(keyId: Long): Set { - return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredCompressionAlgorithms + return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) + .preferredCompressionAlgorithms } val isUsableForThirdPartyCertification: Boolean = - isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) + isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor { if (getPublicKey(keyId) == null) { - throw NoSuchElementException("No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") + throw NoSuchElementException( + "No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") } if (userId != null && !userIds.contains(userId)) { throw NoSuchElementException("No user-id '$userId' found on this key.") @@ -713,26 +721,24 @@ class KeyRingInfo( companion object { - /** - * Evaluate the key for the given signature. - */ + /** Evaluate the key for the given signature. */ @JvmStatic - fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) = KeyRingInfo(keys, signature.creationTime!!) + fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) = + KeyRingInfo(keys, signature.creationTime!!) - private val PATTERN_EMAIL_FROM_USERID = "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern() - private val PATTERN_EMAIL_EXPLICIT = "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern() - - @JvmStatic - private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) + private val PATTERN_EMAIL_FROM_USERID = + "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern() + private val PATTERN_EMAIL_EXPLICIT = + "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern() + @JvmStatic private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) } - private class Signatures( - val keys: PGPKeyRing, - val referenceDate: Date, - val policy: Policy) { - val primaryKeyRevocation: PGPSignature? = SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) - val primaryKeySelfSignature: PGPSignature? = SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) + private class Signatures(val keys: PGPKeyRing, val referenceDate: Date, val policy: Policy) { + val primaryKeyRevocation: PGPSignature? = + SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) + val primaryKeySelfSignature: PGPSignature? = + SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) val userIdRevocations = mutableMapOf() val userIdCertifications = mutableMapOf() val subkeyRevocations = mutableMapOf() @@ -740,21 +746,21 @@ class KeyRingInfo( init { KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId -> - SignaturePicker.pickCurrentUserIdRevocationSignature(keys, userId, policy, referenceDate)?.let { - userIdRevocations[userId] = it - } - SignaturePicker.pickLatestUserIdCertificationSignature(keys, userId, policy, referenceDate)?.let { - userIdCertifications[userId] = it - } + SignaturePicker.pickCurrentUserIdRevocationSignature( + keys, userId, policy, referenceDate) + ?.let { userIdRevocations[userId] = it } + SignaturePicker.pickLatestUserIdCertificationSignature( + keys, userId, policy, referenceDate) + ?.let { userIdCertifications[userId] = it } } keys.publicKeys.asSequence().drop(1).forEach { subkey -> - SignaturePicker.pickCurrentSubkeyBindingRevocationSignature(keys, subkey, policy, referenceDate)?.let { - subkeyRevocations[subkey.keyID] = it - } - SignaturePicker.pickLatestSubkeyBindingSignature(keys, subkey, policy, referenceDate)?.let { - subkeyBindings[subkey.keyID] = it - } + SignaturePicker.pickCurrentSubkeyBindingRevocationSignature( + keys, subkey, policy, referenceDate) + ?.let { subkeyRevocations[subkey.keyID] = it } + SignaturePicker.pickLatestSubkeyBindingSignature( + keys, subkey, policy, referenceDate) + ?.let { subkeyBindings[subkey.keyID] = it } } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 3425145b..891d64e7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -4,6 +4,10 @@ package org.pgpainless.key.modification.secretkeyring +import java.util.* +import java.util.function.Predicate +import javax.annotation.Nonnull +import kotlin.NoSuchElementException import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.extensions.getKeyExpirationDate import org.bouncycastle.extensions.publicKeyAlgorithm @@ -30,19 +34,17 @@ import org.pgpainless.signature.builder.* import org.pgpainless.signature.subpackets.* import org.pgpainless.util.Passphrase import org.pgpainless.util.selection.userid.SelectUserId -import java.util.* -import java.util.function.Predicate -import javax.annotation.Nonnull -import kotlin.NoSuchElementException class SecretKeyRingEditor( - var secretKeyRing: PGPSecretKeyRing, - override val referenceTime: Date = Date() + var secretKeyRing: PGPSecretKeyRing, + override val referenceTime: Date = Date() ) : SecretKeyRingEditorInterface { - override fun addUserId(userId: CharSequence, - callback: SelfSignatureSubpackets.Callback?, - protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun addUserId( + userId: CharSequence, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { val sanitizedUserId = sanitizeUserId(userId).toString() val primaryKey = secretKeyRing.secretKey @@ -51,17 +53,28 @@ class SecretKeyRingEditor( "User-ID $userId is hard revoked and cannot be re-certified." } - val (hashAlgorithmPreferences, symmetricKeyAlgorithmPreferences, compressionAlgorithmPreferences) = try { - Triple(info.preferredHashAlgorithms, info.preferredSymmetricKeyAlgorithms, info.preferredCompressionAlgorithms) - } catch (e : IllegalStateException) { // missing user-id sig - val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite - Triple(algorithmSuite.hashAlgorithms, algorithmSuite.symmetricKeyAlgorithms, algorithmSuite.compressionAlgorithms) - } + val ( + hashAlgorithmPreferences, + symmetricKeyAlgorithmPreferences, + compressionAlgorithmPreferences) = + try { + Triple( + info.preferredHashAlgorithms, + info.preferredSymmetricKeyAlgorithms, + info.preferredCompressionAlgorithms) + } catch (e: IllegalStateException) { // missing user-id sig + val algorithmSuite = AlgorithmSuite.defaultAlgorithmSuite + Triple( + algorithmSuite.hashAlgorithms, + algorithmSuite.symmetricKeyAlgorithms, + algorithmSuite.compressionAlgorithms) + } - val builder = SelfSignatureBuilder(primaryKey, protector).apply { - hashedSubpackets.setSignatureCreationTime(referenceTime) - setSignatureType(SignatureType.POSITIVE_CERTIFICATION) - } + val builder = + SelfSignatureBuilder(primaryKey, protector).apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + setSignatureType(SignatureType.POSITIVE_CERTIFICATION) + } builder.hashedSubpackets.apply { setKeyFlags(info.getKeyFlagsOf(primaryKey.keyID)) setPreferredHashAlgorithms(hashAlgorithmPreferences) @@ -70,66 +83,109 @@ class SecretKeyRingEditor( setFeatures(Feature.MODIFICATION_DETECTION) } builder.applyCallback(callback) - secretKeyRing = injectCertification(secretKeyRing, sanitizedUserId, builder.build(primaryKey.publicKey, sanitizedUserId)) + secretKeyRing = + injectCertification( + secretKeyRing, + sanitizedUserId, + builder.build(primaryKey.publicKey, sanitizedUserId)) return this } - override fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun addPrimaryUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { val uid = sanitizeUserId(userId) val primaryKey = secretKeyRing.publicKey var info = inspectKeyRing(secretKeyRing, referenceTime) val primaryUserId = info.primaryUserId - val signature = if (primaryUserId == null) info.latestDirectKeySelfSignature else info.getLatestUserIdCertification(primaryUserId) + val signature = + if (primaryUserId == null) info.latestDirectKeySelfSignature + else info.getLatestUserIdCertification(primaryUserId) val previousKeyExpiration = signature?.getKeyExpirationDate(primaryKey.creationTime) // Add new primary user-id signature - addUserId(uid, object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - hashedSubpackets.apply { - setPrimaryUserId() - if (previousKeyExpiration != null) setKeyExpirationTime(primaryKey, previousKeyExpiration) - else setKeyExpirationTime(null) + addUserId( + uid, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + hashedSubpackets.apply { + setPrimaryUserId() + if (previousKeyExpiration != null) + setKeyExpirationTime(primaryKey, previousKeyExpiration) + else setKeyExpirationTime(null) + } } - } - }, protector) + }, + protector) // unmark previous primary user-ids to be non-primary info = inspectKeyRing(secretKeyRing, referenceTime) - info.validAndExpiredUserIds.filterNot { it == uid }.forEach { otherUserId -> - if (info.getLatestUserIdCertification(otherUserId)!!.hashedSubPackets.isPrimaryUserID) { - // We need to unmark this user-id as primary - addUserId(otherUserId, object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - hashedSubpackets.apply { - setPrimaryUserId(null) - setKeyExpirationTime(null) // non-primary - } - } - }, protector) + info.validAndExpiredUserIds + .filterNot { it == uid } + .forEach { otherUserId -> + if (info + .getLatestUserIdCertification(otherUserId)!! + .hashedSubPackets + .isPrimaryUserID) { + // We need to unmark this user-id as primary + addUserId( + otherUserId, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { + hashedSubpackets.apply { + setPrimaryUserId(null) + setKeyExpirationTime(null) // non-primary + } + } + }, + protector) + } } - } return this } - @Deprecated("Use of SelectUserId class is deprecated.", replaceWith = ReplaceWith("removeUserId(protector, predicate)")) - override fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { - return revokeUserIds(selector, protector, RevocationAttributes.createCertificateRevocation() + @Deprecated( + "Use of SelectUserId class is deprecated.", + replaceWith = ReplaceWith("removeUserId(protector, predicate)")) + override fun removeUserId( + selector: SelectUserId, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + return revokeUserIds( + selector, + protector, + RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) .withoutDescription()) } - override fun removeUserId(protector: SecretKeyRingProtector, predicate: (String) -> Boolean): SecretKeyRingEditorInterface { - return revokeUserIds(protector, RevocationAttributes.createCertificateRevocation() + override fun removeUserId( + protector: SecretKeyRingProtector, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + return revokeUserIds( + protector, + RevocationAttributes.createCertificateRevocation() .withReason(RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) .withoutDescription(), - predicate) + predicate) } - override fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun removeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { return removeUserId(protector) { uid -> userId == uid } } - override fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun replaceUserId( + oldUserId: CharSequence, + newUserId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { val oldUID = sanitizeUserId(oldUserId) val newUID = sanitizeUserId(newUserId) require(oldUID.isNotBlank()) { "Old user-ID cannot be empty." } @@ -137,148 +193,230 @@ class SecretKeyRingEditor( val info = inspectKeyRing(secretKeyRing, referenceTime) if (!info.isUserIdValid(oldUID)) { - throw NoSuchElementException("Key does not carry user-ID '$oldUID', or it is not valid.") + throw NoSuchElementException( + "Key does not carry user-ID '$oldUID', or it is not valid.") } - val oldCertification = info.getLatestUserIdCertification(oldUID) + val oldCertification = + info.getLatestUserIdCertification(oldUID) ?: throw AssertionError("Certification for old user-ID MUST NOT be null.") - addUserId(newUID, object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) - if (oldUID == info.primaryUserId && !oldCertification.hashedSubPackets.isPrimaryUserID) { - hashedSubpackets.setPrimaryUserId() + addUserId( + newUID, + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + oldCertification.hashedSubPackets, hashedSubpackets as SignatureSubpackets) + if (oldUID == info.primaryUserId && + !oldCertification.hashedSubPackets.isPrimaryUserID) { + hashedSubpackets.setPrimaryUserId() + } } - } - override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { - SignatureSubpacketsHelper.applyFrom(oldCertification.unhashedSubPackets, unhashedSubpackets as SignatureSubpackets) - } - }, protector) + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + oldCertification.unhashedSubPackets, + unhashedSubpackets as SignatureSubpackets) + } + }, + protector) return revokeUserId(oldUID, protector) } - override fun addSubKey(keySpec: KeySpec, - subkeyPassphrase: Passphrase, - protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { - val callback = object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - SignatureSubpacketsHelper.applyFrom(keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + override fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { + val callback = + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + SignatureSubpacketsHelper.applyFrom( + keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + } } - } return addSubKey(keySpec, subkeyPassphrase, callback, protector) } - override fun addSubKey(keySpec: KeySpec, - subkeyPassphrase: Passphrase, - callback: SelfSignatureSubpackets.Callback?, - protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { val keyPair = KeyRingBuilder.generateKeyPair(keySpec) - val subkeyProtector = PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) + val subkeyProtector = + PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() - return addSubKey(keyPair, callback, subkeyProtector, protector, keyFlags.removeFirst(), *keyFlags.toTypedArray()) + return addSubKey( + keyPair, + callback, + subkeyProtector, + protector, + keyFlags.removeFirst(), + *keyFlags.toTypedArray()) } - override fun addSubKey(subkey: PGPKeyPair, - callback: SelfSignatureSubpackets.Callback?, - subkeyProtector: SecretKeyRingProtector, - primaryKeyProtector: SecretKeyRingProtector, - keyFlag: KeyFlag, - vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface { + override fun addSubKey( + subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag + ): SecretKeyRingEditorInterface { val flags = listOf(keyFlag).plus(keyFlags) val subkeyAlgorithm = subkey.publicKey.publicKeyAlgorithm SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm) val bitStrength = subkey.publicKey.bitStrength - require(PGPainless.getPolicy().publicKeyAlgorithmPolicy.isAcceptable(subkeyAlgorithm, bitStrength)) { - "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." - } + require( + PGPainless.getPolicy() + .publicKeyAlgorithmPolicy + .isAcceptable(subkeyAlgorithm, bitStrength)) { + "Public key algorithm policy violation: $subkeyAlgorithm with bit strength $bitStrength is not acceptable." + } val primaryKey = secretKeyRing.secretKey val info = inspectKeyRing(secretKeyRing, referenceTime) - val hashAlgorithm = HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + val hashAlgorithm = + HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) .negotiateHashAlgorithm(info.preferredHashAlgorithms) - var secretSubkey = PGPSecretKey(subkey.privateKey, subkey.publicKey, + var secretSubkey = + PGPSecretKey( + subkey.privateKey, + subkey.publicKey, ImplementationFactory.getInstance().v4FingerprintCalculator, - false, subkeyProtector.getEncryptor(subkey.keyID)) - val skBindingBuilder = SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) + false, + subkeyProtector.getEncryptor(subkey.keyID)) + val skBindingBuilder = + SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector, hashAlgorithm) skBindingBuilder.apply { hashedSubpackets.setSignatureCreationTime(referenceTime) hashedSubpackets.setKeyFlags(flags) if (subkeyAlgorithm.isSigningCapable()) { - val pkBindingBuilder = PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) + val pkBindingBuilder = + PrimaryKeyBindingSignatureBuilder(secretSubkey, subkeyProtector, hashAlgorithm) pkBindingBuilder.hashedSubpackets.setSignatureCreationTime(referenceTime) hashedSubpackets.addEmbeddedSignature(pkBindingBuilder.build(primaryKey.publicKey)) } applyCallback(callback) } - secretSubkey = KeyRingUtils.secretKeyPlusSignature(secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) + secretSubkey = + KeyRingUtils.secretKeyPlusSignature( + secretSubkey, skBindingBuilder.build(secretSubkey.publicKey)) secretKeyRing = KeyRingUtils.keysPlusSecretKey(secretKeyRing, secretSubkey) return this } - override fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + override fun revoke( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { return revoke(protector, callbackFromRevocationAttributes(revocationAttributes)) } - override fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + override fun revoke( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { return revokeSubKey(secretKeyRing.secretKey.keyID, protector, callback) } - override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { - return revokeSubKey(subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) + override fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { + return revokeSubKey( + subkeyId, protector, callbackFromRevocationAttributes(revocationAttributes)) } - override fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + override fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { val revokeeSubKey = secretKeyRing.requirePublicKey(subkeyId) val subkeyRevocation = generateRevocation(protector, revokeeSubKey, callback) secretKeyRing = injectCertification(secretKeyRing, revokeeSubKey, subkeyRevocation) return this } - override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): SecretKeyRingEditorInterface { + override fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): SecretKeyRingEditorInterface { if (revocationAttributes != null) { - require(revocationAttributes.reason == RevocationAttributes.Reason.NO_REASON || - revocationAttributes.reason == RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { - "Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID" - } + require( + revocationAttributes.reason == RevocationAttributes.Reason.NO_REASON || + revocationAttributes.reason == + RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID) { + "Revocation reason must either be NO_REASON or USER_ID_NO_LONGER_VALID" + } } - return revokeUserId(userId, protector, object : RevocationSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { - if (revocationAttributes != null) { - hashedSubpackets.setRevocationReason(false, revocationAttributes) + return revokeUserId( + userId, + protector, + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + if (revocationAttributes != null) { + hashedSubpackets.setRevocationReason(false, revocationAttributes) + } } - } - }) + }) } - override fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { + override fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { return revokeUserIds(protector, callback, SelectUserId.exactMatch(sanitizeUserId(userId))) } - override fun revokeUserIds(protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?, - predicate: (String) -> Boolean): SecretKeyRingEditorInterface { - return revokeUserIds(protector, object : RevocationSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { - if (revocationAttributes != null) hashedSubpackets.setRevocationReason(revocationAttributes) - } - }, predicate) + override fun revokeUserIds( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + return revokeUserIds( + protector, + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + if (revocationAttributes != null) + hashedSubpackets.setRevocationReason(revocationAttributes) + } + }, + predicate) } - override fun revokeUserIds(protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?, - predicate: (String) -> Boolean): SecretKeyRingEditorInterface { - selectUserIds(predicate).also { - if (it.isEmpty()) throw NoSuchElementException("No matching user-ids found on the key.") - }.forEach { userId -> doRevokeUserId(userId, protector, callback) } + override fun revokeUserIds( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface { + selectUserIds(predicate) + .also { + if (it.isEmpty()) + throw NoSuchElementException("No matching user-ids found on the key.") + } + .forEach { userId -> doRevokeUserId(userId, protector, callback) } return this } - override fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface { + override fun setExpirationDate( + expiration: Date?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface { require(secretKeyRing.secretKey.isMasterKey) { "OpenPGP key does not appear to contain a primary secret key." } @@ -286,14 +424,19 @@ class SecretKeyRingEditor( val prevDirectKeySig = getPreviousDirectKeySignature() // reissue direct key sig if (prevDirectKeySig != null) { - secretKeyRing = injectCertification(secretKeyRing, secretKeyRing.publicKey, + secretKeyRing = + injectCertification( + secretKeyRing, + secretKeyRing.publicKey, reissueDirectKeySignature(expiration, protector, prevDirectKeySig)) } - val primaryUserId = inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() + val primaryUserId = + inspectKeyRing(secretKeyRing, referenceTime).getPossiblyExpiredPrimaryUserId() if (primaryUserId != null) { val prevUserIdSig = getPreviousUserIdSignatures(primaryUserId) - val userIdSig = reissuePrimaryUserIdSig(expiration, protector, primaryUserId, prevUserIdSig!!) + val userIdSig = + reissuePrimaryUserIdSig(expiration, protector, primaryUserId, prevUserIdSig!!) secretKeyRing = injectCertification(secretKeyRing, primaryUserId, userIdSig) } @@ -303,9 +446,15 @@ class SecretKeyRingEditor( continue } - val prevUserIdSig = info.getLatestUserIdCertification(userId) ?: throw AssertionError("A valid user-id shall never have no user-id signature.") + val prevUserIdSig = + info.getLatestUserIdCertification(userId) + ?: throw AssertionError( + "A valid user-id shall never have no user-id signature.") if (prevUserIdSig.hashedSubPackets.isPrimaryUserID) { - secretKeyRing = injectCertification(secretKeyRing, primaryUserId!!, + secretKeyRing = + injectCertification( + secretKeyRing, + primaryUserId!!, reissueNonPrimaryUserId(protector, userId, prevUserIdSig)) } } @@ -313,7 +462,10 @@ class SecretKeyRingEditor( return this } - override fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPPublicKeyRing { + override fun createMinimalRevocationCertificate( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPPublicKeyRing { // Check reason if (revocationAttributes != null) { require(RevocationAttributes.Reason.isKeyRevocation(revocationAttributes.reason)) { @@ -328,30 +480,67 @@ class SecretKeyRingEditor( return PGPPublicKeyRing(listOf(primaryKey)) } - override fun createRevocation(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { - return generateRevocation(protector, secretKeyRing.publicKey, callbackFromRevocationAttributes(revocationAttributes)) + override fun createRevocation( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.publicKey, + callbackFromRevocationAttributes(revocationAttributes)) } - override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { - return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callbackFromRevocationAttributes(revocationAttributes)) + override fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.requirePublicKey(subkeyId), + callbackFromRevocationAttributes(revocationAttributes)) } - override fun createRevocation(subkeyId: Long, protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + override fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature { return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyId), callback) } - override fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes?): PGPSignature { - return generateRevocation(protector, secretKeyRing.requirePublicKey(subkeyFingerprint), callbackFromRevocationAttributes(revocationAttributes)) + override fun createRevocation( + subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature { + return generateRevocation( + protector, + secretKeyRing.requirePublicKey(subkeyFingerprint), + callbackFromRevocationAttributes(revocationAttributes)) } - override fun changePassphraseFromOldPassphrase(oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { - return WithKeyRingEncryptionSettingsImpl(this, null, - PasswordBasedSecretKeyRingProtector(oldProtectionSettings, SolitaryPassphraseProvider(oldPassphrase))) + override fun changePassphraseFromOldPassphrase( + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl( + this, + null, + PasswordBasedSecretKeyRingProtector( + oldProtectionSettings, SolitaryPassphraseProvider(oldPassphrase))) } - override fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase, oldProtectionSettings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { - return WithKeyRingEncryptionSettingsImpl(this, keyId, - CachingSecretKeyRingProtector(mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) + override fun changeSubKeyPassphraseFromOldPassphrase( + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + return WithKeyRingEncryptionSettingsImpl( + this, + keyId, + CachingSecretKeyRingProtector( + mapOf(keyId to oldPassphrase), oldProtectionSettings, null)) } override fun done(): PGPSecretKeyRing { @@ -359,45 +548,52 @@ class SecretKeyRingEditor( } private fun sanitizeUserId(userId: CharSequence): CharSequence = - // TODO: Further research how to sanitize user IDs. - // e.g. what about newlines? - userId.toString().trim() + // TODO: Further research how to sanitize user IDs. + // e.g. what about newlines? + userId.toString().trim() private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = - object : RevocationSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { - if (attributes != null) { - hashedSubpackets.setRevocationReason(attributes) - } + object : RevocationSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: RevocationSignatureSubpackets) { + if (attributes != null) { + hashedSubpackets.setRevocationReason(attributes) } } + } - private fun generateRevocation(protector: SecretKeyRingProtector, - revokeeSubkey: PGPPublicKey, - callback: RevocationSignatureSubpackets.Callback?): PGPSignature { + private fun generateRevocation( + protector: SecretKeyRingProtector, + revokeeSubkey: PGPPublicKey, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature { val primaryKey = secretKeyRing.secretKey val signatureType = - if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION - else SignatureType.SUBKEY_REVOCATION + if (revokeeSubkey.isMasterKey) SignatureType.KEY_REVOCATION + else SignatureType.SUBKEY_REVOCATION return RevocationSignatureBuilder(signatureType, primaryKey, protector) - .apply { applyCallback(callback) } - .build(revokeeSubkey) + .apply { applyCallback(callback) } + .build(revokeeSubkey) } - private fun doRevokeUserId(userId: CharSequence, - protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface { - RevocationSignatureBuilder(SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector).apply { - hashedSubpackets.setSignatureCreationTime(referenceTime) - applyCallback(callback) - }.let { - secretKeyRing = injectCertification(secretKeyRing, userId, it.build(userId.toString())) - } + private fun doRevokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface { + RevocationSignatureBuilder( + SignatureType.CERTIFICATION_REVOCATION, secretKeyRing.secretKey, protector) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback(callback) + } + .let { + secretKeyRing = + injectCertification(secretKeyRing, userId, it.build(userId.toString())) + } return this } - private fun getPreviousDirectKeySignature(): PGPSignature? { val info = inspectKeyRing(secretKeyRing, referenceTime) return info.latestDirectKeySelfSignature @@ -410,88 +606,109 @@ class SecretKeyRingEditor( @Throws(PGPException::class) private fun reissueNonPrimaryUserId( - secretKeyRingProtector: SecretKeyRingProtector, - userId: String, - prevUserIdSig: PGPSignature): PGPSignature { - val builder = SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) + secretKeyRingProtector: SecretKeyRingProtector, + userId: String, + prevUserIdSig: PGPSignature + ): PGPSignature { + val builder = + SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) builder.hashedSubpackets.setSignatureCreationTime(referenceTime) - builder.applyCallback(object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { - // unmark as primary - hashedSubpackets.setPrimaryUserId(null) - } - }) + builder.applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + // unmark as primary + hashedSubpackets.setPrimaryUserId(null) + } + }) return builder.build(secretKeyRing.publicKey, userId) } @Throws(PGPException::class) private fun reissuePrimaryUserIdSig( - expiration: Date?, - @Nonnull secretKeyRingProtector: SecretKeyRingProtector, - @Nonnull primaryUserId: String, - @Nonnull prevUserIdSig: PGPSignature): PGPSignature { + expiration: Date?, + @Nonnull secretKeyRingProtector: SecretKeyRingProtector, + @Nonnull primaryUserId: String, + @Nonnull prevUserIdSig: PGPSignature + ): PGPSignature { return SelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevUserIdSig) - .apply { - hashedSubpackets.setSignatureCreationTime(referenceTime) - applyCallback(object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(true, secretKeyRing.publicKey.creationTime, expiration) + hashedSubpackets.setKeyExpirationTime( + true, secretKeyRing.publicKey.creationTime, expiration) } else { hashedSubpackets.setKeyExpirationTime(KeyExpirationTime(true, 0)) } hashedSubpackets.setPrimaryUserId() } }) - }.build(secretKeyRing.publicKey, primaryUserId) + } + .build(secretKeyRing.publicKey, primaryUserId) } @Throws(PGPException::class) private fun reissueDirectKeySignature( - expiration: Date?, - secretKeyRingProtector: SecretKeyRingProtector, - prevDirectKeySig: PGPSignature): PGPSignature { - return DirectKeySelfSignatureBuilder(secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) - .apply { - hashedSubpackets.setSignatureCreationTime(referenceTime) - applyCallback(object : SelfSignatureSubpackets.Callback { - override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + expiration: Date?, + secretKeyRingProtector: SecretKeyRingProtector, + prevDirectKeySig: PGPSignature + ): PGPSignature { + return DirectKeySelfSignatureBuilder( + secretKeyRing.secretKey, secretKeyRingProtector, prevDirectKeySig) + .apply { + hashedSubpackets.setSignatureCreationTime(referenceTime) + applyCallback( + object : SelfSignatureSubpackets.Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: SelfSignatureSubpackets + ) { if (expiration != null) { - hashedSubpackets.setKeyExpirationTime(secretKeyRing.publicKey.creationTime, expiration) + hashedSubpackets.setKeyExpirationTime( + secretKeyRing.publicKey.creationTime, expiration) } else { hashedSubpackets.setKeyExpirationTime(null) } } }) - }.build(secretKeyRing.publicKey) + } + .build(secretKeyRing.publicKey) } private fun selectUserIds(predicate: Predicate): List = - inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } + inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } private class WithKeyRingEncryptionSettingsImpl( - private val editor: SecretKeyRingEditor, - private val keyId: Long?, - private val oldProtector: SecretKeyRingProtector) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector + ) : SecretKeyRingEditorInterface.WithKeyRingEncryptionSettings { override fun withSecureDefaultSettings(): SecretKeyRingEditorInterface.WithPassphrase { return withCustomSettings(KeyRingProtectionSettings.secureDefaultSettings()) } - override fun withCustomSettings(settings: KeyRingProtectionSettings): SecretKeyRingEditorInterface.WithPassphrase { + override fun withCustomSettings( + settings: KeyRingProtectionSettings + ): SecretKeyRingEditorInterface.WithPassphrase { return WithPassphraseImpl(editor, keyId, oldProtector, settings) } } private class WithPassphraseImpl( - private val editor: SecretKeyRingEditor, - private val keyId: Long?, - private val oldProtector: SecretKeyRingProtector, - private val newProtectionSettings: KeyRingProtectionSettings + private val editor: SecretKeyRingEditor, + private val keyId: Long?, + private val oldProtector: SecretKeyRingProtector, + private val newProtectionSettings: KeyRingProtectionSettings ) : SecretKeyRingEditorInterface.WithPassphrase { override fun toNewPassphrase(passphrase: Passphrase): SecretKeyRingEditorInterface { - val protector = PasswordBasedSecretKeyRingProtector(newProtectionSettings, SolitaryPassphraseProvider(passphrase)) + val protector = + PasswordBasedSecretKeyRingProtector( + newProtectionSettings, SolitaryPassphraseProvider(passphrase)) val secretKeys = changePassphrase(keyId, editor.secretKeyRing, oldProtector, protector) editor.secretKeyRing = secretKeys return editor @@ -504,5 +721,4 @@ class SecretKeyRingEditor( return editor } } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt index 8a26161b..b8fb993c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -4,6 +4,10 @@ package org.pgpainless.key.modification.secretkeyring +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import java.util.* import org.bouncycastle.openpgp.* import org.pgpainless.algorithm.KeyFlag import org.pgpainless.key.OpenPgpFingerprint @@ -15,18 +19,12 @@ import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets import org.pgpainless.signature.subpackets.SelfSignatureSubpackets import org.pgpainless.util.Passphrase import org.pgpainless.util.selection.userid.SelectUserId -import java.io.IOException -import java.security.InvalidAlgorithmParameterException -import java.security.NoSuchAlgorithmException -import java.util.* -import java.util.function.Predicate interface SecretKeyRingEditorInterface { /** - * Editors reference time. - * This time is used as creation date for new signatures, or as reference when evaluating expiration of - * existing signatures. + * Editors reference time. This time is used as creation date for new signatures, or as + * reference when evaluating expiration of existing signatures. */ val referenceTime: Date @@ -36,11 +34,11 @@ interface SecretKeyRingEditorInterface { * @param userId user-id * @param protector protector to unlock the secret key * @return the builder - * * @throws PGPException in case we cannot generate a signature for the user-id */ @Throws(PGPException::class) - fun addUserId(userId: CharSequence, protector: SecretKeyRingProtector) = addUserId(userId, null, protector) + fun addUserId(userId: CharSequence, protector: SecretKeyRingProtector) = + addUserId(userId, null, protector) /** * Add a user-id to the key ring. @@ -49,105 +47,126 @@ interface SecretKeyRingEditorInterface { * @param callback callback to modify the self-signature subpackets * @param protector protector to unlock the secret key * @return the builder - * * @throws PGPException in case we cannot generate a signature for the user-id */ @Throws(PGPException::class) - fun addUserId(userId: CharSequence, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun addUserId( + userId: CharSequence, + callback: SelfSignatureSubpackets.Callback? = null, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** - * Add a user-id to the key ring and mark it as primary. - * If the user-id is already present, a new certification signature will be created. + * Add a user-id to the key ring and mark it as primary. If the user-id is already present, a + * new certification signature will be created. * * @param userId user id * @param protector protector to unlock the secret key * @return the builder - * * @throws PGPException in case we cannot generate a signature for the user-id */ @Throws(PGPException::class) - fun addPrimaryUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun addPrimaryUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** - * Convenience method to revoke selected user-ids using soft revocation signatures. - * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the user-id - * can be re-certified at a later point. + * Convenience method to revoke selected user-ids using soft revocation signatures. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the + * user-id can be re-certified at a later point. * * @param selector selector to select user-ids * @param protector protector to unlock the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ - @Deprecated("Use of SelectUserId class is deprecated.", - ReplaceWith("removeUserId(protector, predicate)")) + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("removeUserId(protector, predicate)")) @Throws(PGPException::class) fun removeUserId(selector: SelectUserId, protector: SecretKeyRingProtector) = - removeUserId(protector, selector) + removeUserId(protector, selector) /** - * Convenience method to revoke selected user-ids using soft revocation signatures. - * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the user-id - * can be re-certified at a later point. + * Convenience method to revoke selected user-ids using soft revocation signatures. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID], so that the + * user-id can be re-certified at a later point. * * @param protector protector to unlock the primary key * @param predicate predicate to select user-ids for revocation * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun removeUserId(protector: SecretKeyRingProtector, predicate: (String) -> Boolean): SecretKeyRingEditorInterface + fun removeUserId( + protector: SecretKeyRingProtector, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface /** - * Convenience method to revoke a single user-id using a soft revocation signature. - * The revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID] so that the user-id + * Convenience method to revoke a single user-id using a soft revocation signature. The + * revocation will use [RevocationAttributes.Reason.USER_ID_NO_LONGER_VALID] so that the user-id * can be re-certified at a later point. * * @param userId user-id to revoke * @param protector protector to unlock the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun removeUserId(userId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun removeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** - * Replace a user-id on the key with a new one. - * The old user-id gets soft revoked and the new user-id gets bound with the same signature subpackets as the - * old one, with one exception: - * If the old user-id was implicitly primary (did not carry a [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, - * but effectively was primary), then the new user-id will be explicitly marked as primary. + * Replace a user-id on the key with a new one. The old user-id gets soft revoked and the new + * user-id gets bound with the same signature subpackets as the old one, with one exception: If + * the old user-id was implicitly primary (did not carry a + * [org.bouncycastle.bcpg.sig.PrimaryUserID] packet, but effectively was primary), then the new + * user-id will be explicitly marked as primary. * * @param oldUserId old user-id * @param newUserId new user-id * @param protector protector to unlock the secret key * @return the builder * @throws PGPException in case we cannot generate a revocation and certification signature - * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if the oldUserId - * was already invalid + * @throws java.util.NoSuchElementException if the old user-id was not found on the key; or if + * the oldUserId was already invalid */ @Throws(PGPException::class) - fun replaceUserId(oldUserId: CharSequence, newUserId: CharSequence, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun replaceUserId( + oldUserId: CharSequence, + newUserId: CharSequence, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** - * Add a subkey to the key ring. - * The subkey will be generated from the provided [KeySpec]. + * Add a subkey to the key ring. The subkey will be generated from the provided [KeySpec]. * * @param keySpec key specification * @param subkeyPassphrase passphrase to encrypt the sub key * @param callback callback to modify the subpackets of the subkey binding signature * @param protector protector to unlock the secret key of the key ring * @return the builder - * - * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters for the key + * @throws InvalidAlgorithmParameterException in case the user wants to use invalid parameters + * for the key * @throws NoSuchAlgorithmException in case of missing algorithm support in the crypto backend * @throws PGPException in case we cannot generate a binding signature for the subkey * @throws IOException in case of an IO error */ - @Throws(PGPException::class, IOException::class, InvalidAlgorithmParameterException::class, NoSuchAlgorithmException::class) - fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, callback: SelfSignatureSubpackets.Callback? = null, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + @Throws( + PGPException::class, + IOException::class, + InvalidAlgorithmParameterException::class, + NoSuchAlgorithmException::class) + fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + callback: SelfSignatureSubpackets.Callback? = null, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** * Add a subkey to the key ring. @@ -159,140 +178,140 @@ interface SecretKeyRingEditorInterface { * @param keyFlag first mandatory key flag for the subkey * @param keyFlags optional additional key flags * @return builder - * * @throws PGPException in case we cannot generate a binding signature for the subkey * @throws IOException in case of an IO error */ @Throws(PGPException::class, IOException::class) - fun addSubKey(subkey: PGPKeyPair, - callback: SelfSignatureSubpackets.Callback?, - subkeyProtector: SecretKeyRingProtector, - primaryKeyProtector: SecretKeyRingProtector, - keyFlag: KeyFlag, - vararg keyFlags: KeyFlag): SecretKeyRingEditorInterface + fun addSubKey( + subkey: PGPKeyPair, + callback: SelfSignatureSubpackets.Callback?, + subkeyProtector: SecretKeyRingProtector, + primaryKeyProtector: SecretKeyRingProtector, + keyFlag: KeyFlag, + vararg keyFlags: KeyFlag + ): SecretKeyRingEditorInterface /** * Revoke the key ring using a hard revocation. * * @param protector protector of the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature */ @Throws(PGPException::class) fun revoke(protector: SecretKeyRingProtector) = revoke(protector, null as RevocationAttributes?) /** - * Revoke the key ring using the provided revocation attributes. - * The attributes define, whether the revocation was a hard revocation or not. + * Revoke the key ring using the provided revocation attributes. The attributes define, whether + * the revocation was a hard revocation or not. * * @param protector protector of the primary key * @param revocationAttributes reason for the revocation * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature */ @Throws(PGPException::class) - fun revoke(protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + fun revoke( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface /** - * Revoke the key ring. - * You can use the [RevocationSignatureSubpackets.Callback] to modify the revocation signatures - * subpackets, e.g. in order to define whether this is a hard or soft revocation. + * Revoke the key ring. You can use the [RevocationSignatureSubpackets.Callback] to modify the + * revocation signatures subpackets, e.g. in order to define whether this is a hard or soft + * revocation. * * @param protector protector to unlock the primary secret key * @param callback callback to modify the revocations subpackets * @return builder - * * @throws PGPException in case we cannot generate a revocation signature */ @Throws(PGPException::class) - fun revoke(protector: SecretKeyRingProtector, callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + fun revoke( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * Revoke the subkey binding signature of a subkey. The subkey with the provided fingerprint + * will be revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * @param fingerprint fingerprint of the subkey to be revoked * @param protector protector to unlock the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) - fun revokeSubKey(fingerprint: OpenPgpFingerprint, - protector: SecretKeyRingProtector) = revokeSubKey(fingerprint, protector, null) + fun revokeSubKey(fingerprint: OpenPgpFingerprint, protector: SecretKeyRingProtector) = + revokeSubKey(fingerprint, protector, null) /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided fingerprint will be revoked. - * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * Revoke the subkey binding signature of a subkey. The subkey with the provided fingerprint + * will be revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * @param fingerprint fingerprint of the subkey to be revoked * @param protector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) - fun revokeSubKey(fingerprint: OpenPgpFingerprint, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface = - revokeSubKey(fingerprint.keyId, protector, revocationAttributes) + fun revokeSubKey( + fingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface = + revokeSubKey(fingerprint.keyId, protector, revocationAttributes) /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * @param subkeyId id of the subkey * @param protector protector to unlock the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) fun revokeSubKey(subkeyId: Long, protector: SecretKeyRingProtector) = - revokeSubKey(subkeyId, protector, null as RevocationAttributes?) + revokeSubKey(subkeyId, protector, null as RevocationAttributes?) /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * @param subkeyId id of the subkey * @param protector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) - fun revokeSubKey(subkeyId: Long, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface /** - * Revoke the subkey binding signature of a subkey. - * The subkey with the provided key-id will be revoked. - * If no suitable subkey is found, a [NoSuchElementException] will be thrown. + * Revoke the subkey binding signature of a subkey. The subkey with the provided key-id will be + * revoked. If no suitable subkey is found, a [NoSuchElementException] will be thrown. * * The provided subpackets callback is used to modify the revocation signatures subpackets. * * @param subkeyId id of the subkey * @param protector protector to unlock the secret key ring * @param callback callback which can be used to modify the subpackets of the revocation - * signature + * signature * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the subkey */ @Throws(PGPException::class) - fun revokeSubKey(subkeyId: Long, - protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + fun revokeSubKey( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface /** * Hard-revoke the given userID. @@ -300,11 +319,11 @@ interface SecretKeyRingEditorInterface { * @param userId userId to revoke * @param protector protector to unlock the primary key * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector) = revokeUserId(userId, protector, null as RevocationAttributes?) + fun revokeUserId(userId: CharSequence, protector: SecretKeyRingProtector) = + revokeUserId(userId, protector, null as RevocationAttributes?) /** * Revoke the given userID using the provided revocation attributes. @@ -313,83 +332,85 @@ interface SecretKeyRingEditorInterface { * @param protector protector to unlock the primary key * @param revocationAttributes reason for the revocation * @return the builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun revokeUserId(userId: CharSequence, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes? = null): SecretKeyRingEditorInterface + fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? = null + ): SecretKeyRingEditorInterface /** - * Revoke the provided user-id. - * Note: If you don't provide a [RevocationSignatureSubpackets.Callback] which - * sets a revocation reason ([RevocationAttributes]), the revocation will be considered hard. - * So if you intend to re-certify the user-id at a later point to make it valid again, - * make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback. + * Revoke the provided user-id. Note: If you don't provide a + * [RevocationSignatureSubpackets.Callback] which sets a revocation reason + * ([RevocationAttributes]), the revocation will be considered hard. So if you intend to + * re-certify the user-id at a later point to make it valid again, make sure to set a soft + * revocation reason in the signatures hashed area using the subpacket callback. * * @param userId userid to be revoked * @param protector protector to unlock the primary secret key * @param callback callback to modify the revocations subpackets * @return builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun revokeUserId(userId: CharSequence, - protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?): SecretKeyRingEditorInterface + fun revokeUserId( + userId: CharSequence, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): SecretKeyRingEditorInterface /** - * Revoke all user-ids that match the provided [SelectUserId] filter. - * The provided [RevocationAttributes] will be set as reason for revocation in each - * revocation signature. + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationAttributes] will be set as reason for revocation in each revocation signature. * - * Note: If you intend to re-certify these user-ids at a later point, make sure to choose - * a soft revocation reason. See [RevocationAttributes.Reason] for more information. + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose a soft + * revocation reason. See [RevocationAttributes.Reason] for more information. * * @param selector user-id selector * @param protector protector to unlock the primary secret key * @param revocationAttributes revocation attributes * @return builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - @Deprecated("Use of SelectUserId class is deprecated.", - ReplaceWith("revokeUserIds(protector, revocationAttributes, predicate)")) - fun revokeUserIds(selector: SelectUserId, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?) = - revokeUserIds(protector, revocationAttributes, selector) + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, revocationAttributes, predicate)")) + fun revokeUserIds( + selector: SelectUserId, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ) = revokeUserIds(protector, revocationAttributes, selector) /** - * Revoke all user-ids that match the provided [SelectUserId] filter. - * The provided [RevocationAttributes] will be set as reason for revocation in each - * revocation signature. + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationAttributes] will be set as reason for revocation in each revocation signature. * - * Note: If you intend to re-certify these user-ids at a later point, make sure to choose - * a soft revocation reason. See [RevocationAttributes.Reason] for more information. + * Note: If you intend to re-certify these user-ids at a later point, make sure to choose a soft + * revocation reason. See [RevocationAttributes.Reason] for more information. * * @param protector protector to unlock the primary secret key * @param revocationAttributes revocation attributes * @param predicate to select user-ids for revocation * @return builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun revokeUserIds(protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?, - predicate: (String) -> Boolean): SecretKeyRingEditorInterface + fun revokeUserIds( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface /** - * Revoke all user-ids that match the provided [SelectUserId] filter. - * The provided [RevocationSignatureSubpackets.Callback] will be used to modify the - * revocation signatures subpackets. + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationSignatureSubpackets.Callback] will be used to modify the revocation signatures + * subpackets. * - * Note: If you intend to re-certify these user-ids at a later point, make sure to set - * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. + * Note: If you intend to re-certify these user-ids at a later point, make sure to set a soft + * revocation reason in the revocation signatures hashed subpacket area using the callback. * * See [RevocationAttributes.Reason] for more information. * @@ -397,24 +418,25 @@ interface SecretKeyRingEditorInterface { * @param protector protector to unlock the primary secret key * @param callback callback to modify the revocations subpackets * @return builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - @Deprecated("Use of SelectUserId class is deprecated.", - ReplaceWith("revokeUserIds(protector, callback, predicate)")) - fun revokeUserIds(selector: SelectUserId, - protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?) = - revokeUserIds(protector, callback, selector) + @Deprecated( + "Use of SelectUserId class is deprecated.", + ReplaceWith("revokeUserIds(protector, callback, predicate)")) + fun revokeUserIds( + selector: SelectUserId, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ) = revokeUserIds(protector, callback, selector) /** - * Revoke all user-ids that match the provided [SelectUserId] filter. - * The provided [RevocationSignatureSubpackets.Callback] will be used to modify the - * revocation signatures subpackets. + * Revoke all user-ids that match the provided [SelectUserId] filter. The provided + * [RevocationSignatureSubpackets.Callback] will be used to modify the revocation signatures + * subpackets. * - * Note: If you intend to re-certify these user-ids at a later point, make sure to set - * a soft revocation reason in the revocation signatures hashed subpacket area using the callback. + * Note: If you intend to re-certify these user-ids at a later point, make sure to set a soft + * revocation reason in the revocation signatures hashed subpacket area using the callback. * * See [RevocationAttributes.Reason] for more information. * @@ -422,56 +444,61 @@ interface SecretKeyRingEditorInterface { * @param callback callback to modify the revocations subpackets * @param predicate to select user-ids for revocation * @return builder - * * @throws PGPException in case we cannot generate a revocation signature for the user-id */ @Throws(PGPException::class) - fun revokeUserIds(protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?, - predicate: (String) -> Boolean): SecretKeyRingEditorInterface + fun revokeUserIds( + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback?, + predicate: (String) -> Boolean + ): SecretKeyRingEditorInterface /** - * Set the expiration date for the primary key of the key ring. - * If the key is supposed to never expire, then an expiration date of null is expected. + * Set the expiration date for the primary key of the key ring. If the key is supposed to never + * expire, then an expiration date of null is expected. * * @param expiration new expiration date or null * @param protector to unlock the secret key * @return the builder - * - * @throws PGPException in case we cannot generate a new self-signature with the changed expiration date + * @throws PGPException in case we cannot generate a new self-signature with the changed + * expiration date */ @Throws(PGPException::class) - fun setExpirationDate(expiration: Date?, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface + fun setExpirationDate( + expiration: Date?, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface /** * Create a minimal, self-authorizing revocation certificate, containing only the primary key - * and a revocation signature. - * This type of revocation certificates was introduced in OpenPGP v6. - * This method has no side effects on the original key and will leave it intact. + * and a revocation signature. This type of revocation certificates was introduced in OpenPGP + * v6. This method has no side effects on the original key and will leave it intact. * * @param protector protector to unlock the primary key. * @param revocationAttributes reason for the revocation (key revocation) * @return minimal revocation certificate - * * @throws PGPException in case we cannot generate a revocation signature */ @Throws(PGPException::class) - fun createMinimalRevocationCertificate(protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?): PGPPublicKeyRing + fun createMinimalRevocationCertificate( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPPublicKeyRing /** - * Create a detached revocation certificate, which can be used to revoke the whole key. - * The original key will not be modified by this method. + * Create a detached revocation certificate, which can be used to revoke the whole key. The + * original key will not be modified by this method. * * @param protector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate - * * @throws PGPException in case we cannot generate a revocation certificate */ @Throws(PGPException::class) - fun createRevocation(protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?): PGPSignature + fun createRevocation( + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. @@ -481,13 +508,14 @@ interface SecretKeyRingEditorInterface { * @param protector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate - * * @throws PGPException in case we cannot generate a revocation certificate */ @Throws(PGPException::class) - fun createRevocation(subkeyId: Long, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?): PGPSignature + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. @@ -497,13 +525,14 @@ interface SecretKeyRingEditorInterface { * @param protector protector to unlock the primary key. * @param callback callback to modify the subpackets of the revocation certificate. * @return revocation certificate - * * @throws PGPException in case we cannot generate a revocation certificate */ @Throws(PGPException::class) - fun createRevocation(subkeyId: Long, - protector: SecretKeyRingProtector, - callback: RevocationSignatureSubpackets.Callback?): PGPSignature + fun createRevocation( + subkeyId: Long, + protector: SecretKeyRingProtector, + callback: RevocationSignatureSubpackets.Callback? + ): PGPSignature /** * Create a detached revocation certificate, which can be used to revoke the specified subkey. @@ -513,13 +542,14 @@ interface SecretKeyRingEditorInterface { * @param protector protector to unlock the primary key. * @param revocationAttributes reason for the revocation * @return revocation certificate - * * @throws PGPException in case we cannot generate a revocation certificate */ @Throws(PGPException::class) - fun createRevocation(subkeyFingerprint: OpenPgpFingerprint, - protector: SecretKeyRingProtector, - revocationAttributes: RevocationAttributes?): PGPSignature + fun createRevocation( + subkeyFingerprint: OpenPgpFingerprint, + protector: SecretKeyRingProtector, + revocationAttributes: RevocationAttributes? + ): PGPSignature /** * Change the passphrase of the whole key ring. @@ -527,8 +557,9 @@ interface SecretKeyRingEditorInterface { * @param oldPassphrase old passphrase (empty, if the key was unprotected) * @return next builder step */ - fun changePassphraseFromOldPassphrase( - oldPassphrase: Passphrase) = changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + fun changePassphraseFromOldPassphrase(oldPassphrase: Passphrase) = + changePassphraseFromOldPassphrase( + oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) /** * Change the passphrase of the whole key ring. @@ -538,27 +569,30 @@ interface SecretKeyRingEditorInterface { * @return next builder step */ fun changePassphraseFromOldPassphrase( - oldPassphrase: Passphrase, - oldProtectionSettings: KeyRingProtectionSettings = KeyRingProtectionSettings.secureDefaultSettings()): WithKeyRingEncryptionSettings + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings = + KeyRingProtectionSettings.secureDefaultSettings() + ): WithKeyRingEncryptionSettings /** * Change the passphrase of a single subkey in the key ring. * - * Note: While it is a valid use-case to have different passphrases per subKey, - * this is one of the reasons why OpenPGP sucks in practice. + * Note: While it is a valid use-case to have different passphrases per subKey, this is one of + * the reasons why OpenPGP sucks in practice. * * @param keyId id of the subkey * @param oldPassphrase old passphrase (empty if the key was unprotected) * @return next builder step */ fun changeSubKeyPassphraseFromOldPassphrase(keyId: Long, oldPassphrase: Passphrase) = - changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) + changeSubKeyPassphraseFromOldPassphrase( + keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings()) /** * Change the passphrase of a single subkey in the key ring. * - * Note: While it is a valid use-case to have different passphrases per subKey, - * this is one of the reasons why OpenPGP sucks in practice. + * Note: While it is a valid use-case to have different passphrases per subKey, this is one of + * the reasons why OpenPGP sucks in practice. * * @param keyId id of the subkey * @param oldPassphrase old passphrase (empty if the key was unprotected) @@ -566,15 +600,16 @@ interface SecretKeyRingEditorInterface { * @return next builder step */ fun changeSubKeyPassphraseFromOldPassphrase( - keyId: Long, - oldPassphrase: Passphrase, - oldProtectionSettings: KeyRingProtectionSettings): WithKeyRingEncryptionSettings + keyId: Long, + oldPassphrase: Passphrase, + oldProtectionSettings: KeyRingProtectionSettings + ): WithKeyRingEncryptionSettings interface WithKeyRingEncryptionSettings { /** - * Set secure default settings for the symmetric passphrase encryption. - * Note that this obviously has no effect if you decide to set [WithPassphrase.toNoPassphrase]. + * Set secure default settings for the symmetric passphrase encryption. Note that this + * obviously has no effect if you decide to set [WithPassphrase.toNoPassphrase]. * * @return next builder step */ @@ -596,7 +631,6 @@ interface SecretKeyRingEditorInterface { * * @param passphrase passphrase * @return editor builder - * * @throws PGPException in case the passphrase cannot be changed */ @Throws(PGPException::class) @@ -606,17 +640,21 @@ interface SecretKeyRingEditorInterface { * Leave the key unprotected. * * @return editor builder - * * @throws PGPException in case the passphrase cannot be changed */ - @Throws(PGPException::class) - fun toNoPassphrase(): SecretKeyRingEditorInterface + @Throws(PGPException::class) fun toNoPassphrase(): SecretKeyRingEditorInterface } /** * Return the [PGPSecretKeyRing]. + * * @return the key */ fun done(): PGPSecretKeyRing - fun addSubKey(keySpec: KeySpec, subkeyPassphrase: Passphrase, protector: SecretKeyRingProtector): SecretKeyRingEditorInterface -} \ No newline at end of file + + fun addSubKey( + keySpec: KeySpec, + subkeyPassphrase: Passphrase, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt index 388294d5..6f6bde61 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/parsing/KeyRingReader.kt @@ -4,124 +4,118 @@ package org.pgpainless.key.parsing +import java.io.IOException +import java.io.InputStream +import java.nio.charset.Charset +import kotlin.jvm.Throws import org.bouncycastle.openpgp.* import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.collection.PGPKeyRingCollection import org.pgpainless.util.ArmorUtils -import java.io.IOException -import java.io.InputStream -import java.nio.charset.Charset -import kotlin.jvm.Throws class KeyRingReader { /** - * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given [InputStream]. + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given + * [InputStream]. * * @param inputStream inputStream containing the OpenPGP key or certificate * @return key ring * @throws IOException in case of an IO error */ @Throws(IOException::class) - fun keyRing(inputStream: InputStream): PGPKeyRing? = - readKeyRing(inputStream) + fun keyRing(inputStream: InputStream): PGPKeyRing? = readKeyRing(inputStream) /** - * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given byte array. + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given byte + * array. * * @param bytes byte array containing the OpenPGP key or certificate * @return key ring * @throws IOException in case of an IO error */ @Throws(IOException::class) - fun keyRing(bytes: ByteArray): PGPKeyRing? = - keyRing(bytes.inputStream()) + fun keyRing(bytes: ByteArray): PGPKeyRing? = keyRing(bytes.inputStream()) /** - * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given - * ASCII armored string. + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given ASCII + * armored string. * * @param asciiArmored ASCII armored OpenPGP key or certificate * @return key ring * @throws IOException in case of an IO error */ @Throws(IOException::class) - fun keyRing(asciiArmored: String): PGPKeyRing? = - keyRing(asciiArmored.toByteArray(UTF8)) + fun keyRing(asciiArmored: String): PGPKeyRing? = keyRing(asciiArmored.toByteArray(UTF8)) @Throws(IOException::class) - fun publicKeyRing(inputStream: InputStream): PGPPublicKeyRing? = - readPublicKeyRing(inputStream) + fun publicKeyRing(inputStream: InputStream): PGPPublicKeyRing? = readPublicKeyRing(inputStream) @Throws(IOException::class) - fun publicKeyRing(bytes: ByteArray): PGPPublicKeyRing? = - publicKeyRing(bytes.inputStream()) + fun publicKeyRing(bytes: ByteArray): PGPPublicKeyRing? = publicKeyRing(bytes.inputStream()) @Throws(IOException::class) fun publicKeyRing(asciiArmored: String): PGPPublicKeyRing? = - publicKeyRing(asciiArmored.toByteArray(UTF8)) + publicKeyRing(asciiArmored.toByteArray(UTF8)) @Throws(IOException::class) fun publicKeyRingCollection(inputStream: InputStream): PGPPublicKeyRingCollection = - readPublicKeyRingCollection(inputStream) + readPublicKeyRingCollection(inputStream) @Throws(IOException::class) fun publicKeyRingCollection(bytes: ByteArray): PGPPublicKeyRingCollection = - publicKeyRingCollection(bytes.inputStream()) + publicKeyRingCollection(bytes.inputStream()) @Throws(IOException::class) fun publicKeyRingCollection(asciiArmored: String): PGPPublicKeyRingCollection = - publicKeyRingCollection(asciiArmored.toByteArray(UTF8)) + publicKeyRingCollection(asciiArmored.toByteArray(UTF8)) @Throws(IOException::class) - fun secretKeyRing(inputStream: InputStream): PGPSecretKeyRing? = - readSecretKeyRing(inputStream) + fun secretKeyRing(inputStream: InputStream): PGPSecretKeyRing? = readSecretKeyRing(inputStream) @Throws(IOException::class) - fun secretKeyRing(bytes: ByteArray): PGPSecretKeyRing? = - secretKeyRing(bytes.inputStream()) + fun secretKeyRing(bytes: ByteArray): PGPSecretKeyRing? = secretKeyRing(bytes.inputStream()) @Throws(IOException::class) fun secretKeyRing(asciiArmored: String): PGPSecretKeyRing? = - secretKeyRing(asciiArmored.toByteArray(UTF8)) + secretKeyRing(asciiArmored.toByteArray(UTF8)) @Throws(IOException::class) fun secretKeyRingCollection(inputStream: InputStream): PGPSecretKeyRingCollection = - readSecretKeyRingCollection(inputStream) + readSecretKeyRingCollection(inputStream) @Throws(IOException::class) fun secretKeyRingCollection(bytes: ByteArray): PGPSecretKeyRingCollection = - secretKeyRingCollection(bytes.inputStream()) + secretKeyRingCollection(bytes.inputStream()) @Throws(IOException::class) fun secretKeyRingCollection(asciiArmored: String): PGPSecretKeyRingCollection = - secretKeyRingCollection(asciiArmored.toByteArray(UTF8)) + secretKeyRingCollection(asciiArmored.toByteArray(UTF8)) @Throws(IOException::class) fun keyRingCollection(inptStream: InputStream, isSilent: Boolean): PGPKeyRingCollection = - readKeyRingCollection(inptStream, isSilent) + readKeyRingCollection(inptStream, isSilent) @Throws(IOException::class) fun keyRingCollection(bytes: ByteArray, isSilent: Boolean): PGPKeyRingCollection = - keyRingCollection(bytes.inputStream(), isSilent) + keyRingCollection(bytes.inputStream(), isSilent) @Throws(IOException::class) fun keyRingCollection(asciiArmored: String, isSilent: Boolean): PGPKeyRingCollection = - keyRingCollection(asciiArmored.toByteArray(UTF8), isSilent) + keyRingCollection(asciiArmored.toByteArray(UTF8), isSilent) companion object { private const val MAX_ITERATIONS = 10000 - @JvmStatic - val UTF8: Charset = charset("UTF8") - + @JvmStatic val UTF8: Charset = charset("UTF8") /** - * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given [InputStream]. - * This method will attempt to read at most

maxIterations
objects from the stream before aborting. - * The first [PGPPublicKeyRing] or [PGPSecretKeyRing] will be returned. + * Read a [PGPKeyRing] (either [PGPSecretKeyRing] or [PGPPublicKeyRing]) from the given + * [InputStream]. This method will attempt to read at most
maxIterations
objects + * from the stream before aborting. The first [PGPPublicKeyRing] or [PGPSecretKeyRing] will + * be returned. * * @param inputStream inputStream containing the OpenPGP key or certificate * @param maxIterations maximum number of objects that are read before the method will abort @@ -131,10 +125,13 @@ class KeyRingReader { @JvmStatic @JvmOverloads @Throws(IOException::class) - fun readKeyRing(inputStream: InputStream, - maxIterations: Int = MAX_ITERATIONS): PGPKeyRing? { - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)) + fun readKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPKeyRing? { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) try { for ((i, next) in objectFactory.withIndex()) { @@ -152,30 +149,31 @@ class KeyRingReader { } continue } - } catch (e : PGPRuntimeOperationException) { + } catch (e: PGPRuntimeOperationException) { throw e.cause!! } return null } /** - * Read a public key ring from the provided [InputStream]. - * If more than maxIterations PGP packets are encountered before a [PGPPublicKeyRing] is read, - * an [IOException] is thrown. + * Read a public key ring from the provided [InputStream]. If more than maxIterations PGP + * packets are encountered before a [PGPPublicKeyRing] is read, an [IOException] is thrown. * * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring - * * @throws IOException in case of an IO error or exceeding of max iterations */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun readPublicKeyRing(inputStream: InputStream, - maxIterations: Int = MAX_ITERATIONS): PGPPublicKeyRing? { - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)) + fun readPublicKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPPublicKeyRing? { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) try { for ((i, next) in objectFactory.withIndex()) { @@ -190,31 +188,33 @@ class KeyRingReader { } continue } - } catch (e : PGPRuntimeOperationException) { + } catch (e: PGPRuntimeOperationException) { throw e.cause!! } return null } /** - * Read a public key ring collection from the provided [InputStream]. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an [IOException] is thrown. - * If the stream contain secret key packets, their public key parts are extracted and returned. + * Read a public key ring collection from the provided [InputStream]. If more than + * maxIterations PGP packets are encountered before the stream is exhausted, an + * [IOException] is thrown. If the stream contain secret key packets, their public key parts + * are extracted and returned. * * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring collection - * * @throws IOException in case of an IO error or exceeding of max iterations */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun readPublicKeyRingCollection(inputStream: InputStream, - maxIterations: Int = MAX_ITERATIONS): PGPPublicKeyRingCollection { - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)) + fun readPublicKeyRingCollection( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPPublicKeyRingCollection { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) val certificates = mutableListOf() try { for ((i, next) in objectFactory.withIndex()) { @@ -237,30 +237,31 @@ class KeyRingReader { continue } } - } catch (e : PGPRuntimeOperationException) { + } catch (e: PGPRuntimeOperationException) { throw e.cause!! } return PGPPublicKeyRingCollection(certificates) } /** - * Read a secret key ring from the provided [InputStream]. - * If more than maxIterations PGP packets are encountered before a [PGPSecretKeyRing] is read, - * an [IOException] is thrown. + * Read a secret key ring from the provided [InputStream]. If more than maxIterations PGP + * packets are encountered before a [PGPSecretKeyRing] is read, an [IOException] is thrown. * * @param inputStream input stream * @param maxIterations max iterations before abort * @return public key ring - * * @throws IOException in case of an IO error or exceeding of max iterations */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun readSecretKeyRing(inputStream: InputStream, - maxIterations: Int = MAX_ITERATIONS): PGPSecretKeyRing? { + fun readSecretKeyRing( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPSecretKeyRing? { val decoderStream = ArmorUtils.getDecoderStream(inputStream) - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream) + val objectFactory = + ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream) try { for ((i, next) in objectFactory.withIndex()) { @@ -275,30 +276,32 @@ class KeyRingReader { return next } } - } catch (e : PGPRuntimeOperationException) { + } catch (e: PGPRuntimeOperationException) { throw e.cause!! } return null } /** - * Read a secret key ring collection from the provided [InputStream]. - * If more than maxIterations PGP packets are encountered before the stream is exhausted, - * an [IOException] is thrown. + * Read a secret key ring collection from the provided [InputStream]. If more than + * maxIterations PGP packets are encountered before the stream is exhausted, an + * [IOException] is thrown. * * @param inputStream input stream * @param maxIterations max iterations before abort * @return secret key ring collection - * * @throws IOException in case of an IO error or exceeding of max iterations */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun readSecretKeyRingCollection(inputStream: InputStream, - maxIterations: Int = MAX_ITERATIONS): PGPSecretKeyRingCollection { - val objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( - ArmorUtils.getDecoderStream(inputStream)) + fun readSecretKeyRingCollection( + inputStream: InputStream, + maxIterations: Int = MAX_ITERATIONS + ): PGPSecretKeyRingCollection { + val objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory(ArmorUtils.getDecoderStream(inputStream)) val secretKeys = mutableListOf() try { @@ -318,7 +321,7 @@ class KeyRingReader { continue } } - } catch (e : PGPRuntimeOperationException) { + } catch (e: PGPRuntimeOperationException) { throw e.cause!! } return PGPSecretKeyRingCollection(secretKeys) @@ -326,8 +329,9 @@ class KeyRingReader { @JvmStatic @Throws(IOException::class) - fun readKeyRingCollection(inputStream: InputStream, isSilent: Boolean): PGPKeyRingCollection = - PGPKeyRingCollection(inputStream, isSilent) + fun readKeyRingCollection( + inputStream: InputStream, + isSilent: Boolean + ): PGPKeyRingCollection = PGPKeyRingCollection(inputStream, isSilent) } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt index 450ca7f5..c5db2086 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/BaseSecretKeyRingProtector.kt @@ -10,31 +10,35 @@ import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider /** - * Basic [SecretKeyRingProtector] implementation that respects the users [KeyRingProtectionSettings] when encrypting keys. + * Basic [SecretKeyRingProtector] implementation that respects the users [KeyRingProtectionSettings] + * when encrypting keys. */ open class BaseSecretKeyRingProtector( - private val passphraseProvider: SecretKeyPassphraseProvider, - private val protectionSettings: KeyRingProtectionSettings + private val passphraseProvider: SecretKeyPassphraseProvider, + private val protectionSettings: KeyRingProtectionSettings ) : SecretKeyRingProtector { - constructor(passphraseProvider: SecretKeyPassphraseProvider): - this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()) + constructor( + passphraseProvider: SecretKeyPassphraseProvider + ) : this(passphraseProvider, KeyRingProtectionSettings.secureDefaultSettings()) override fun hasPassphraseFor(keyId: Long): Boolean = passphraseProvider.hasPassphrase(keyId) override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = - passphraseProvider.getPassphraseFor(keyId)?.let { - if (it.isEmpty) null - else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(it) - } + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else ImplementationFactory.getInstance().getPBESecretKeyDecryptor(it) + } override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = - passphraseProvider.getPassphraseFor(keyId)?.let { - if (it.isEmpty) null - else ImplementationFactory.getInstance().getPBESecretKeyEncryptor( + passphraseProvider.getPassphraseFor(keyId)?.let { + if (it.isEmpty) null + else + ImplementationFactory.getInstance() + .getPBESecretKeyEncryptor( protectionSettings.encryptionAlgorithm, protectionSettings.hashAlgorithm, protectionSettings.s2kCount, it) - } -} \ No newline at end of file + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt index 32f69f70..5a3ff47f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -13,8 +13,8 @@ import org.pgpainless.util.Passphrase /** * Implementation of the [SecretKeyRingProtector] which holds a map of key ids and their passwords. - * In case the needed passphrase is not contained in the map, the `missingPassphraseCallback` will be consulted, - * and the passphrase is added to the map. + * In case the needed passphrase is not contained in the map, the `missingPassphraseCallback` will + * be consulted, and the passphrase is added to the map. * * If you need to unlock multiple [PGPKeyRing] instances, it is advised to use a separate * [CachingSecretKeyRingProtector] instance for each ring. @@ -25,29 +25,33 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras private val protector: SecretKeyRingProtector private val provider: SecretKeyPassphraseProvider? - constructor(): this(null) + constructor() : this(null) - constructor(missingPassphraseCallback: SecretKeyPassphraseProvider?): this( - mapOf(), - KeyRingProtectionSettings.secureDefaultSettings(), - missingPassphraseCallback) + constructor( + missingPassphraseCallback: SecretKeyPassphraseProvider? + ) : this( + mapOf(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback) - constructor(passphrases: Map, - protectionSettings: KeyRingProtectionSettings, - missingPassphraseCallback: SecretKeyPassphraseProvider?) { + constructor( + passphrases: Map, + protectionSettings: KeyRingProtectionSettings, + missingPassphraseCallback: SecretKeyPassphraseProvider? + ) { this.cache = passphrases.toMutableMap() this.protector = PasswordBasedSecretKeyRingProtector(protectionSettings, this) this.provider = missingPassphraseCallback } /** - * Add a passphrase to the cache. - * If the cache already contains a passphrase for the given key-id, a [IllegalArgumentException] is thrown. - * The reason for this is to prevent accidental override of passphrases when dealing with multiple key rings - * containing a key with the same key-id but different passphrases. + * Add a passphrase to the cache. If the cache already contains a passphrase for the given + * key-id, a [IllegalArgumentException] is thrown. The reason for this is to prevent accidental + * override of passphrases when dealing with multiple key rings containing a key with the same + * key-id but different passphrases. * - * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, you can use - * [replacePassphrase] to replace the passphrase. + * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, + * you can use [replacePassphrase] to replace the passphrase. * * @param keyId id of the key * @param passphrase passphrase @@ -55,7 +59,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras fun addPassphrase(keyId: Long, passphrase: Passphrase) = apply { require(!cache.containsKey(keyId)) { "The cache already holds a passphrase for ID ${keyId.openPgpKeyId()}.\n" + - "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." + "If you want to replace this passphrase, use replacePassphrase(Long, Passphrase) instead." } cache[keyId] = passphrase } @@ -66,22 +70,20 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param keyId keyId * @param passphrase passphrase */ - fun replacePassphrase(keyId: Long, passphrase: Passphrase) = apply { - cache[keyId] = passphrase - } + fun replacePassphrase(keyId: Long, passphrase: Passphrase) = apply { cache[keyId] = passphrase } /** - * Remember the given passphrase for all keys in the given key ring. - * If for the key-id of any key on the key ring the cache already contains a passphrase, a - * [IllegalArgumentException] is thrown before any changes are committed to the cache. - * This is to prevent accidental passphrase override when dealing with multiple key rings containing - * keys with conflicting key-ids. + * Remember the given passphrase for all keys in the given key ring. If for the key-id of any + * key on the key ring the cache already contains a passphrase, a [IllegalArgumentException] is + * thrown before any changes are committed to the cache. This is to prevent accidental + * passphrase override when dealing with multiple key rings containing keys with conflicting + * key-ids. * - * If you can ensure that there will be no key-id clashes, and you want to replace the passphrases for the key ring, - * use [replacePassphrase] instead. + * If you can ensure that there will be no key-id clashes, and you want to replace the + * passphrases for the key ring, use [replacePassphrase] instead. * - * If you need to unlock multiple [PGPKeyRing], it is advised to use a separate [CachingSecretKeyRingProtector] - * instance for each ring. + * If you need to unlock multiple [PGPKeyRing], it is advised to use a separate + * [CachingSecretKeyRingProtector] instance for each ring. * * @param keyRing key ring * @param passphrase passphrase @@ -91,14 +93,12 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras keyRing.publicKeys.forEach { require(!cache.containsKey(it.keyID)) { "The cache already holds a passphrase for the key with ID ${it.keyID.openPgpKeyId()}.\n" + - "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." + "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead." } } // only then instert - keyRing.publicKeys.forEach { - cache[it.keyID] = passphrase - } + keyRing.publicKeys.forEach { cache[it.keyID] = passphrase } } /** @@ -118,7 +118,7 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param passphrase passphrase */ fun addPassphrase(key: PGPPublicKey, passphrase: Passphrase) = - addPassphrase(key.keyID, passphrase) + addPassphrase(key.keyID, passphrase) /** * Remember the given passphrase for the key with the given fingerprint. @@ -127,17 +127,14 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * @param passphrase passphrase */ fun addPassphrase(fingerprint: OpenPgpFingerprint, passphrase: Passphrase) = - addPassphrase(fingerprint.keyId, passphrase) + addPassphrase(fingerprint.keyId, passphrase) /** - * Remove a passphrase from the cache. - * The passphrase will be cleared and then removed. + * Remove a passphrase from the cache. The passphrase will be cleared and then removed. * * @param keyId id of the key */ - fun forgetPassphrase(keyId: Long) = apply { - cache.remove(keyId)?.clear() - } + fun forgetPassphrase(keyId: Long) = apply { cache.remove(keyId)?.clear() } /** * Forget the passphrase to all keys in the provided key ring. @@ -153,15 +150,11 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras * * @param key key */ - fun forgetPassphrase(key: PGPPublicKey) = apply { - forgetPassphrase(key.keyID) - } + fun forgetPassphrase(key: PGPPublicKey) = apply { forgetPassphrase(key.keyID) } override fun getPassphraseFor(keyId: Long): Passphrase? { - return if (hasPassphrase(keyId)) - cache[keyId] - else - provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } + return if (hasPassphrase(keyId)) cache[keyId] + else provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } } override fun hasPassphrase(keyId: Long) = cache[keyId]?.isValid ?: false @@ -171,4 +164,4 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras override fun getDecryptor(keyId: Long) = protector.getDecryptor(keyId) override fun getEncryptor(keyId: Long) = protector.getEncryptor(keyId) -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt index 6158d322..c7566f6d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/KeyRingProtectionSettings.kt @@ -8,49 +8,48 @@ import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm /** - * Secret key protection settings for iterated and salted S2K. - * The salt gets randomly chosen by the library each time. - * Note, that the s2kCount is the already encoded single-octet number. - * - * @see Encoding Formula + * Secret key protection settings for iterated and salted S2K. The salt gets randomly chosen by the + * library each time. Note, that the s2kCount is the already encoded single-octet number. * * @param encryptionAlgorithm encryption algorithm * @param hashAlgorithm hash algorithm * @param s2kCount encoded (!) s2k iteration count + * @see Encoding Formula */ data class KeyRingProtectionSettings( - val encryptionAlgorithm: SymmetricKeyAlgorithm, - val hashAlgorithm: HashAlgorithm, - val s2kCount: Int + val encryptionAlgorithm: SymmetricKeyAlgorithm, + val hashAlgorithm: HashAlgorithm, + val s2kCount: Int ) { /** - * Create a [KeyRingProtectionSettings] object using the given encryption algorithm, [HashAlgorithm.SHA1] and - * 65536 iterations. - * It is okay to use SHA1 here, since we don't care about collisions. + * Create a [KeyRingProtectionSettings] object using the given encryption algorithm, + * [HashAlgorithm.SHA1] and 65536 iterations. It is okay to use SHA1 here, since we don't care + * about collisions. * * @param encryptionAlgorithm encryption algorithm */ - constructor(encryptionAlgorithm: SymmetricKeyAlgorithm): this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60) + constructor( + encryptionAlgorithm: SymmetricKeyAlgorithm + ) : this(encryptionAlgorithm, HashAlgorithm.SHA1, 0x60) init { require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { "Unencrypted is not allowed here!" } - require(s2kCount > 0) { - "s2kCount cannot be less than 1." - } + require(s2kCount > 0) { "s2kCount cannot be less than 1." } } companion object { /** - * Secure default settings using [SymmetricKeyAlgorithm.AES_256], [HashAlgorithm.SHA256] - * and an iteration count of 65536. + * Secure default settings using [SymmetricKeyAlgorithm.AES_256], [HashAlgorithm.SHA256] and + * an iteration count of 65536. * * @return secure protection settings */ @JvmStatic - fun secureDefaultSettings() = KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60) + fun secureDefaultSettings() = + KeyRingProtectionSettings(SymmetricKeyAlgorithm.AES_256, HashAlgorithm.SHA256, 0x60) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt index 1b8df815..74ef881f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt @@ -10,54 +10,64 @@ import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProv import org.pgpainless.util.Passphrase /** - * Provides [PBESecretKeyDecryptor] and [PBESecretKeyEncryptor] objects while getting the passphrases - * from a [SecretKeyPassphraseProvider] and using settings from an [KeyRingProtectionSettings]. + * Provides [PBESecretKeyDecryptor] and [PBESecretKeyEncryptor] objects while getting the + * passphrases from a [SecretKeyPassphraseProvider] and using settings from an + * [KeyRingProtectionSettings]. */ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { - constructor(passphraseProvider: SecretKeyPassphraseProvider): super(passphraseProvider) + constructor(passphraseProvider: SecretKeyPassphraseProvider) : super(passphraseProvider) /** - * Constructor. - * Passphrases for keys are sourced from the `passphraseProvider` and decryptors/encryptors are constructed - * following the settings given in `settings`. + * Constructor. Passphrases for keys are sourced from the `passphraseProvider` and + * decryptors/encryptors are constructed following the settings given in `settings`. * * @param settings S2K settings etc. * @param passphraseProvider provider which provides passphrases. */ - constructor(settings: KeyRingProtectionSettings, - passphraseProvider: SecretKeyPassphraseProvider): super(passphraseProvider, settings) + constructor( + settings: KeyRingProtectionSettings, + passphraseProvider: SecretKeyPassphraseProvider + ) : super(passphraseProvider, settings) companion object { @JvmStatic - fun forKey(keyRing: PGPKeyRing, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector { + fun forKey( + keyRing: PGPKeyRing, + passphrase: Passphrase + ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? { - return if (hasPassphrase(keyId)) passphrase else null - } + override fun getPassphraseFor(keyId: Long): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } - override fun hasPassphrase(keyId: Long): Boolean { - return keyRing.getPublicKey(keyId) != null + override fun hasPassphrase(keyId: Long): Boolean { + return keyRing.getPublicKey(keyId) != null + } } - }.let { PasswordBasedSecretKeyRingProtector(it) } + .let { PasswordBasedSecretKeyRingProtector(it) } } @JvmStatic fun forKey(key: PGPSecretKey, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector = - forKeyId(key.publicKey.keyID, passphrase) + forKeyId(key.publicKey.keyID, passphrase) @JvmStatic - fun forKeyId(singleKeyId: Long, passphrase: Passphrase): PasswordBasedSecretKeyRingProtector { + fun forKeyId( + singleKeyId: Long, + passphrase: Passphrase + ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? { - return if (hasPassphrase(keyId)) passphrase else null - } + override fun getPassphraseFor(keyId: Long): Passphrase? { + return if (hasPassphrase(keyId)) passphrase else null + } - override fun hasPassphrase(keyId: Long): Boolean { - return keyId == singleKeyId + override fun hasPassphrase(keyId: Long): Boolean { + return keyId == singleKeyId + } } - }.let { PasswordBasedSecretKeyRingProtector(it) } + .let { PasswordBasedSecretKeyRingProtector(it) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt index 8bdac8d6..5e86d950 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/SecretKeyRingProtector.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.protection +import kotlin.jvm.Throws import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing @@ -12,14 +13,14 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider import org.pgpainless.key.protection.passphrase_provider.SolitaryPassphraseProvider import org.pgpainless.util.Passphrase -import kotlin.jvm.Throws /** * Task of the [SecretKeyRingProtector] is to map encryptor/decryptor objects to key-ids. - * [PBESecretKeyEncryptor]/[PBESecretKeyDecryptor] are used to encrypt/decrypt secret keys using a passphrase. + * [PBESecretKeyEncryptor]/[PBESecretKeyDecryptor] are used to encrypt/decrypt secret keys using a + * passphrase. * - * While it is easy to create an implementation of this interface that fits your needs, there are a bunch of - * implementations ready for use. + * While it is easy to create an implementation of this interface that fits your needs, there are a + * bunch of implementations ready for use. */ interface SecretKeyRingProtector { @@ -32,56 +33,61 @@ interface SecretKeyRingProtector { fun hasPassphraseFor(keyId: Long): Boolean /** - * Return a decryptor for the key of id `keyId`. - * This method returns null if the key is unprotected. + * Return a decryptor for the key of id `keyId`. This method returns null if the key is + * unprotected. * * @param keyId id of the key * @return decryptor for the key */ - @Throws(PGPException::class) - fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? + @Throws(PGPException::class) fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? /** - * Return an encryptor for the key of id `keyId`. - * This method returns null if the key is unprotected. + * Return an encryptor for the key of id `keyId`. This method returns null if the key is + * unprotected. * * @param keyId id of the key * @return encryptor for the key */ - @Throws(PGPException::class) - fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? + @Throws(PGPException::class) fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? companion object { /** - * Return a protector for secret keys. - * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases - * at runtime. + * Return a protector for secret keys. The protector maintains an in-memory cache of + * passphrases and can be extended with new passphrases at runtime. * - * See [CachingSecretKeyRingProtector] for how to memorize/forget additional passphrases during runtime. + * See [CachingSecretKeyRingProtector] for how to memorize/forget additional passphrases + * during runtime. * * @param missingPassphraseCallback callback that is used to provide missing passphrases. * @return caching secret key protector */ @JvmStatic fun defaultSecretKeyRingProtector( - missingPassphraseCallback: SecretKeyPassphraseProvider? - ): CachingSecretKeyRingProtector = CachingSecretKeyRingProtector( - mapOf(), KeyRingProtectionSettings.secureDefaultSettings(), missingPassphraseCallback) + missingPassphraseCallback: SecretKeyPassphraseProvider? + ): CachingSecretKeyRingProtector = + CachingSecretKeyRingProtector( + mapOf(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback) /** * Use the provided passphrase to lock/unlock all keys in the provided key ring. * - * This protector will use the provided passphrase to lock/unlock all subkeys present in the provided keys object. - * For other keys that are not present in the ring, it will return null. + * This protector will use the provided passphrase to lock/unlock all subkeys present in the + * provided keys object. For other keys that are not present in the ring, it will return + * null. * * @param passphrase passphrase * @param keys key ring * @return protector */ @JvmStatic - fun unlockEachKeyWith(passphrase: Passphrase, keys: PGPSecretKeyRing): SecretKeyRingProtector = - fromPassphraseMap(keys.map { it.keyID }.associateWith { passphrase }) + fun unlockEachKeyWith( + passphrase: Passphrase, + keys: PGPSecretKeyRing + ): SecretKeyRingProtector = + fromPassphraseMap(keys.map { it.keyID }.associateWith { passphrase }) /** * Use the provided passphrase to unlock any key. @@ -91,11 +97,11 @@ interface SecretKeyRingProtector { */ @JvmStatic fun unlockAnyKeyWith(passphrase: Passphrase): SecretKeyRingProtector = - BaseSecretKeyRingProtector(SolitaryPassphraseProvider(passphrase)) + BaseSecretKeyRingProtector(SolitaryPassphraseProvider(passphrase)) /** - * Use the provided passphrase to lock/unlock only the provided (sub-)key. - * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if + * Use the provided passphrase to lock/unlock only the provided (sub-)key. This protector + * will only return a non-null encryptor/decryptor based on the provided passphrase if * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. * * Otherwise, this protector will always return null. @@ -106,11 +112,11 @@ interface SecretKeyRingProtector { */ @JvmStatic fun unlockSingleKeyWith(passphrase: Passphrase, key: PGPSecretKey): SecretKeyRingProtector = - PasswordBasedSecretKeyRingProtector.forKey(key, passphrase) + PasswordBasedSecretKeyRingProtector.forKey(key, passphrase) /** - * Use the provided passphrase to lock/unlock only the provided (sub-)key. - * This protector will only return a non-null encryptor/decryptor based on the provided passphrase if + * Use the provided passphrase to lock/unlock only the provided (sub-)key. This protector + * will only return a non-null encryptor/decryptor based on the provided passphrase if * [getEncryptor]/[getDecryptor] is getting called with the key-id of the provided key. * * Otherwise, this protector will always return null. @@ -121,21 +127,20 @@ interface SecretKeyRingProtector { */ @JvmStatic fun unlockSingleKeyWith(passphrase: Passphrase, keyId: Long): SecretKeyRingProtector = - PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase) + PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase) /** - * Protector for unprotected keys. - * This protector returns null for all [getEncryptor]/[getDecryptor] calls, - * no matter what the key-id is. + * Protector for unprotected keys. This protector returns null for all + * [getEncryptor]/[getDecryptor] calls, no matter what the key-id is. * - * As a consequence, this protector can only "unlock" keys which are not protected using a passphrase, and it will - * leave keys unprotected, should it be used to "protect" a key - * (e.g. in [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor.changePassphraseFromOldPassphrase]). + * As a consequence, this protector can only "unlock" keys which are not protected using a + * passphrase, and it will leave keys unprotected, should it be used to "protect" a key + * (e.g. in + * [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditor.changePassphraseFromOldPassphrase]). * * @return protector */ - @JvmStatic - fun unprotectedKeys() = UnprotectedKeysProtector() + @JvmStatic fun unprotectedKeys() = UnprotectedKeysProtector() /** * Use the provided map of key-ids and passphrases to unlock keys. @@ -145,6 +150,7 @@ interface SecretKeyRingProtector { */ @JvmStatic fun fromPassphraseMap(passphraseMap: Map): SecretKeyRingProtector = - CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null) + CachingSecretKeyRingProtector( + passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index 84d7d0d7..f9fe3e1d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -5,6 +5,7 @@ package org.pgpainless.key.protection +import kotlin.jvm.Throws import openpgp.openPgpKeyId import org.bouncycastle.extensions.isEncrypted import org.bouncycastle.openpgp.PGPException @@ -16,7 +17,6 @@ import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.PublicKeyParameterValidationUtil import org.pgpainless.util.Passphrase -import kotlin.jvm.Throws class UnlockSecretKey { @@ -24,7 +24,10 @@ class UnlockSecretKey { @JvmStatic @Throws(PGPException::class, KeyIntegrityException::class) - fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { + fun unlockSecretKey( + secretKey: PGPSecretKey, + protector: SecretKeyRingProtector + ): PGPPrivateKey { return if (secretKey.isEncrypted()) { unlockSecretKey(secretKey, protector.getDecryptor(secretKey.keyID)) } else { @@ -34,23 +37,29 @@ class UnlockSecretKey { @JvmStatic @Throws(PGPException::class) - fun unlockSecretKey(secretKey: PGPSecretKey, decryptor: PBESecretKeyDecryptor?): PGPPrivateKey { - val privateKey = try { - secretKey.extractPrivateKey(decryptor) - } catch (e : PGPException) { - throw WrongPassphraseException(secretKey.keyID, e) - } + fun unlockSecretKey( + secretKey: PGPSecretKey, + decryptor: PBESecretKeyDecryptor? + ): PGPPrivateKey { + val privateKey = + try { + secretKey.extractPrivateKey(decryptor) + } catch (e: PGPException) { + throw WrongPassphraseException(secretKey.keyID, e) + } if (privateKey == null) { if (secretKey.s2K.type in 100..110) { - throw PGPException("Cannot decrypt secret key ${secretKey.keyID.openPgpKeyId()}: \n" + + throw PGPException( + "Cannot decrypt secret key ${secretKey.keyID.openPgpKeyId()}: \n" + "Unsupported private S2K type ${secretKey.s2K.type}") } throw PGPException("Cannot decrypt secret key.") } if (PGPainless.getPolicy().isEnableKeyParameterValidation()) { - PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.publicKey) + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity( + privateKey, secretKey.publicKey) } return privateKey @@ -61,8 +70,9 @@ class UnlockSecretKey { return if (passphrase == null) { unlockSecretKey(secretKey, SecretKeyRingProtector.unprotectedKeys()) } else { - unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)) + unlockSecretKey( + secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt index f87c6d3d..a25bb31a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnprotectedKeysProtector.kt @@ -4,7 +4,8 @@ package org.pgpainless.key.protection /** - * Implementation of the [SecretKeyRingProtector] which assumes that all handled keys are not password protected. + * Implementation of the [SecretKeyRingProtector] which assumes that all handled keys are not + * password protected. */ class UnprotectedKeysProtector : SecretKeyRingProtector { override fun hasPassphraseFor(keyId: Long) = true @@ -12,4 +13,4 @@ class UnprotectedKeysProtector : SecretKeyRingProtector { override fun getDecryptor(keyId: Long) = null override fun getEncryptor(keyId: Long) = null -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt index c02486ac..43cca885 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -14,12 +14,14 @@ import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector /** - * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. - * The method [replaceUsageChecksumWithUsageSha1] ensures that such keys are encrypted using - * S2K usage [SecretKeyPacket.USAGE_SHA1] instead. + * Repair class to fix keys which use S2K usage of value [SecretKeyPacket.USAGE_CHECKSUM]. The + * method [replaceUsageChecksumWithUsageSha1] ensures that such keys are encrypted using S2K usage + * [SecretKeyPacket.USAGE_SHA1] instead. * - * @see Related PGPainless Bug Report - * @see Related PGPainless Feature Request + * @see Related PGPainless Bug + * Report + * @see Related PGPainless Feature + * Request * @see Related upstream BC bug report */ class S2KUsageFix { @@ -27,23 +29,26 @@ class S2KUsageFix { companion object { /** - * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed insecure. - * This method fixes the private keys by changing them to
USAGE_SHA1
instead. + * Repair method for keys which use S2K usage
USAGE_CHECKSUM
which is deemed + * insecure. This method fixes the private keys by changing them to
USAGE_SHA1
+ * instead. * * @param keys keys * @param protector protector to unlock and re-lock affected private keys - * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will cause the subkey to stay unaffected. + * @param skipKeysWithMissingPassphrase if set to true, missing subkey passphrases will + * cause the subkey to stay unaffected. * @return fixed key ring * @throws PGPException in case of a PGP error. */ @JvmStatic @JvmOverloads fun replaceUsageChecksumWithUsageSha1( - keys: PGPSecretKeyRing, - protector: SecretKeyRingProtector, - skipKeysWithMissingPassphrase: Boolean = false + keys: PGPSecretKeyRing, + protector: SecretKeyRingProtector, + skipKeysWithMissingPassphrase: Boolean = false ): PGPSecretKeyRing { - val digestCalculator = ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1) + val digestCalculator = + ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1) val keyList = mutableListOf() for (key in keys) { // CHECKSUM is not recommended @@ -59,21 +64,22 @@ class S2KUsageFix { keyList.add(key) continue } - throw WrongPassphraseException("Missing passphrase for key with ID " + java.lang.Long.toHexString(keyId)) + throw WrongPassphraseException( + "Missing passphrase for key with ID " + java.lang.Long.toHexString(keyId)) } val privateKey = key.unlock(protector) // This constructor makes use of USAGE_SHA1 by default - val fixedKey = PGPSecretKey( + val fixedKey = + PGPSecretKey( privateKey, key.publicKey, digestCalculator, key.isMasterKey, - protector.getEncryptor(keyId) - ) + protector.getEncryptor(keyId)) keyList.add(fixedKey) } return PGPSecretKeyRing(keyList) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt index 22260126..ffa3619a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt @@ -7,10 +7,11 @@ package org.pgpainless.key.protection.passphrase_provider import org.pgpainless.util.Passphrase /** - * Implementation of the [SecretKeyPassphraseProvider] that holds a map of key-IDs and respective [Passphrase]. - * It will return the right passphrase depending on the key-id. + * Implementation of the [SecretKeyPassphraseProvider] that holds a map of key-IDs and respective + * [Passphrase]. It will return the right passphrase depending on the key-id. * * Note: This provider might return null! + * * TODO: Make this null-safe and throw an exception instead? */ class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { @@ -18,4 +19,4 @@ class MapBasedPassphraseProvider(val map: Map) : SecretKeyPass override fun getPassphraseFor(keyId: Long): Passphrase? = map[keyId] override fun hasPassphrase(keyId: Long): Boolean = map.containsKey(keyId) -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt index aaf24b70..d872da51 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt @@ -7,16 +7,13 @@ package org.pgpainless.key.protection.passphrase_provider import org.bouncycastle.openpgp.PGPSecretKey import org.pgpainless.util.Passphrase -/** - * Interface to allow the user to provide a [Passphrase] for an encrypted OpenPGP secret key. - */ +/** Interface to allow the user to provide a [Passphrase] for an encrypted OpenPGP secret key. */ interface SecretKeyPassphraseProvider { /** - * Return a passphrase for the given secret key. - * If no record is found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a [Passphrase] with - * a content of null. + * Return a passphrase for the given secret key. If no record is found, return null. Note: In + * case of an unprotected secret key, this method must may not return null, but a [Passphrase] + * with a content of null. * * @param secretKey secret key * @return passphrase or null, if no passphrase record is found. @@ -26,9 +23,9 @@ interface SecretKeyPassphraseProvider { } /** - * Return a passphrase for the given key. If no record has been found, return null. - * Note: In case of an unprotected secret key, this method must may not return null, but a [Passphrase] with - * a content of null. + * Return a passphrase for the given key. If no record has been found, return null. Note: In + * case of an unprotected secret key, this method must may not return null, but a [Passphrase] + * with a content of null. * * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. @@ -36,4 +33,4 @@ interface SecretKeyPassphraseProvider { fun getPassphraseFor(keyId: Long): Passphrase? fun hasPassphrase(keyId: Long): Boolean -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt index 46f77342..a8a1735d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt @@ -6,12 +6,10 @@ package org.pgpainless.key.protection.passphrase_provider import org.pgpainless.util.Passphrase -/** - * Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. - */ +/** Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. */ class SolitaryPassphraseProvider(val passphrase: Passphrase?) : SecretKeyPassphraseProvider { override fun getPassphraseFor(keyId: Long): Passphrase? = passphrase override fun hasPassphrase(keyId: Long): Boolean = true -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt index 63b65672..b26d84dc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -12,25 +12,24 @@ class KeyIdUtil { companion object { /** - * Convert a long key-id into a key-id. - * A long key-id is a 16 digit hex string. + * Convert a long key-id into a key-id. A long key-id is a 16 digit hex string. * * @param longKeyId 16-digit hexadecimal string * @return key-id converted to {@link Long}. */ @JvmStatic - @Deprecated("Superseded by Long extension method.", - ReplaceWith("Long.fromHexKeyId(longKeyId)")) + @Deprecated( + "Superseded by Long extension method.", ReplaceWith("Long.fromHexKeyId(longKeyId)")) fun fromLongKeyId(longKeyId: String) = Long.fromOpenPgpKeyId(longKeyId) /** * Format a long key-ID as upper-case hex string. + * * @param keyId keyId * @return hex encoded key ID */ @JvmStatic - @Deprecated("Superseded by Long extension method.", - ReplaceWith("keyId.hexKeyId()")) + @Deprecated("Superseded by Long extension method.", ReplaceWith("keyId.hexKeyId()")) fun formatKeyId(keyId: Long) = keyId.openPgpKeyId() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 79215408..29f343c1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -4,6 +4,8 @@ package org.pgpainless.key.util +import java.io.ByteArrayOutputStream +import kotlin.jvm.Throws import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket @@ -17,34 +19,32 @@ import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.key.protection.fixes.S2KUsageFix import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.ByteArrayOutputStream -import kotlin.jvm.Throws class KeyRingUtils { companion object { - @JvmStatic - private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) + @JvmStatic private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. - * If it has no primary secret key, throw a {@link NoSuchElementException}. + * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. If it + * has no primary secret key, throw a {@link NoSuchElementException}. * * @param secretKeys secret keys * @return primary secret key */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSecretKeyRing extension function.", - ReplaceWith("secretKeys.requireSecretKey(keyId)")) + @Deprecated( + "Deprecated in favor of PGPSecretKeyRing extension function.", + ReplaceWith("secretKeys.requireSecretKey(keyId)")) fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { return secretKeys.requireSecretKey(secretKeys.publicKey.keyID) } /** - * Return the primary secret key from the given secret key ring. - * If the key ring only contains subkeys (e.g. if the primary secret key is stripped), - * this method may return null. + * Return the primary secret key from the given secret key ring. If the key ring only + * contains subkeys (e.g. if the primary secret key is stripped), this method may return + * null. * * @param secretKeys secret key ring * @return primary secret key @@ -61,8 +61,8 @@ class KeyRingUtils { } /** - * Return the primary {@link PGPPublicKey} from the provided key ring. - * Throws a {@link NoSuchElementException} if the key ring has no primary public key. + * Return the primary {@link PGPPublicKey} from the provided key ring. Throws a {@link + * NoSuchElementException} if the key ring has no primary public key. * * @param keyRing key ring * @return primary public key @@ -70,11 +70,12 @@ class KeyRingUtils { @JvmStatic fun requirePrimaryPublicKeyFrom(keyRing: PGPKeyRing): PGPPublicKey { return getPrimaryPublicKey(keyRing) - ?: throw NoSuchElementException("Provided PGPKeyRing has no primary public key.") + ?: throw NoSuchElementException("Provided PGPKeyRing has no primary public key.") } /** - * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. + * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has + * none. * * @param keyRing key ring * @return primary public key @@ -91,8 +92,8 @@ class KeyRingUtils { } /** - * Require the public key with the given subKeyId from the keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. + * Require the public key with the given subKeyId from the keyRing. If no such subkey + * exists, throw an {@link NoSuchElementException}. * * @param keyRing key ring * @param subKeyId subkey id @@ -101,12 +102,13 @@ class KeyRingUtils { @JvmStatic fun requirePublicKeyFrom(keyRing: PGPKeyRing, subKeyId: Long): PGPPublicKey { return keyRing.getPublicKey(subKeyId) - ?: throw NoSuchElementException("KeyRing does not contain public key with keyId ${subKeyId.openPgpKeyId()}.") + ?: throw NoSuchElementException( + "KeyRing does not contain public key with keyId ${subKeyId.openPgpKeyId()}.") } /** - * Require the secret key with the given secret subKeyId from the secret keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. + * Require the secret key with the given secret subKeyId from the secret keyRing. If no such + * subkey exists, throw an {@link NoSuchElementException}. * * @param keyRing secret key ring * @param subKeyId subkey id @@ -115,7 +117,8 @@ class KeyRingUtils { @JvmStatic fun requireSecretKeyFrom(keyRing: PGPSecretKeyRing, subKeyId: Long): PGPSecretKey { return keyRing.getSecretKey(subKeyId) - ?: throw NoSuchElementException("KeyRing does not contain secret key with keyID ${subKeyId.openPgpKeyId()}.") + ?: throw NoSuchElementException( + "KeyRing does not contain secret key with keyID ${subKeyId.openPgpKeyId()}.") } @JvmStatic @@ -128,57 +131,67 @@ class KeyRingUtils { } /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. + * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link + * PGPSecretKeyRing}. * * @param secretKeys secret key ring * @return public key ring */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSecretKeyRing extension method.", - ReplaceWith("secretKeys.certificate", "org.bouncycastle.extensions.certificate")) + @Deprecated( + "Deprecated in favor of PGPSecretKeyRing extension method.", + ReplaceWith("secretKeys.certificate", "org.bouncycastle.extensions.certificate")) fun publicKeyRingFrom(secretKeys: PGPSecretKeyRing): PGPPublicKeyRing { return secretKeys.certificate } /** - * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in - * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. + * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing + * PGPSecretKeyRings} in the given {@link PGPSecretKeyRingCollection} and return them as a + * {@link PGPPublicKeyRingCollection}. * * @param secretKeyRings secret key ring collection * @return public key ring collection */ @JvmStatic - fun publicKeyRingCollectionFrom(secretKeyRings: PGPSecretKeyRingCollection): PGPPublicKeyRingCollection { + fun publicKeyRingCollectionFrom( + secretKeyRings: PGPSecretKeyRingCollection + ): PGPPublicKeyRingCollection { return PGPPublicKeyRingCollection( - secretKeyRings.keyRings.asSequence() - .map { it.certificate } - .toList()) + secretKeyRings.keyRings.asSequence().map { it.certificate }.toList()) } /** - * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. + * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing + * PGPPublicKeyRings}. * * @param certificates array of public key rings * @return key ring collection */ @JvmStatic - fun keyRingsToKeyRingCollection(vararg certificates: PGPPublicKeyRing): PGPPublicKeyRingCollection { + fun keyRingsToKeyRingCollection( + vararg certificates: PGPPublicKeyRing + ): PGPPublicKeyRingCollection { return PGPPublicKeyRingCollection(certificates.toList()) } /** - * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. + * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing + * PGPSecretKeyRings}. * * @param secretKeys array of secret key rings * @return secret key ring collection */ @JvmStatic - fun keyRingsToKeyRingCollection(vararg secretKeys: PGPSecretKeyRing): PGPSecretKeyRingCollection { + fun keyRingsToKeyRingCollection( + vararg secretKeys: PGPSecretKeyRing + ): PGPSecretKeyRingCollection { return PGPSecretKeyRingCollection(secretKeys.toList()) } /** - * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. + * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for + * the given key id. * * @param certificate public key ring * @param keyId id of the key in question @@ -194,8 +207,8 @@ class KeyRingUtils { * * @param keyRing key ring * @param certification key signature - * @return key ring with injected signature * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected signature */ @JvmStatic fun injectCertification(keyRing: T, certification: PGPSignature): T { @@ -210,27 +223,35 @@ class KeyRingUtils { * @param certification key signature * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} * @return key ring with injected signature - * * @throws NoSuchElementException in case that the signed key is not part of the key ring */ @JvmStatic - fun injectCertification(keyRing: T, certifiedKey: PGPPublicKey, certification: PGPSignature): T { + fun injectCertification( + keyRing: T, + certifiedKey: PGPPublicKey, + certification: PGPSignature + ): T { val secretAndPublicKeys = secretAndPublicKeys(keyRing) val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first var certificate: PGPPublicKeyRing = secretAndPublicKeys.second if (!keyRingContainsKeyWithId(certificate, certifiedKey.keyID)) { - throw NoSuchElementException("Cannot find public key with id ${certifiedKey.keyID.openPgpKeyId()} in the provided key ring.") + throw NoSuchElementException( + "Cannot find public key with id ${certifiedKey.keyID.openPgpKeyId()} in the provided key ring.") } - certificate = PGPPublicKeyRing( - certificate.publicKeys.asSequence().map { - if (it.keyID == certifiedKey.keyID) { - PGPPublicKey.addCertification(it, certification) - } else { - it + certificate = + PGPPublicKeyRing( + certificate.publicKeys + .asSequence() + .map { + if (it.keyID == certifiedKey.keyID) { + PGPPublicKey.addCertification(it, certification) + } else { + it + } } - }.toList()) + .toList()) return if (secretKeys == null) { certificate as T } else { @@ -248,16 +269,23 @@ class KeyRingUtils { * @return key ring with injected certification */ @JvmStatic - fun injectCertification(keyRing: T, userId: CharSequence, certification: PGPSignature): T { + fun injectCertification( + keyRing: T, + userId: CharSequence, + certification: PGPSignature + ): T { val secretAndPublicKeys = secretAndPublicKeys(keyRing) val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first var certificate: PGPPublicKeyRing = secretAndPublicKeys.second - certificate = PGPPublicKeyRing( + certificate = + PGPPublicKeyRing( listOf( - PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), - userId.toString(), certification) - ).plus(certificate.publicKeys.asSequence().drop(1))) + PGPPublicKey.addCertification( + requirePrimaryPublicKeyFrom(certificate), + userId.toString(), + certification)) + .plus(certificate.publicKeys.asSequence().drop(1))) return if (secretKeys == null) { certificate as T @@ -276,16 +304,23 @@ class KeyRingUtils { * @return key ring with injected user-attribute certification */ @JvmStatic - fun injectCertification(keyRing: T, userAttributes: PGPUserAttributeSubpacketVector, certification: PGPSignature): T { + fun injectCertification( + keyRing: T, + userAttributes: PGPUserAttributeSubpacketVector, + certification: PGPSignature + ): T { val secretAndPublicKeys = secretAndPublicKeys(keyRing) val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first var certificate: PGPPublicKeyRing = secretAndPublicKeys.second - certificate = PGPPublicKeyRing( + certificate = + PGPPublicKeyRing( listOf( - PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), - userAttributes, certification) - ).plus(certificate.publicKeys.asSequence().drop(1))) + PGPPublicKey.addCertification( + requirePrimaryPublicKeyFrom(certificate), + userAttributes, + certification)) + .plus(certificate.publicKeys.asSequence().drop(1))) return if (secretKeys == null) { certificate as T @@ -316,7 +351,9 @@ class KeyRingUtils { } @JvmStatic - private fun secretAndPublicKeys(keyRing: PGPKeyRing): Pair { + private fun secretAndPublicKeys( + keyRing: PGPKeyRing + ): Pair { var secretKeys: PGPSecretKeyRing? = null val certificate: PGPPublicKeyRing when (keyRing) { @@ -327,7 +364,9 @@ class KeyRingUtils { is PGPPublicKeyRing -> { certificate = keyRing } - else -> throw IllegalArgumentException("keyRing is an unknown PGPKeyRing subclass: ${keyRing.javaClass.name}") + else -> + throw IllegalArgumentException( + "keyRing is an unknown PGPKeyRing subclass: ${keyRing.javaClass.name}") } return secretKeys to certificate } @@ -340,12 +379,16 @@ class KeyRingUtils { * @return secret key ring with injected secret key */ @JvmStatic - fun keysPlusSecretKey(secretKeys: PGPSecretKeyRing, secretKey: PGPSecretKey): PGPSecretKeyRing { + fun keysPlusSecretKey( + secretKeys: PGPSecretKeyRing, + secretKey: PGPSecretKey + ): PGPSecretKeyRing { return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey) } /** * Inject the given signature into the public part of the given secret key. + * * @param secretKey secret key * @param signature signature * @return secret key with the signature injected in its public key @@ -358,15 +401,16 @@ class KeyRingUtils { } /** - * Remove the secret key of the subkey identified by the given secret key id from the key ring. - * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. + * Remove the secret key of the subkey identified by the given secret key id from the key + * ring. The public part stays attached to the key ring, so that it can still be used for + * encryption / verification of signatures. * - * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. + * This method is intended to be used to remove secret primary keys from live keys when + * those are kept in offline storage. * * @param secretKeys secret key ring * @param keyId id of the secret key to remove * @return secret key ring with removed secret key - * * @throws IOException in case of an error during serialization / deserialization of the key * @throws PGPException in case of a broken key */ @@ -376,7 +420,8 @@ class KeyRingUtils { "Bouncy Castle currently cannot deal with stripped primary secret keys." } if (secretKeys.getSecretKey(keyId) == null) { - throw NoSuchElementException("PGPSecretKeyRing does not contain secret key ${keyId.openPgpKeyId()}.") + throw NoSuchElementException( + "PGPSecretKeyRing does not contain secret key ${keyId.openPgpKeyId()}.") } val out = ByteArrayOutputStream() @@ -389,10 +434,9 @@ class KeyRingUtils { it.encode(out) } } - secretKeys.extraPublicKeys.forEach { - it.encode(out) - } - return PGPSecretKeyRing(out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) + secretKeys.extraPublicKeys.forEach { it.encode(out) } + return PGPSecretKeyRing( + out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) } /** @@ -404,7 +448,9 @@ class KeyRingUtils { */ @JvmStatic fun getStrippedDownPublicKey(bloatedKey: PGPPublicKey): PGPPublicKey { - return PGPPublicKey(bloatedKey.publicKeyPacket, ImplementationFactory.getInstance().keyFingerprintCalculator) + return PGPPublicKey( + bloatedKey.publicKeyPacket, + ImplementationFactory.getInstance().keyFingerprintCalculator) } @JvmStatic @@ -413,7 +459,7 @@ class KeyRingUtils { key.rawUserIDs.forEach { try { add(Strings.fromUTF8ByteArray(it)) - } catch (e : IllegalArgumentException) { + } catch (e: IllegalArgumentException) { LOGGER.warn("Invalid UTF-8 user-ID encountered: ${String(it)}") } } @@ -422,50 +468,62 @@ class KeyRingUtils { @JvmStatic @Throws(MissingPassphraseException::class, PGPException::class) - fun changePassphrase(keyId: Long?, - secretKeys: PGPSecretKeyRing, - oldProtector: SecretKeyRingProtector, - newProtector: SecretKeyRingProtector): PGPSecretKeyRing { + fun changePassphrase( + keyId: Long?, + secretKeys: PGPSecretKeyRing, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector + ): PGPSecretKeyRing { return if (keyId == null) { - PGPSecretKeyRing( - secretKeys.secretKeys.asSequence().map { - reencryptPrivateKey(it, oldProtector, newProtector) - }.toList() - ) - } else { - PGPSecretKeyRing( - secretKeys.secretKeys.asSequence().map { - if (it.keyID == keyId) { - reencryptPrivateKey(it, oldProtector, newProtector) - } else { - it + PGPSecretKeyRing( + secretKeys.secretKeys + .asSequence() + .map { reencryptPrivateKey(it, oldProtector, newProtector) } + .toList()) + } else { + PGPSecretKeyRing( + secretKeys.secretKeys + .asSequence() + .map { + if (it.keyID == keyId) { + reencryptPrivateKey(it, oldProtector, newProtector) + } else { + it + } } - }.toList() - ) - }.let { s2kUsageFixIfNecessary(it, newProtector) } + .toList()) + } + .let { s2kUsageFixIfNecessary(it, newProtector) } } @JvmStatic - fun reencryptPrivateKey(secretKey: PGPSecretKey, - oldProtector: SecretKeyRingProtector, - newProtector: SecretKeyRingProtector): PGPSecretKey { + fun reencryptPrivateKey( + secretKey: PGPSecretKey, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector + ): PGPSecretKey { if (secretKey.s2K != null && secretKey.s2K.type == S2K.GNU_DUMMY_S2K) { // If the key uses GNU_DUMMY_S2K we leave it as is return secretKey } - return PGPSecretKey.copyWithNewPassword(secretKey, - oldProtector.getDecryptor(secretKey.keyID), - newProtector.getEncryptor(secretKey.keyID)) + return PGPSecretKey.copyWithNewPassword( + secretKey, + oldProtector.getDecryptor(secretKey.keyID), + newProtector.getEncryptor(secretKey.keyID)) } @JvmStatic - fun s2kUsageFixIfNecessary(secretKeys: PGPSecretKeyRing, protector: SecretKeyRingProtector): PGPSecretKeyRing { - if (secretKeys.secretKeys.asSequence().any { it.s2KUsage == SecretKeyPacket.USAGE_CHECKSUM }) { + fun s2kUsageFixIfNecessary( + secretKeys: PGPSecretKeyRing, + protector: SecretKeyRingProtector + ): PGPSecretKeyRing { + if (secretKeys.secretKeys.asSequence().any { + it.s2KUsage == SecretKeyPacket.USAGE_CHECKSUM + }) { return S2KUsageFix.replaceUsageChecksumWithUsageSha1(secretKeys, protector, true) } return secretKeys } - } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt index b5576928..57f0d423 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt @@ -4,6 +4,10 @@ package org.pgpainless.key.util +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.math.BigInteger +import java.security.SecureRandom import org.bouncycastle.bcpg.* import org.bouncycastle.extensions.publicKeyAlgorithm import org.bouncycastle.openpgp.* @@ -15,16 +19,12 @@ import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.math.BigInteger -import java.security.SecureRandom /** - * Utility class to verify keys against Key Overwriting (KO) attacks. - * This class of attacks is only possible if the attacker has access to the (encrypted) secret key material. - * To execute the attack, they would modify the unauthenticated parameters of the users public key. - * Using the modified public key in combination with the unmodified secret key material can then lead to the + * Utility class to verify keys against Key Overwriting (KO) attacks. This class of attacks is only + * possible if the attacker has access to the (encrypted) secret key material. To execute the + * attack, they would modify the unauthenticated parameters of the users public key. Using the + * modified public key in combination with the unmodified secret key material can then lead to the * extraction of secret key parameters via weakly crafted messages. * * @see Key Overwriting (KO) Attacks against OpenPGP @@ -41,19 +41,32 @@ class PublicKeyParameterValidationUtil { val key = privateKey.privateKeyDataPacket when (privateKey.privateKeyDataPacket) { is RSASecretBCPGKey -> - valid = verifyRSAKeyIntegrity(key as RSASecretBCPGKey, publicKey.publicKeyPacket.key as RSAPublicBCPGKey) + valid = + verifyRSAKeyIntegrity( + key as RSASecretBCPGKey, + publicKey.publicKeyPacket.key as RSAPublicBCPGKey) is EdSecretBCPGKey -> - valid = verifyEdDsaKeyIntegrity(key as EdSecretBCPGKey, publicKey.publicKeyPacket.key as EdDSAPublicBCPGKey) + valid = + verifyEdDsaKeyIntegrity( + key as EdSecretBCPGKey, + publicKey.publicKeyPacket.key as EdDSAPublicBCPGKey) is DSASecretBCPGKey -> - valid = verifyDsaKeyIntegrity(key as DSASecretBCPGKey, publicKey.publicKeyPacket.key as DSAPublicBCPGKey) + valid = + verifyDsaKeyIntegrity( + key as DSASecretBCPGKey, + publicKey.publicKeyPacket.key as DSAPublicBCPGKey) is ElGamalSecretBCPGKey -> - valid = verifyElGamalKeyIntegrity(key as ElGamalSecretBCPGKey, publicKey.publicKeyPacket.key as ElGamalPublicBCPGKey) + valid = + verifyElGamalKeyIntegrity( + key as ElGamalSecretBCPGKey, + publicKey.publicKeyPacket.key as ElGamalPublicBCPGKey) } if (!valid) throw KeyIntegrityException() // Additional to the algorithm-specific tests further above, we also perform - // generic functionality tests with the key, such as whether it is able to decrypt encrypted data + // generic functionality tests with the key, such as whether it is able to decrypt + // encrypted data // or verify signatures. // These tests should be more or less constant time. if (algorithm.isSigningCapable()) { @@ -68,21 +81,30 @@ class PublicKeyParameterValidationUtil { @JvmStatic @Throws(KeyIntegrityException::class) - private fun verifyRSAKeyIntegrity(secretKey: RSASecretBCPGKey, publicKey: RSAPublicBCPGKey): Boolean { + private fun verifyRSAKeyIntegrity( + secretKey: RSASecretBCPGKey, + publicKey: RSAPublicBCPGKey + ): Boolean { // Verify that the public keys N is equal to private keys p*q return publicKey.modulus.equals(secretKey.primeP.multiply(secretKey.primeQ)) } @JvmStatic @Throws(KeyIntegrityException::class) - private fun verifyEdDsaKeyIntegrity(secretKey: EdSecretBCPGKey, publicKey: EdDSAPublicBCPGKey): Boolean { + private fun verifyEdDsaKeyIntegrity( + secretKey: EdSecretBCPGKey, + publicKey: EdDSAPublicBCPGKey + ): Boolean { // TODO: Implement return true } @JvmStatic @Throws(KeyIntegrityException::class) - private fun verifyDsaKeyIntegrity(privateKey: DSASecretBCPGKey, publicKey: DSAPublicBCPGKey): Boolean { + private fun verifyDsaKeyIntegrity( + privateKey: DSASecretBCPGKey, + publicKey: DSAPublicBCPGKey + ): Boolean { // Not sure what value to put here in order to have a "robust" primality check // I went with 40, since that's what SO recommends: // https://stackoverflow.com/a/6330138 @@ -134,15 +156,20 @@ class PublicKeyParameterValidationUtil { /** * Validate ElGamal public key parameters. * - * Original implementation by the openpgpjs authors: - * OpenPGP.js + * source + * * @param secretKey secret key * @param publicKey public key * @return true if supposedly valid, false if invalid */ @JvmStatic @Throws(KeyIntegrityException::class) - private fun verifyElGamalKeyIntegrity(secretKey: ElGamalSecretBCPGKey, publicKey: ElGamalPublicBCPGKey): Boolean { + private fun verifyElGamalKeyIntegrity( + secretKey: ElGamalSecretBCPGKey, + publicKey: ElGamalPublicBCPGKey + ): Boolean { val p = publicKey.p val g = publicKey.g val y = publicKey.y @@ -185,7 +212,9 @@ class PublicKeyParameterValidationUtil { } /** - * Verify that the public key can be used to successfully verify a signature made by the private key. + * Verify that the public key can be used to successfully verify a signature made by the + * private key. + * * @param privateKey private key * @param publicKey public key * @return false if signature verification fails @@ -193,16 +222,23 @@ class PublicKeyParameterValidationUtil { @JvmStatic private fun verifyCanSign(privateKey: PGPPrivateKey, publicKey: PGPPublicKey): Boolean { val data = ByteArray(512).also { SecureRandom().nextBytes(it) } - val signatureGenerator = PGPSignatureGenerator( - getInstance().getPGPContentSignerBuilder(requireFromId(publicKey.algorithm), HashAlgorithm.SHA256)) + val signatureGenerator = + PGPSignatureGenerator( + getInstance() + .getPGPContentSignerBuilder( + requireFromId(publicKey.algorithm), HashAlgorithm.SHA256)) return try { - signatureGenerator.apply { - init(SignatureType.TIMESTAMP.code, privateKey) - update(data) - }.generate().apply { - init(getInstance().pgpContentVerifierBuilderProvider, publicKey) - update(data) - }.verify() + signatureGenerator + .apply { + init(SignatureType.TIMESTAMP.code, privateKey) + update(data) + } + .generate() + .apply { + init(getInstance().pgpContentVerifierBuilderProvider, publicKey) + update(data) + } + .verify() } catch (e: PGPException) { false } @@ -211,6 +247,7 @@ class PublicKeyParameterValidationUtil { /** * Verify that the public key can be used to encrypt a message which can successfully be * decrypted using the private key. + * * @param privateKey private key * @param publicKey public key * @return false if decryption of a message encrypted with the public key fails @@ -218,10 +255,12 @@ class PublicKeyParameterValidationUtil { @JvmStatic private fun verifyCanDecrypt(privateKey: PGPPrivateKey, publicKey: PGPPublicKey): Boolean { val data = ByteArray(1024).also { SecureRandom().nextBytes(it) } - val encryptedDataGenerator = PGPEncryptedDataGenerator( - getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256)).apply { - addMethod(getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey)) - } + val encryptedDataGenerator = + PGPEncryptedDataGenerator( + getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256)) + .apply { + addMethod(getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey)) + } var out = ByteArrayOutputStream() try { @@ -230,7 +269,8 @@ class PublicKeyParameterValidationUtil { encryptedDataGenerator.close() val encryptedDataList = PGPEncryptedDataList(out.toByteArray()) val decryptorFactory = getInstance().getPublicKeyDataDecryptorFactory(privateKey) - val encryptedData = encryptedDataList.encryptedDataObjects.next() as PGPPublicKeyEncryptedData + val encryptedData = + encryptedDataList.encryptedDataObjects.next() as PGPPublicKeyEncryptedData val decrypted = encryptedData.getDataStream(decryptorFactory) out = ByteArrayOutputStream() Streams.pipeAll(decrypted, out) @@ -243,4 +283,4 @@ class PublicKeyParameterValidationUtil { return Arrays.constantTimeAreEqual(data, out.toByteArray()) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt index 39f69fbe..2300325a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/RevocationAttributes.kt @@ -4,50 +4,43 @@ package org.pgpainless.key.util -class RevocationAttributes( - val reason: Reason, - val description: String) { +class RevocationAttributes(val reason: Reason, val description: String) { /** - * Reason for revocation. - * There are two kinds of reasons: hard and soft reason. + * Reason for revocation. There are two kinds of reasons: hard and soft reason. * - * Soft revocation reasons gracefully disable keys or user-ids. - * Softly revoked keys can no longer be used to encrypt data to or to generate signatures. - * Any signature made after a key has been soft revoked is deemed invalid. - * Any signature made before the key has been soft revoked stays valid. - * Soft revoked info can be re-certified at a later point. + * Soft revocation reasons gracefully disable keys or user-ids. Softly revoked keys can no + * longer be used to encrypt data to or to generate signatures. Any signature made after a key + * has been soft revoked is deemed invalid. Any signature made before the key has been soft + * revoked stays valid. Soft revoked info can be re-certified at a later point. * * Hard revocation reasons on the other hand renders the key or user-id invalid immediately. - * Hard reasons are suitable to use if for example a key got compromised. - * Any signature made before or after a key has been hard revoked is no longer considered valid. - * Hard revoked information can also not be re-certified. + * Hard reasons are suitable to use if for example a key got compromised. Any signature made + * before or after a key has been hard revoked is no longer considered valid. Hard revoked + * information can also not be re-certified. */ enum class Reason(val code: Byte) { /** - * The key or certification is being revoked without a reason. - * This is a HARD revocation reason and cannot be undone. + * The key or certification is being revoked without a reason. This is a HARD revocation + * reason and cannot be undone. */ NO_REASON(0), /** - * The key was superseded by another key. - * This is a SOFT revocation reason and can be undone. + * The key was superseded by another key. This is a SOFT revocation reason and can be + * undone. */ KEY_SUPERSEDED(1), /** - * The key has potentially been compromised. - * This is a HARD revocation reason and cannot be undone. + * The key has potentially been compromised. This is a HARD revocation reason and cannot be + * undone. */ KEY_COMPROMISED(2), /** - * The key was retired and shall no longer be used. - * This is a SOFT revocation reason can can be undone. + * The key was retired and shall no longer be used. This is a SOFT revocation reason can can + * be undone. */ KEY_RETIRED(3), - /** - * The user-id is no longer valid. - * This is a SOFT revocation reason and can be undone. - */ + /** The user-id is no longer valid. This is a SOFT revocation reason and can be undone. */ USER_ID_NO_LONGER_VALID(32), ; @@ -59,8 +52,7 @@ class RevocationAttributes( companion object { - @JvmStatic - private val MAP = values().associateBy { it.code } + @JvmStatic private val MAP = values().associateBy { it.code } /** * Decode a machine-readable reason code. @@ -69,13 +61,13 @@ class RevocationAttributes( * @return reason */ @JvmStatic - fun fromCode(code: Byte) = MAP[code] ?: throw IllegalArgumentException("Invalid revocation reason: $code") + fun fromCode(code: Byte) = + MAP[code] ?: throw IllegalArgumentException("Invalid revocation reason: $code") /** - * Return true if the [Reason] the provided code encodes is a hard revocation reason, false - * otherwise. - * Hard revocations cannot be undone, while keys or certifications with soft revocations can be - * re-certified by placing another signature on them. + * Return true if the [Reason] the provided code encodes is a hard revocation reason, + * false otherwise. Hard revocations cannot be undone, while keys or certifications with + * soft revocations can be re-certified by placing another signature on them. * * @param code reason code * @return is hard @@ -84,21 +76,25 @@ class RevocationAttributes( fun isHardRevocation(code: Byte) = MAP[code]?.let { isHardRevocation(it) } ?: true /** - * Return true if the given [Reason] is a hard revocation, false otherwise. - * Hard revocations cannot be undone, while keys or certifications with soft revocations can be - * re-certified by placing another signature on them. + * Return true if the given [Reason] is a hard revocation, false otherwise. Hard + * revocations cannot be undone, while keys or certifications with soft revocations can + * be re-certified by placing another signature on them. * * @param reason reason * @return is hard */ @JvmStatic - fun isHardRevocation(reason: Reason) = when (reason) { - KEY_SUPERSEDED, KEY_RETIRED, USER_ID_NO_LONGER_VALID -> false - else -> true - } + fun isHardRevocation(reason: Reason) = + when (reason) { + KEY_SUPERSEDED, + KEY_RETIRED, + USER_ID_NO_LONGER_VALID -> false + else -> true + } /** * Return true if the given reason code denotes a key revocation. + * * @param code reason code * @return is key revocation */ @@ -107,14 +103,16 @@ class RevocationAttributes( /** * Return true if the given [Reason] denotes a key revocation. + * * @param reason reason * @return is key revocation */ @JvmStatic - fun isKeyRevocation(reason: Reason) = when (reason) { - USER_ID_NO_LONGER_VALID -> false - else -> true - } + fun isKeyRevocation(reason: Reason) = + when (reason) { + USER_ID_NO_LONGER_VALID -> false + else -> true + } } } @@ -124,11 +122,9 @@ class RevocationAttributes( } companion object { - @JvmStatic - fun createKeyRevocation() = WithReason(RevocationType.KEY_REVOCATION) + @JvmStatic fun createKeyRevocation() = WithReason(RevocationType.KEY_REVOCATION) - @JvmStatic - fun createCertificateRevocation() = WithReason(RevocationType.CERT_REVOCATION) + @JvmStatic fun createCertificateRevocation() = WithReason(RevocationType.CERT_REVOCATION) } class WithReason(val type: RevocationType) { @@ -143,13 +139,16 @@ class RevocationAttributes( private fun reasonTypeMatches(reason: Reason, type: RevocationType): Boolean { return when (type) { RevocationType.KEY_REVOCATION -> reason != Reason.USER_ID_NO_LONGER_VALID - RevocationType.CERT_REVOCATION -> reason == Reason.USER_ID_NO_LONGER_VALID || reason == Reason.NO_REASON + RevocationType.CERT_REVOCATION -> + reason == Reason.USER_ID_NO_LONGER_VALID || reason == Reason.NO_REASON } } } class WithDescription(val reason: Reason) { - fun withDescription(description: String): RevocationAttributes = RevocationAttributes(reason, description) + fun withDescription(description: String): RevocationAttributes = + RevocationAttributes(reason, description) + fun withoutDescription() = RevocationAttributes(reason, "") } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt index b461f6b4..c2b81700 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/UserId.kt @@ -4,11 +4,7 @@ package org.pgpainless.key.util -class UserId internal constructor( - name: String?, - comment: String?, - email: String? -) : CharSequence { +class UserId internal constructor(name: String?, comment: String?, email: String?) : CharSequence { private val _name: String? val comment: String? @@ -60,9 +56,9 @@ class UserId internal constructor( if (other !is UserId) { return false } - return isComponentEqual(_name, other._name, false) - && isComponentEqual(comment, other.comment, false) - && isComponentEqual(email, other.email, true) + return isComponentEqual(_name, other._name, false) && + isComponentEqual(comment, other.comment, false) && + isComponentEqual(email, other.email, true) } override fun get(index: Int): Char { @@ -81,59 +77,73 @@ class UserId internal constructor( return full } - private fun isComponentEqual(value: String?, otherValue: String?, ignoreCase: Boolean): Boolean = value.equals(otherValue, ignoreCase) + private fun isComponentEqual( + value: String?, + otherValue: String?, + ignoreCase: Boolean + ): Boolean = value.equals(otherValue, ignoreCase) - fun toBuilder() = builder().also { builder -> - if (this._name != null) builder.withName(_name) - if (this.comment != null) builder.withComment(comment) - if (this.email != null) builder.withEmail(email) - } + fun toBuilder() = + builder().also { builder -> + if (this._name != null) builder.withName(_name) + if (this.comment != null) builder.withComment(comment) + if (this.email != null) builder.withEmail(email) + } companion object { // Email regex: https://emailregex.com/ - // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international characters + // switched "a-z0-9" to "\p{L}\u0900-\u097F0-9" for better support for international + // characters // \\p{L} = Unicode Letters // \u0900-\u097F = Hindi Letters @JvmStatic - private val emailPattern = ("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + - "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + - "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + - "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + - "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])").toPattern() + private val emailPattern = + ("(?:[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+(?:\\.[\\p{L}\\u0900-\\u097F0-9!#\\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-" + + "\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9" + + "-]*[\\p{L}\\u0900-\\u097F0-9])?\\.)+[\\p{L}\\u0900-\\u097F0-9](?:[\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[$\\p{L}\\u0900-\\u097F0-9-]*[\\p{L}\\u0900-\\u097F0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f" + + "\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])") + .toPattern() // User-ID Regex // "Firstname Lastname (Comment) " // All groups are optional // https://www.rfc-editor.org/rfc/rfc5322#page-16 @JvmStatic - private val nameAddrPattern = "^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$".toPattern() + private val nameAddrPattern = + "^((?.+?)\\s)?(\\((?.+?)\\)\\s)?(<(?.+?)>)?$".toPattern() /** - * Parse a [UserId] from free-form text,
name-addr
or
mailbox
string and split it - * up into its components. - * Example inputs for this method: + * Parse a [UserId] from free-form text,
name-addr
or
mailbox
string + * and split it up into its components. Example inputs for this method: *
    - *
  • john@pgpainless.org
  • - *
  • <john@pgpainless.org>
  • - *
  • John Doe
  • - *
  • John Doe <john@pgpainless.org>
  • - *
  • John Doe (work email) <john@pgpainless.org>
  • + *
  • john@pgpainless.org
  • + *
  • <john@pgpainless.org>
  • + *
  • John Doe
  • + *
  • John Doe <john@pgpainless.org>
  • + *
  • John Doe (work email) <john@pgpainless.org>
  • *
- * In these cases, this method will detect email addresses, names and comments and expose those - * via the respective getters. - * This method does not support parsing mail addresses of the following formats: - *
    - *
  • Local domains without TLDs (
    user@localdomain1
    )
  • - *
  • " "@example.org
    (spaces between the quotes)
  • - *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • - *
- * Note: This method does not guarantee that
string.equals(UserId.parse(string).toString())
is true. - * For example,
UserId.parse("alice@pgpainless.org").toString()
wraps the mail address in angled brackets. * - * @see RFC5322 §3.4. Address Specification + * In these cases, this method will detect email addresses, names and comments and expose + * those via the respective getters. This method does not support parsing mail addresses of + * the following formats: + *
    + *
  • Local domains without TLDs (
    user@localdomain1
    )
  • + *
  • " "@example.org
    (spaces between the quotes)
  • + *
  • "very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
  • + *
+ * + * Note: This method does not guarantee that + *
string.equals(UserId.parse(string).toString())
is true. For example, + *
UserId.parse("alice@pgpainless.org").toString()
wraps the mail address in + * + * angled brackets. + * * @param string user-id * @return parsed UserId object + * @see RFC5322 §3.4. Address + * Specification */ @JvmStatic fun parse(string: String): UserId { @@ -152,21 +162,19 @@ class UserId internal constructor( } } - @JvmStatic - fun onlyEmail(email: String) = UserId(null, null, email) + @JvmStatic fun onlyEmail(email: String) = UserId(null, null, email) + + @JvmStatic fun nameAndEmail(name: String, email: String) = UserId(name, null, email) @JvmStatic - fun nameAndEmail(name: String, email: String) = UserId(name, null, email) - - @JvmStatic - fun compare(u1: UserId?, u2: UserId?, comparator: Comparator) = comparator.compare(u1, u2) + fun compare(u1: UserId?, u2: UserId?, comparator: Comparator) = + comparator.compare(u1, u2) @JvmStatic @Deprecated("Deprecated in favor of builde() method.", ReplaceWith("builder()")) fun newBuilder() = builder() - @JvmStatic - fun builder() = Builder() + @JvmStatic fun builder() = Builder() } class Builder internal constructor() { @@ -175,11 +183,15 @@ class UserId internal constructor( var email: String? = null fun withName(name: String) = apply { this.name = name } - fun withComment(comment: String) = apply { this.comment = comment} + + fun withComment(comment: String) = apply { this.comment = comment } + fun withEmail(email: String) = apply { this.email = email } fun noName() = apply { this.name = null } + fun noComment() = apply { this.comment = null } + fun noEmail() = apply { this.email = null } fun build() = UserId(name, comment, email) @@ -188,19 +200,18 @@ class UserId internal constructor( class DefaultComparator : Comparator { override fun compare(o1: UserId?, o2: UserId?): Int { return compareBy { it?._name } - .thenBy { it?.comment } - .thenBy { it?.email } - .compare(o1, o2) + .thenBy { it?.comment } + .thenBy { it?.email } + .compare(o1, o2) } } class DefaultIgnoreCaseComparator : Comparator { override fun compare(p0: UserId?, p1: UserId?): Int { return compareBy { it?._name?.lowercase() } - .thenBy { it?.comment?.lowercase() } - .thenBy { it?.email?.lowercase() } - .compare(p0, p1) + .thenBy { it?.comment?.lowercase() } + .thenBy { it?.email?.lowercase() } + .compare(p0, p1) } - } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt index b7c9f39e..22f42cd9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -4,21 +4,23 @@ package org.pgpainless.policy +import java.util.* import org.pgpainless.algorithm.* import org.pgpainless.util.DateUtil import org.pgpainless.util.NotationRegistry -import java.util.* class Policy( - var signatureHashAlgorithmPolicy: HashAlgorithmPolicy, - var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, - var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, - var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, - var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, - var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, - var notationRegistry: NotationRegistry) { + var signatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, + var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, + var publicKeyAlgorithmPolicy: PublicKeyAlgorithmPolicy, + var notationRegistry: NotationRegistry +) { - constructor(): this( + constructor() : + this( HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(), @@ -33,29 +35,32 @@ class Policy( fun isEnableKeyParameterValidation() = enableKeyParameterValidation - /** - * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the - * given map, if the queried usage date is BEFORE the respective termination date. - * A termination date value of
null
means no termination, resulting in the algorithm being - * acceptable, regardless of usage date. + * Create a HashAlgorithmPolicy which accepts all [HashAlgorithms][HashAlgorithm] from the given + * map, if the queried usage date is BEFORE the respective termination date. A termination date + * value of
null
means no termination, resulting in the algorithm being acceptable, + * regardless of usage date. * * @param defaultHashAlgorithm default hash algorithm * @param algorithmTerminationDates map of acceptable algorithms and their termination dates */ class HashAlgorithmPolicy( - val defaultHashAlgorithm: HashAlgorithm, - val acceptableHashAlgorithmsAndTerminationDates: Map) { + val defaultHashAlgorithm: HashAlgorithm, + val acceptableHashAlgorithmsAndTerminationDates: Map + ) { /** - * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed in - * the given list, regardless of usage date. + * Create a [HashAlgorithmPolicy] which accepts all [HashAlgorithms][HashAlgorithm] listed + * in the given list, regardless of usage date. * - * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation + * fails) * @param acceptableHashAlgorithms list of acceptable hash algorithms */ - constructor(defaultHashAlgorithm: HashAlgorithm, acceptableHashAlgorithms: List) : - this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) + constructor( + defaultHashAlgorithm: HashAlgorithm, + acceptableHashAlgorithms: List + ) : this(defaultHashAlgorithm, acceptableHashAlgorithms.associateWith { null }) fun isAcceptable(hashAlgorithm: HashAlgorithm) = isAcceptable(hashAlgorithm, Date()) @@ -64,13 +69,13 @@ class Policy( * * @param hashAlgorithm algorithm * @param referenceTime usage date (e.g. signature creation time) - * * @return acceptance */ fun isAcceptable(hashAlgorithm: HashAlgorithm, referenceTime: Date): Boolean { if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) return false - val terminationDate = acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] ?: return true + val terminationDate = + acceptableHashAlgorithmsAndTerminationDates[hashAlgorithm] ?: return true return terminationDate > referenceTime } @@ -85,66 +90,73 @@ class Policy( companion object { @JvmStatic - fun smartSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( - HashAlgorithm.SHA512, buildMap { - put(HashAlgorithm.SHA3_512, null) - put(HashAlgorithm.SHA3_256, null) - put(HashAlgorithm.SHA512, null) - put(HashAlgorithm.SHA384, null) - put(HashAlgorithm.SHA256, null) - put(HashAlgorithm.SHA224, null) - put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) - put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) - put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) - }) + fun smartSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + buildMap { + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA224, null) + put( + HashAlgorithm.RIPEMD160, + DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")) + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + }) /** - * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are acceptable - * according to 2022 standards. + * [HashAlgorithmPolicy] which only accepts signatures made using algorithms which are + * acceptable according to 2022 standards. * * Particularly this policy only accepts algorithms from the SHA2 and SHA3 families. * * @return static signature algorithm policy */ @JvmStatic - fun static2022SignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( + fun static2022SignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( HashAlgorithm.SHA512, listOf( - HashAlgorithm.SHA3_512, - HashAlgorithm.SHA3_256, - HashAlgorithm.SHA512, - HashAlgorithm.SHA384, - HashAlgorithm.SHA256, - HashAlgorithm.SHA224) - ) + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224)) /** - * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 algorithms, as well as RIPEMD160. + * Hash algorithm policy for revocation signatures, which accepts SHA1 and SHA2 + * algorithms, as well as RIPEMD160. * * @return static revocation signature hash algorithm policy */ @JvmStatic - fun static2022RevocationSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( + fun static2022RevocationSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( HashAlgorithm.SHA512, listOf( - HashAlgorithm.SHA3_512, - HashAlgorithm.SHA3_256, - HashAlgorithm.SHA512, - HashAlgorithm.SHA384, - HashAlgorithm.SHA256, - HashAlgorithm.SHA224, - HashAlgorithm.SHA1, - HashAlgorithm.RIPEMD160 - ) - ) + HashAlgorithm.SHA3_512, + HashAlgorithm.SHA3_256, + HashAlgorithm.SHA512, + HashAlgorithm.SHA384, + HashAlgorithm.SHA256, + HashAlgorithm.SHA224, + HashAlgorithm.SHA1, + HashAlgorithm.RIPEMD160)) } } class SymmetricKeyAlgorithmPolicy( - val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm, - val acceptableSymmetricKeyAlgorithms: List) { + val defaultSymmetricKeyAlgorithm: SymmetricKeyAlgorithm, + val acceptableSymmetricKeyAlgorithms: List + ) { + + fun isAcceptable(algorithm: SymmetricKeyAlgorithm) = + acceptableSymmetricKeyAlgorithms.contains(algorithm) - fun isAcceptable(algorithm: SymmetricKeyAlgorithm) = acceptableSymmetricKeyAlgorithms.contains(algorithm) fun isAcceptable(algorithmId: Int): Boolean { val algorithm = SymmetricKeyAlgorithm.fromId(algorithmId) ?: return false return isAcceptable(algorithm) @@ -169,29 +181,29 @@ class Policy( */ @JvmStatic @Deprecated( - "Not expressive - will be removed in a future release", - ReplaceWith("symmetricKeyEncryptionPolicy2022")) + "Not expressive - will be removed in a future release", + ReplaceWith("symmetricKeyEncryptionPolicy2022")) fun defaultSymmetricKeyEncryptionAlgorithmPolicy() = symmetricKeyEncryptionPolicy2022() /** - * Policy for symmetric encryption algorithms in the context of message production (encryption). - * This suite contains algorithms that are deemed safe to use in 2022. + * Policy for symmetric encryption algorithms in the context of message production + * (encryption). This suite contains algorithms that are deemed safe to use in 2022. * * @return 2022 symmetric key encryption algorithm policy */ @JvmStatic - fun symmetricKeyEncryptionPolicy2022() = SymmetricKeyAlgorithmPolicy( + fun symmetricKeyEncryptionPolicy2022() = + SymmetricKeyAlgorithmPolicy( SymmetricKeyAlgorithm.AES_128, // Reject: Unencrypted, IDEA, TripleDES, CAST5, Blowfish listOf( - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128 - )) + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128)) /** * The default symmetric decryption algorithm policy of PGPainless. @@ -200,38 +212,42 @@ class Policy( * @deprecated not expressive - will be removed in a future update */ @JvmStatic - @Deprecated("not expressive - will be removed in a future update", - ReplaceWith("symmetricKeyDecryptionPolicy2022()")) + @Deprecated( + "not expressive - will be removed in a future update", + ReplaceWith("symmetricKeyDecryptionPolicy2022()")) fun defaultSymmetricKeyDecryptionAlgorithmPolicy() = symmetricKeyDecryptionPolicy2022() /** - * Policy for symmetric key encryption algorithms in the context of message consumption (decryption). - * This suite contains algorithms that are deemed safe to use in 2022. + * Policy for symmetric key encryption algorithms in the context of message consumption + * (decryption). This suite contains algorithms that are deemed safe to use in 2022. * * @return 2022 symmetric key decryption algorithm policy */ @JvmStatic - fun symmetricKeyDecryptionPolicy2022() = SymmetricKeyAlgorithmPolicy( + fun symmetricKeyDecryptionPolicy2022() = + SymmetricKeyAlgorithmPolicy( SymmetricKeyAlgorithm.AES_128, // Reject: Unencrypted, IDEA, TripleDES, Blowfish listOf( - SymmetricKeyAlgorithm.AES_256, - SymmetricKeyAlgorithm.AES_192, - SymmetricKeyAlgorithm.AES_128, - SymmetricKeyAlgorithm.TWOFISH, - SymmetricKeyAlgorithm.CAMELLIA_256, - SymmetricKeyAlgorithm.CAMELLIA_192, - SymmetricKeyAlgorithm.CAMELLIA_128, - SymmetricKeyAlgorithm.CAST5 - )) + SymmetricKeyAlgorithm.AES_256, + SymmetricKeyAlgorithm.AES_192, + SymmetricKeyAlgorithm.AES_128, + SymmetricKeyAlgorithm.TWOFISH, + SymmetricKeyAlgorithm.CAMELLIA_256, + SymmetricKeyAlgorithm.CAMELLIA_192, + SymmetricKeyAlgorithm.CAMELLIA_128, + SymmetricKeyAlgorithm.CAST5)) } } class CompressionAlgorithmPolicy( - val defaultCompressionAlgorithm: CompressionAlgorithm, - val acceptableCompressionAlgorithms: List) { + val defaultCompressionAlgorithm: CompressionAlgorithm, + val acceptableCompressionAlgorithms: List + ) { + + fun isAcceptable(algorithm: CompressionAlgorithm) = + acceptableCompressionAlgorithms.contains(algorithm) - fun isAcceptable(algorithm: CompressionAlgorithm) = acceptableCompressionAlgorithms.contains(algorithm) fun isAcceptable(algorithmId: Int): Boolean { val algorithm = CompressionAlgorithm.fromId(algorithmId) ?: return false return isAcceptable(algorithm) @@ -242,30 +258,33 @@ class Policy( companion object { /** - * Default {@link CompressionAlgorithmPolicy} of PGPainless. - * The default compression algorithm policy accepts any compression algorithm. + * Default {@link CompressionAlgorithmPolicy} of PGPainless. The default compression + * algorithm policy accepts any compression algorithm. * * @return default algorithm policy * @deprecated not expressive - might be removed in a future release */ @JvmStatic - @Deprecated("not expressive - might be removed in a future release", - ReplaceWith("anyCompressionAlgorithmPolicy()")) + @Deprecated( + "not expressive - might be removed in a future release", + ReplaceWith("anyCompressionAlgorithmPolicy()")) fun defaultCompressionAlgorithmPolicy() = anyCompressionAlgorithmPolicy() /** - * Policy that accepts any known compression algorithm and offers [CompressionAlgorithm.ZIP] as - * default algorithm. + * Policy that accepts any known compression algorithm and offers + * [CompressionAlgorithm.ZIP] as default algorithm. * * @return compression algorithm policy */ @JvmStatic - fun anyCompressionAlgorithmPolicy() = CompressionAlgorithmPolicy( + fun anyCompressionAlgorithmPolicy() = + CompressionAlgorithmPolicy( CompressionAlgorithm.ZIP, - listOf(CompressionAlgorithm.UNCOMPRESSED, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.BZIP2, - CompressionAlgorithm.ZLIB)) + listOf( + CompressionAlgorithm.UNCOMPRESSED, + CompressionAlgorithm.ZIP, + CompressionAlgorithm.BZIP2, + CompressionAlgorithm.ZLIB)) } } @@ -283,77 +302,84 @@ class Policy( companion object { /** - * Return PGPainless' default public key algorithm policy. - * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * Return PGPainless' default public key algorithm policy. This policy is based upon + * recommendations made by the German Federal Office for Information Security (BSI). * * @return default algorithm policy * @deprecated not expressive - might be removed in a future release */ @JvmStatic - @Deprecated("not expressive - might be removed in a future release", - ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()")) + @Deprecated( + "not expressive - might be removed in a future release", + ReplaceWith("bsi2021PublicKeyAlgorithmPolicy()")) fun defaultPublicKeyAlgorithmPolicy() = bsi2021PublicKeyAlgorithmPolicy() /** - * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * This policy is based upon recommendations made by the German Federal Office for + * Information Security (BSI). * - * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, - * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. - * - * @see BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) - * @see BlueKrypt | Cryptographic Key Length Recommendation + * Basically this policy requires keys based on elliptic curves to have a bit strength + * of at least 250, and keys based on prime number factorization / discrete logarithm + * problems to have a strength of at least 2000 bits. * * @return default algorithm policy + * @see BSI - + * Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths + * (2021-01) + * @see BlueKrypt | Cryptographic Key Length + * Recommendation */ @JvmStatic - fun bsi2021PublicKeyAlgorithmPolicy() = PublicKeyAlgorithmPolicy(buildMap { - // §5.4.1 - put(PublicKeyAlgorithm.RSA_GENERAL, 2000) - put(PublicKeyAlgorithm.RSA_SIGN, 2000) - put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000) - // Note: ElGamal is not mentioned in the BSI document. - // We assume that the requirements are similar to other DH algorithms - put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000) - put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000) - // §5.4.2 - put(PublicKeyAlgorithm.DSA, 2000) - // §5.4.3 - put(PublicKeyAlgorithm.ECDSA, 250) - // Note: EdDSA is not mentioned in the BSI document. - // We assume that the requirements are similar to other EC algorithms. - put(PublicKeyAlgorithm.EDDSA, 250) - // §7.2.1 - put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) - // §7.2.2 - put(PublicKeyAlgorithm.ECDH, 250) - }) + fun bsi2021PublicKeyAlgorithmPolicy() = + PublicKeyAlgorithmPolicy( + buildMap { + // §5.4.1 + put(PublicKeyAlgorithm.RSA_GENERAL, 2000) + put(PublicKeyAlgorithm.RSA_SIGN, 2000) + put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000) + // Note: ElGamal is not mentioned in the BSI document. + // We assume that the requirements are similar to other DH algorithms + put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000) + put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000) + // §5.4.2 + put(PublicKeyAlgorithm.DSA, 2000) + // §5.4.3 + put(PublicKeyAlgorithm.ECDSA, 250) + // Note: EdDSA is not mentioned in the BSI document. + // We assume that the requirements are similar to other EC algorithms. + put(PublicKeyAlgorithm.EDDSA, 250) + // §7.2.1 + put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) + // §7.2.2 + put(PublicKeyAlgorithm.ECDH, 250) + }) } } enum class SignerUserIdValidationLevel { /** - * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in signatures strictly. - * This means, that signatures with Signer's User-ID subpackets containing a value that does not match the signer key's - * user-id exactly, will be rejected. - * E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not - * match exactly and is therefore rejected. + * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in + * signatures strictly. This means, that signatures with Signer's User-ID subpackets + * containing a value that does not match the signer key's user-id exactly, will be + * rejected. E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice + * <alice@pgpainless.org>" does not match exactly and is therefore rejected. */ STRICT, /** - * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on signature. + * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on + * signature. */ DISABLED } companion object { - @Volatile - private var INSTANCE: Policy? = null + @Volatile private var INSTANCE: Policy? = null @JvmStatic - fun getInstance() = INSTANCE ?: synchronized(this) { - INSTANCE ?: Policy().also { INSTANCE = it } - } + fun getInstance() = + INSTANCE ?: synchronized(this) { INSTANCE ?: Policy().also { INSTANCE = it } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt index 4654f529..27192953 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/BouncyCastleProviderFactory.kt @@ -4,9 +4,9 @@ package org.pgpainless.provider -import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.Provider +import org.bouncycastle.jce.provider.BouncyCastleProvider class BouncyCastleProviderFactory : ProviderFactory() { override val securityProvider: Provider = BouncyCastleProvider() -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt index 3fe127f2..531ae54b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/provider/ProviderFactory.kt @@ -7,11 +7,10 @@ package org.pgpainless.provider import java.security.Provider /** - * Allow the use of different [Provider] implementations to provide cryptographic primitives by setting - * a [ProviderFactory] singleton. - * By default, the class is initialized with a [BouncyCastleProviderFactory]. - * To make use of your own custom [Provider], call [setFactory], passing your - * own custom [ProviderFactory] instance. + * Allow the use of different [Provider] implementations to provide cryptographic primitives by + * setting a [ProviderFactory] singleton. By default, the class is initialized with a + * [BouncyCastleProviderFactory]. To make use of your own custom [Provider], call [setFactory], + * passing your own custom [ProviderFactory] instance. */ abstract class ProviderFactory { @@ -21,16 +20,14 @@ abstract class ProviderFactory { companion object { // singleton instance - @JvmStatic - var factory: ProviderFactory = BouncyCastleProviderFactory() + @JvmStatic var factory: ProviderFactory = BouncyCastleProviderFactory() @JvmStatic val provider: Provider - @JvmName("getProvider") - get() = factory.securityProvider + @JvmName("getProvider") get() = factory.securityProvider @JvmStatic val providerName: String get() = factory.securityProviderName } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index 492c4fe4..e84ed0d3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -4,6 +4,9 @@ package org.pgpainless.signature +import java.io.IOException +import java.io.InputStream +import java.util.* import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.extensions.* @@ -14,9 +17,6 @@ import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.util.ArmorUtils -import java.io.IOException -import java.io.InputStream -import java.util.* const val MAX_ITERATIONS = 10000 @@ -24,89 +24,103 @@ class SignatureUtils { companion object { /** - * Extract and return the key expiration date value from the given signature. - * If the signature does not carry a [KeyExpirationTime] subpacket, return null. + * Extract and return the key expiration date value from the given signature. If the + * signature does not carry a [KeyExpirationTime] subpacket, return null. * * @param keyCreationDate creation date of the key * @param signature signature * @return key expiration date as given by the signature */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.getKeyExpirationDate(keyCreationDate)", "org.bouncycastle.extensions.getKeyExpirationDate")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.getKeyExpirationDate(keyCreationDate)", + "org.bouncycastle.extensions.getKeyExpirationDate")) fun getKeyExpirationDate(keyCreationDate: Date, signature: PGPSignature): Date? { return signature.getKeyExpirationDate(keyCreationDate) } /** - * Return the expiration date of the signature. - * If the signature has no expiration date, this will return null. + * Return the expiration date of the signature. If the signature has no expiration date, + * this will return null. * * @param signature signature * @return expiration date of the signature, or null if it does not expire. */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.signatureExpirationDate", "org.bouncycastle.extensions.signatureExpirationDate")) - fun getSignatureExpirationDate(signature: PGPSignature): Date? = signature.signatureExpirationDate + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.signatureExpirationDate", + "org.bouncycastle.extensions.signatureExpirationDate")) + fun getSignatureExpirationDate(signature: PGPSignature): Date? = + signature.signatureExpirationDate /** * Return a new date which represents the given date plus the given amount of seconds added. * - * Since '0' is a special date value in the OpenPGP specification - * (e.g. '0' means no expiration for expiration dates), this method will return 'null' if seconds is 0. + * Since '0' is a special date value in the OpenPGP specification (e.g. '0' means no + * expiration for expiration dates), this method will return 'null' if seconds is 0. * * @param date date * @param seconds number of seconds to be added * @return date plus seconds or null if seconds is '0' */ @JvmStatic - @Deprecated("Deprecated in favor of Date extension method.", - ReplaceWith("date.plusSeconds(seconds)", "openpgp.plusSeconds")) + @Deprecated( + "Deprecated in favor of Date extension method.", + ReplaceWith("date.plusSeconds(seconds)", "openpgp.plusSeconds")) fun datePlusSeconds(date: Date, seconds: Long): Date? { return date.plusSeconds(seconds) } /** - * Return true, if the expiration date of the [PGPSignature] lays in the past. - * If no expiration date is present in the signature, it is considered non-expired. + * Return true, if the expiration date of the [PGPSignature] lays in the past. If no + * expiration date is present in the signature, it is considered non-expired. * * @param signature signature * @return true if expired, false otherwise */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.isExpired()", "org.bouncycastle.extensions.isExpired")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.isExpired()", "org.bouncycastle.extensions.isExpired")) fun isSignatureExpired(signature: PGPSignature): Boolean { return signature.isExpired() } /** - * Return true, if the expiration date of the given [PGPSignature] is past the given comparison [Date]. - * If no expiration date is present in the signature, it is considered non-expiring. + * Return true, if the expiration date of the given [PGPSignature] is past the given + * comparison [Date]. If no expiration date is present in the signature, it is considered + * non-expiring. * * @param signature signature * @param referenceTime reference date * @return true if sig is expired at reference date, false otherwise */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.isExpired(referenceTime)", "org.bouncycastle.extensions.isExpired")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith( + "signature.isExpired(referenceTime)", "org.bouncycastle.extensions.isExpired")) fun isSignatureExpired(signature: PGPSignature, referenceTime: Date): Boolean { return signature.isExpired(referenceTime) } /** - * Return true if the provided signature is a hard revocation. - * Hard revocations are revocation signatures which either carry a revocation reason of - * [Reason.KEY_COMPROMISED] or [Reason.NO_REASON], or no reason at all. + * Return true if the provided signature is a hard revocation. Hard revocations are + * revocation signatures which either carry a revocation reason of [Reason.KEY_COMPROMISED] + * or [Reason.NO_REASON], or no reason at all. * * @param signature signature * @return true if signature is a hard revocation */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension function.", - ReplaceWith("signature.isHardRevocation", "org.bouncycastle.extensions.isHardRevocation")) + @Deprecated( + "Deprecated in favor of PGPSignature extension function.", + ReplaceWith( + "signature.isHardRevocation", "org.bouncycastle.extensions.isHardRevocation")) fun isHardRevocation(signature: PGPSignature): Boolean { return signature.isHardRevocation } @@ -128,8 +142,8 @@ class SignatureUtils { } /** - * Read and return [PGPSignatures][PGPSignature]. - * This method can deal with signatures that may be binary, armored and may contain marker packets. + * Read and return [PGPSignatures][PGPSignature]. This method can deal with signatures that + * may be binary, armored and may contain marker packets. * * @param inputStream input stream * @param maxIterations number of loop iterations until reading is aborted @@ -143,13 +157,18 @@ class SignatureUtils { var i = 0 var nextObject: Any? = null - while (i++ < maxIterations && objectFactory.nextObject().also { nextObject = it } != null) { - // Since signatures are indistinguishable from randomness, there is no point in having them compressed, - // except for an attacker who is trying to exploit flaws in the decompression algorithm. + while (i++ < maxIterations && + objectFactory.nextObject().also { nextObject = it } != null) { + // Since signatures are indistinguishable from randomness, there is no point in + // having them compressed, + // except for an attacker who is trying to exploit flaws in the decompression + // algorithm. // Therefore, we ignore compressed data packets without attempting decompression. if (nextObject is PGPCompressedData) { // getInputStream() does not do decompression, contrary to getDataStream(). - Streams.drain((nextObject as PGPCompressedData).inputStream) // Skip packet without decompressing + Streams.drain( + (nextObject as PGPCompressedData) + .inputStream) // Skip packet without decompressing } if (nextObject is PGPSignatureList) { @@ -166,17 +185,20 @@ class SignatureUtils { } /** - * Determine the issuer key-id of a [PGPSignature]. - * This method first inspects the [org.bouncycastle.bcpg.sig.IssuerKeyID] subpacket of the signature and returns the key-id if present. - * If not, it inspects the [org.bouncycastle.bcpg.sig.IssuerFingerprint] packet and retrieves the key-id from the fingerprint. + * Determine the issuer key-id of a [PGPSignature]. This method first inspects the + * [org.bouncycastle.bcpg.sig.IssuerKeyID] subpacket of the signature and returns the key-id + * if present. If not, it inspects the [org.bouncycastle.bcpg.sig.IssuerFingerprint] packet + * and retrieves the key-id from the fingerprint. * * Otherwise, it returns 0. + * * @param signature signature * @return signatures issuing key id */ @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method.", - ReplaceWith("signature.issuerKeyId", "org.bouncycastle.extensions.issuerKeyId")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method.", + ReplaceWith("signature.issuerKeyId", "org.bouncycastle.extensions.issuerKeyId")) fun determineIssuerKeyId(signature: PGPSignature): Long { return signature.issuerKeyId } @@ -193,22 +215,26 @@ class SignatureUtils { } @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method", - ReplaceWith("signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method", + ReplaceWith( + "signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) fun wasIssuedBy(fingerprint: ByteArray, signature: PGPSignature): Boolean { return signature.wasIssuedBy(fingerprint) } @JvmStatic - @Deprecated("Deprecated in favor of PGPSignature extension method", - ReplaceWith("signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) + @Deprecated( + "Deprecated in favor of PGPSignature extension method", + ReplaceWith( + "signature.wasIssuedBy(fingerprint)", "org.bouncycastle.extensions.wasIssuedBy")) fun wasIssuedBy(fingerprint: OpenPgpFingerprint, signature: PGPSignature): Boolean { return signature.wasIssuedBy(fingerprint) } /** - * Extract all signatures from the given
key
which were issued by
issuerKeyId
- * over
userId
. + * Extract all signatures from the given
key
which were issued by + *
issuerKeyId
over
userId
. * * @param key public key * @param userId user-id @@ -216,28 +242,33 @@ class SignatureUtils { * @return (potentially empty) list of signatures */ @JvmStatic - fun getSignaturesOverUserIdBy(key: PGPPublicKey, userId: String, issuer: Long): List { + fun getSignaturesOverUserIdBy( + key: PGPPublicKey, + userId: String, + issuer: Long + ): List { val signatures = key.getSignaturesForID(userId) ?: return listOf() - return signatures - .asSequence() - .filter { it.keyID == issuer } - .toList() + return signatures.asSequence().filter { it.keyID == issuer }.toList() } @JvmStatic fun getDelegations(key: PGPPublicKeyRing): List { return key.publicKey.keySignatures - .asSequence() - .filter { key.getPublicKey(it.keyID) == null } // Filter out back-sigs from subkeys - .toList() + .asSequence() + .filter { key.getPublicKey(it.keyID) == null } // Filter out back-sigs from subkeys + .toList() } @JvmStatic - fun get3rdPartyCertificationsFor(key: PGPPublicKeyRing, userId: String): List { - return key.publicKey.getSignaturesForID(userId) - .asSequence() - .filter { it.keyID != key.publicKey.keyID } // Filter out self-sigs - .toList() + fun get3rdPartyCertificationsFor( + key: PGPPublicKeyRing, + userId: String + ): List { + return key.publicKey + .getSignaturesForID(userId) + .asSequence() + .filter { it.keyID != key.publicKey.keyID } // Filter out self-sigs + .toList() } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt index 126803d4..43c92959 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -4,6 +4,8 @@ package org.pgpainless.signature.consumer +import java.io.InputStream +import java.util.* import openpgp.openPgpKeyId import org.bouncycastle.extensions.issuerKeyId import org.bouncycastle.openpgp.PGPPublicKey @@ -17,18 +19,16 @@ import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.slf4j.LoggerFactory -import java.io.InputStream -import java.util.* /** - * A collection of static methods that validate signing certificates (public keys) and verify signature correctness. + * A collection of static methods that validate signing certificates (public keys) and verify + * signature correctness. */ class CertificateValidator { companion object { - @JvmStatic - private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) + @JvmStatic private val LOGGER = LoggerFactory.getLogger(CertificateValidator::class.java) /** * Check if the signing key was eligible to create the provided signature. @@ -47,43 +47,56 @@ class CertificateValidator { */ @JvmStatic @Throws(SignatureValidationException::class) - fun validateCertificate(signature: PGPSignature, - signingKeyRing: PGPPublicKeyRing, - policy: Policy = PGPainless.getPolicy()): Boolean { - val signingSubkey: PGPPublicKey = signingKeyRing.getPublicKey(signature.issuerKeyId) - ?: throw SignatureValidationException("Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") + fun validateCertificate( + signature: PGPSignature, + signingKeyRing: PGPPublicKeyRing, + policy: Policy = PGPainless.getPolicy() + ): Boolean { + val signingSubkey: PGPPublicKey = + signingKeyRing.getPublicKey(signature.issuerKeyId) + ?: throw SignatureValidationException( + "Provided key ring does not contain a subkey with id ${signature.issuerKeyId.openPgpKeyId()}.") val primaryKey = signingKeyRing.publicKey!! val directKeyAndRevSigs = mutableListOf() val rejections = mutableMapOf() // revocations - primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.code).asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } // We do not support external rev keys - .forEach { - try { - if (SignatureVerifier.verifyKeyRevocationSignature(it, primaryKey, policy, signature.creationTime)) { - directKeyAndRevSigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) + primaryKey + .getSignaturesOfType(SignatureType.KEY_REVOCATION.code) + .asSequence() + .filter { + it.issuerKeyId == primaryKey.keyID + } // We do not support external rev keys + .forEach { + try { + if (SignatureVerifier.verifyKeyRevocationSignature( + it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key revocation signature: ${e.message}", e) } + } // direct-key sigs - primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.code).asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } - .forEach { - try { - if (SignatureVerifier.verifyDirectKeySignature(it, primaryKey, policy, signature.creationTime)) { - directKeyAndRevSigs.add(it) - } - } catch (e: SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting key signature: ${e.message}, e") + primaryKey + .getSignaturesOfType(SignatureType.DIRECT_KEY.code) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifyDirectKeySignature( + it, primaryKey, policy, signature.creationTime)) { + directKeyAndRevSigs.add(it) } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting key signature: ${e.message}, e") } + } - directKeyAndRevSigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + directKeyAndRevSigs.sortWith( + SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) if (directKeyAndRevSigs.isNotEmpty()) { if (directKeyAndRevSigs[0].signatureType == SignatureType.KEY_REVOCATION.code) { throw SignatureValidationException("Primary key has been revoked.") @@ -94,12 +107,18 @@ class CertificateValidator { val userIdSignatures = mutableMapOf>() KeyRingUtils.getUserIdsIgnoringInvalidUTF8(primaryKey).forEach { userId -> buildList { - primaryKey.getSignaturesForID(userId) + primaryKey + .getSignaturesForID(userId) .asSequence() .filter { it.issuerKeyId == primaryKey.keyID } .forEach { uidSig -> try { - if (SignatureVerifier.verifySignatureOverUserId(userId, uidSig, primaryKey, policy, signature.creationTime)) { + if (SignatureVerifier.verifySignatureOverUserId( + userId, + uidSig, + primaryKey, + policy, + signature.creationTime)) { add(uidSig) } } catch (e: SignatureValidationException) { @@ -107,14 +126,19 @@ class CertificateValidator { LOGGER.debug("Rejecting user-id signature: ${e.message}", e) } } - }.sortedWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) - .let { userIdSignatures[userId] = it } + } + .sortedWith( + SignatureValidityComparator( + SignatureCreationDateComparator.Order.NEW_TO_OLD)) + .let { userIdSignatures[userId] = it } } val hasAnyUserIds = userIdSignatures.isNotEmpty() - val isAnyUserIdValid = userIdSignatures.any { entry -> - entry.value.isNotEmpty() && entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code - } + val isAnyUserIdValid = + userIdSignatures.any { entry -> + entry.value.isNotEmpty() && + entry.value[0].signatureType != SignatureType.CERTIFICATION_REVOCATION.code + } if (hasAnyUserIds && !isAnyUserIdValid) { throw SignatureValidationException("No valid user-id found.", rejections) @@ -124,12 +148,15 @@ class CertificateValidator { if (policy.signerUserIdValidationLevel == Policy.SignerUserIdValidationLevel.STRICT) { SignatureSubpacketsUtil.getSignerUserID(signature)?.let { if (userIdSignatures[it.id] == null || userIdSignatures[it.id]!!.isEmpty()) { - throw SignatureValidationException("Signature was allegedly made by user-id '${it.id}'," + + throw SignatureValidationException( + "Signature was allegedly made by user-id '${it.id}'," + " but we have no valid signatures for that on the certificate.") } - if (userIdSignatures[it.id]!![0].signatureType == SignatureType.CERTIFICATION_REVOCATION.code) { - throw SignatureValidationException("Signature was made with user-id '${it.id}' which is revoked.") + if (userIdSignatures[it.id]!![0].signatureType == + SignatureType.CERTIFICATION_REVOCATION.code) { + throw SignatureValidationException( + "Signature was made with user-id '${it.id}' which is revoked.") } } } @@ -144,32 +171,39 @@ class CertificateValidator { } } else { // signing key is subkey val subkeySigs = mutableListOf() - signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code).asSequence() - .filter { it.issuerKeyId == primaryKey.keyID } - .forEach { - try { - if (SignatureVerifier.verifySubkeyBindingRevocation(it, primaryKey, signingSubkey, policy, signature.creationTime)) { - subkeySigs.add(it) - } - } catch (e : SignatureValidationException) { - rejections[it] = e - LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e) + signingSubkey + .getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.code) + .asSequence() + .filter { it.issuerKeyId == primaryKey.keyID } + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingRevocation( + it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e) } + } - signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.code).asSequence() - .forEach { - try { - if (SignatureVerifier.verifySubkeyBindingSignature(it, primaryKey, signingSubkey, policy, signature.creationTime)) { - subkeySigs.add(it) - } - } catch (e : SignatureValidationException) { - rejections[it] = e - LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) + signingSubkey + .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) + .asSequence() + .forEach { + try { + if (SignatureVerifier.verifySubkeyBindingSignature( + it, primaryKey, signingSubkey, policy, signature.creationTime)) { + subkeySigs.add(it) } + } catch (e: SignatureValidationException) { + rejections[it] = e + LOGGER.debug("Rejecting subkey binding signature: ${e.message}", e) } + } - subkeySigs.sortWith(SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) + subkeySigs.sortWith( + SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD)) if (subkeySigs.isEmpty()) { throw SignatureValidationException("Subkey is not bound.", rejections) } @@ -180,15 +214,17 @@ class CertificateValidator { val keyFlags = SignatureSubpacketsUtil.getKeyFlags(subkeySigs[0]) if (keyFlags == null || !KeyFlag.hasKeyFlag(keyFlags.flags, KeyFlag.SIGN_DATA)) { - throw SignatureValidationException("Signature was made by key which is not capable of signing (no keyflag).") + throw SignatureValidationException( + "Signature was made by key which is not capable of signing (no keyflag).") } } return true } /** - * Validate the given signing key and then verify the given signature while parsing out the signed data. - * Uninitialized means that no signed data has been read and the hash generators state has not yet been updated. + * Validate the given signing key and then verify the given signature while parsing out the + * signed data. Uninitialized means that no signed data has been read and the hash + * generators state has not yet been updated. * * @param signature uninitialized signature * @param signedData input stream containing signed data @@ -200,18 +236,25 @@ class CertificateValidator { */ @JvmStatic @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyUninitializedSignature(signature: PGPSignature, - signedData: InputStream, - signingKeyRing: PGPPublicKeyRing, - policy: Policy, - referenceTime: Date = signature.creationTime): Boolean { - return validateCertificate(signature, signingKeyRing, policy) - && SignatureVerifier.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(signature.issuerKeyId)!!, policy, referenceTime) + fun validateCertificateAndVerifyUninitializedSignature( + signature: PGPSignature, + signedData: InputStream, + signingKeyRing: PGPPublicKeyRing, + policy: Policy, + referenceTime: Date = signature.creationTime + ): Boolean { + return validateCertificate(signature, signingKeyRing, policy) && + SignatureVerifier.verifyUninitializedSignature( + signature, + signedData, + signingKeyRing.getPublicKey(signature.issuerKeyId)!!, + policy, + referenceTime) } /** - * Validate the signing key and the given initialized signature. - * Initialized means that the signatures hash generator has already been updated by reading the signed data completely. + * Validate the signing key and the given initialized signature. Initialized means that the + * signatures hash generator has already been updated by reading the signed data completely. * * @param signature initialized signature * @param verificationKeys key ring containing the verification key @@ -221,11 +264,17 @@ class CertificateValidator { */ @JvmStatic @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyInitializedSignature(signature: PGPSignature, - verificationKeys: PGPPublicKeyRing, - policy: Policy): Boolean { + fun validateCertificateAndVerifyInitializedSignature( + signature: PGPSignature, + verificationKeys: PGPPublicKeyRing, + policy: Policy + ): Boolean { return validateCertificate(signature, verificationKeys, policy) && - SignatureVerifier.verifyInitializedSignature(signature, verificationKeys.getPublicKey(signature.issuerKeyId), policy, signature.creationTime) + SignatureVerifier.verifyInitializedSignature( + signature, + verificationKeys.getPublicKey(signature.issuerKeyId), + policy, + signature.creationTime) } /** @@ -238,12 +287,18 @@ class CertificateValidator { */ @JvmStatic @Throws(SignatureValidationException::class) - fun validateCertificateAndVerifyOnePassSignature(onePassSignature: OnePassSignatureCheck, - policy: Policy): Boolean { - return validateCertificate(onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && - SignatureVerifier.verifyOnePassSignature(onePassSignature.signature!!, - onePassSignature.verificationKeys.getPublicKey(onePassSignature.signature!!.issuerKeyId), - onePassSignature, policy) + fun validateCertificateAndVerifyOnePassSignature( + onePassSignature: OnePassSignatureCheck, + policy: Policy + ): Boolean { + return validateCertificate( + onePassSignature.signature!!, onePassSignature.verificationKeys, policy) && + SignatureVerifier.verifyOnePassSignature( + onePassSignature.signature!!, + onePassSignature.verificationKeys.getPublicKey( + onePassSignature.signature!!.issuerKeyId), + onePassSignature, + policy) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt index a315f577..4a89e0b2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/OnePassSignatureCheck.kt @@ -10,18 +10,19 @@ import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.key.SubkeyIdentifier /** - * Tuple-class that bundles together a [PGPOnePassSignature] object, a [PGPPublicKeyRing] - * destined to verify the signature, the [PGPSignature] itself and a record of whether the signature - * was verified. + * Tuple-class that bundles together a [PGPOnePassSignature] object, a [PGPPublicKeyRing] destined + * to verify the signature, the [PGPSignature] itself and a record of whether the signature was + * verified. * * @param onePassSignature the one-pass-signature packet * @param verificationKeys certificate containing the signing subkey * @param signature the signature packet */ data class OnePassSignatureCheck( - val onePassSignature: PGPOnePassSignature, - val verificationKeys: PGPPublicKeyRing, - var signature: PGPSignature? = null) { + val onePassSignature: PGPOnePassSignature, + val verificationKeys: PGPPublicKeyRing, + var signature: PGPSignature? = null +) { /** * Return an identifier for the signing key. @@ -30,4 +31,4 @@ data class OnePassSignatureCheck( */ val signingKey: SubkeyIdentifier get() = SubkeyIdentifier(verificationKeys, onePassSignature.keyID) -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt index 48a3aa96..15564773 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCheck.kt @@ -9,15 +9,17 @@ import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.key.SubkeyIdentifier /** - * Tuple-class which bundles together a signature, the signing key that created the signature, - * an identifier of the signing key and a record of whether the signature was verified. + * Tuple-class which bundles together a signature, the signing key that created the signature, an + * identifier of the signing key and a record of whether the signature was verified. * * @param signature OpenPGP signature - * @param signingKeyIdentifier identifier pointing to the exact signing key which was used to create the signature - * @param signingKeyRing certificate or key ring that contains the signing key that created the signature + * @param signingKeyIdentifier identifier pointing to the exact signing key which was used to create + * the signature + * @param signingKeyRing certificate or key ring that contains the signing key that created the + * signature */ data class SignatureCheck( - val signature: PGPSignature, - val signingKeyRing: PGPKeyRing, - val signingKeyIdentifier: SubkeyIdentifier) { -} \ No newline at end of file + val signature: PGPSignature, + val signingKeyRing: PGPKeyRing, + val signingKeyIdentifier: SubkeyIdentifier +) {} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt index 75cd274c..a913bf32 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureCreationDateComparator.kt @@ -8,27 +8,23 @@ import org.bouncycastle.openpgp.PGPSignature /** * Create a new comparator which sorts signatures according to the passed ordering. + * * @param order ordering */ -class SignatureCreationDateComparator( - private val order: Order = Order.OLD_TO_NEW -) : Comparator { +class SignatureCreationDateComparator(private val order: Order = Order.OLD_TO_NEW) : + Comparator { enum class Order { - /** - * Oldest signatures first. - */ + /** Oldest signatures first. */ OLD_TO_NEW, - /** - * Newest signatures first. - */ + /** Newest signatures first. */ NEW_TO_OLD } override fun compare(one: PGPSignature, two: PGPSignature): Int { - return when(order) { + return when (order) { Order.OLD_TO_NEW -> one.creationTime.compareTo(two.creationTime) Order.NEW_TO_OLD -> two.creationTime.compareTo(one.creationTime) } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt index 9c5b9a8d..56db7ee0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt @@ -4,8 +4,8 @@ package org.pgpainless.signature.consumer +import java.util.Date import org.bouncycastle.extensions.getPublicKeyFor -import org.bouncycastle.extensions.hasPublicKey import org.bouncycastle.extensions.isExpired import org.bouncycastle.extensions.wasIssuedBy import org.bouncycastle.openpgp.PGPKeyRing @@ -14,30 +14,24 @@ import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.SignatureType import org.pgpainless.exception.SignatureValidationException import org.pgpainless.policy.Policy -import java.util.Date -import kotlin.math.sign /** * Pick signatures from keys. * * The format of a V4 OpenPGP key is: * - * Primary-Key - * [Revocation Self Signature] - * [Direct Key Signature...] - * User ID [Signature ...] - * [User ID [Signature ...] ...] - * [User Attribute [Signature ...] ...] - * [[Subkey [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] + * Primary-Key [Revocation Self Signature] [Direct Key Signature...] User ID [Signature ...] [User + * ID [Signature ...] ...] [User Attribute [Signature ...] ...] [[Subkey + * [Binding-Signature-Revocation] Primary-Key-Binding-Signature] ...] */ class SignaturePicker { companion object { /** - * Pick the at validation date most recent valid key revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. + * Pick the at validation date most recent valid key revocation signature. If there are hard + * revocation signatures, the latest hard revocation sig is picked, even if it was created + * after validationDate or if it is already expired. * * @param keyRing key ring * @param policy policy @@ -45,21 +39,26 @@ class SignaturePicker { * @return most recent, valid key revocation signature */ @JvmStatic - fun pickCurrentRevocationSelfSignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentRevocationSelfSignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey return getSortedSignaturesOfType(primaryKey, SignatureType.KEY_REVOCATION).lastOrNull { return@lastOrNull try { - SignatureVerifier.verifyKeyRevocationSignature(it, primaryKey, policy, referenceTime) + SignatureVerifier.verifyKeyRevocationSignature( + it, primaryKey, policy, referenceTime) true // valid - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false // not valid } } } /** - * Pick the at validationDate most recent, valid direct key signature. - * This method might return null, if there is no direct key self-signature which is valid at validationDate. + * Pick the at validationDate most recent, valid direct key signature. This method might + * return null, if there is no direct key self-signature which is valid at validationDate. * * @param keyRing key ring * @param policy policy @@ -67,28 +66,38 @@ class SignaturePicker { * @return direct-key self-signature */ @JvmStatic - fun pickCurrentDirectKeySelfSignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentDirectKeySelfSignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey return pickCurrentDirectKeySignature(primaryKey, primaryKey, policy, referenceTime) } @JvmStatic - fun pickCurrentDirectKeySignature(signingKey: PGPPublicKey, signedKey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentDirectKeySignature( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { return@lastOrNull try { - SignatureVerifier.verifyDirectKeySignature(it, signingKey, signedKey, policy, referenceTime) + SignatureVerifier.verifyDirectKeySignature( + it, signingKey, signedKey, policy, referenceTime) true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } } } /** - * Pick the at validationDate latest direct key signature. - * This method might return an expired signature. - * If there are more than one direct-key signature, and some of those are not expired, the latest non-expired - * yet already effective direct-key signature will be returned. + * Pick the at validationDate latest direct key signature. This method might return an + * expired signature. If there are more than one direct-key signature, and some of those are + * not expired, the latest non-expired yet already effective direct-key signature will be + * returned. * * @param keyRing key ring * @param policy policy @@ -96,15 +105,20 @@ class SignaturePicker { * @return latest direct key signature */ @JvmStatic - fun pickLatestDirectKeySignature(keyRing: PGPKeyRing, policy: Policy, referenceTime: Date): PGPSignature? { - return pickLatestDirectKeySignature(keyRing.publicKey, keyRing.publicKey, policy, referenceTime) + fun pickLatestDirectKeySignature( + keyRing: PGPKeyRing, + policy: Policy, + referenceTime: Date + ): PGPSignature? { + return pickLatestDirectKeySignature( + keyRing.publicKey, keyRing.publicKey, policy, referenceTime) } /** * Pick the at validationDate latest direct key signature made by signingKey on signedKey. - * This method might return an expired signature. - * If a non-expired direct-key signature exists, the latest non-expired yet already effective direct-key - * signature will be returned. + * This method might return an expired signature. If a non-expired direct-key signature + * exists, the latest non-expired yet already effective direct-key signature will be + * returned. * * @param signingKey signing key (key that made the sig) * @param signedKey signed key (key that carries the sig) @@ -113,7 +127,12 @@ class SignaturePicker { * @return latest direct key sig */ @JvmStatic - fun pickLatestDirectKeySignature(signingKey: PGPPublicKey, signedKey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickLatestDirectKeySignature( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { var latest: PGPSignature? = null return getSortedSignaturesOfType(signedKey, SignatureType.DIRECT_KEY).lastOrNull { try { @@ -126,16 +145,16 @@ class SignaturePicker { SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(it) latest = it true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } } } /** - * Pick the at validationDate most recent, valid user-id revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. + * Pick the at validationDate most recent, valid user-id revocation signature. If there are + * hard revocation signatures, the latest hard revocation sig is picked, even if it was + * created after validationDate or if it is already expired. * * @param keyRing key ring * @param userId user-Id that gets revoked @@ -144,23 +163,31 @@ class SignaturePicker { * @return revocation signature */ @JvmStatic - fun pickCurrentUserIdRevocationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentUserIdRevocationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - return getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION).lastOrNull { - keyRing.getPublicKeyFor(it) ?: return@lastOrNull false // signature made by external key. skip. - return@lastOrNull try { - SignatureVerifier.verifyUserIdRevocation(userId.toString(), it, primaryKey, policy, referenceTime) - true - } catch (e : SignatureValidationException) { - false // signature not valid + return getSortedSignaturesOfType(primaryKey, SignatureType.CERTIFICATION_REVOCATION) + .lastOrNull { + keyRing.getPublicKeyFor(it) + ?: return@lastOrNull false // signature made by external key. skip. + return@lastOrNull try { + SignatureVerifier.verifyUserIdRevocation( + userId.toString(), it, primaryKey, policy, referenceTime) + true + } catch (e: SignatureValidationException) { + false // signature not valid + } } - } } /** - * Pick the at validationDate latest, valid certification self-signature for the given user-id. - * This method might return null, if there is no certification self signature for that user-id which is valid - * at validationDate. + * Pick the at validationDate latest, valid certification self-signature for the given + * user-id. This method might return null, if there is no certification self signature for + * that user-id which is valid at validationDate. * * @param keyRing keyring * @param userId userid @@ -169,25 +196,34 @@ class SignaturePicker { * @return user-id certification */ @JvmStatic - fun pickCurrentUserIdCertificationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentUserIdCertificationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - return primaryKey.getSignaturesForID(userId.toString()).asSequence() - .sortedWith(SignatureCreationDateComparator()) - .lastOrNull { - return@lastOrNull it.wasIssuedBy(primaryKey) && try { - SignatureVerifier.verifyUserIdCertification(userId.toString(), it, primaryKey, policy, referenceTime) + return primaryKey + .getSignaturesForID(userId.toString()) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull it.wasIssuedBy(primaryKey) && + try { + SignatureVerifier.verifyUserIdCertification( + userId.toString(), it, primaryKey, policy, referenceTime) true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } - } + } } /** * Pick the at validationDate latest certification self-signature for the given user-id. - * This method might return an expired signature. - * If a non-expired user-id certification signature exists, the latest non-expired yet already effective - * user-id certification signature for the given user-id will be returned. + * This method might return an expired signature. If a non-expired user-id certification + * signature exists, the latest non-expired yet already effective user-id certification + * signature for the given user-id will be returned. * * @param keyRing keyring * @param userId userid @@ -196,28 +232,38 @@ class SignaturePicker { * @return user-id certification */ @JvmStatic - fun pickLatestUserIdCertificationSignature(keyRing: PGPKeyRing, userId: CharSequence, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickLatestUserIdCertificationSignature( + keyRing: PGPKeyRing, + userId: CharSequence, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - return primaryKey.getSignaturesForID(userId.toString()).asSequence() - .sortedWith(SignatureCreationDateComparator()) - .lastOrNull { - return@lastOrNull try { - SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(it) - SignatureValidator.signatureIsCertification().verify(it) - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) - SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) - SignatureValidator.correctSignatureOverUserId(userId.toString(), primaryKey, primaryKey).verify(it) - true - } catch (e : SignatureValidationException) { - false - } + return primaryKey + .getSignaturesForID(userId.toString()) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .lastOrNull { + return@lastOrNull try { + SignatureValidator.wasPossiblyMadeByKey(primaryKey).verify(it) + SignatureValidator.signatureIsCertification().verify(it) + SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy) + .verify(it) + SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) + SignatureValidator.correctSignatureOverUserId( + userId.toString(), primaryKey, primaryKey) + .verify(it) + true + } catch (e: SignatureValidationException) { + false } + } } /** - * Pick the at validationDate most recent, valid subkey revocation signature. - * If there are hard revocation signatures, the latest hard revocation sig is picked, even if it was created after - * validationDate or if it is already expired. + * Pick the at validationDate most recent, valid subkey revocation signature. If there are + * hard revocation signatures, the latest hard revocation sig is picked, even if it was + * created after validationDate or if it is already expired. * * @param keyRing keyring * @param subkey subkey @@ -226,14 +272,22 @@ class SignaturePicker { * @return subkey revocation signature */ @JvmStatic - fun pickCurrentSubkeyBindingRevocationSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentSubkeyBindingRevocationSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding revocations." } + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding revocations." + } return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_REVOCATION).lastOrNull { return@lastOrNull try { - SignatureVerifier.verifySubkeyBindingRevocation(it, primaryKey, subkey, policy, referenceTime) + SignatureVerifier.verifySubkeyBindingRevocation( + it, primaryKey, subkey, policy, referenceTime) true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } } @@ -241,8 +295,8 @@ class SignaturePicker { /** * Pick the at validationDate latest, valid subkey binding signature for the given subkey. - * This method might return null, if there is no subkey binding signature which is valid - * at validationDate. + * This method might return null, if there is no subkey binding signature which is valid at + * validationDate. * * @param keyRing key ring * @param subkey subkey @@ -251,24 +305,32 @@ class SignaturePicker { * @return most recent valid subkey binding signature */ @JvmStatic - fun pickCurrentSubkeyBindingSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickCurrentSubkeyBindingSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding signatures." } + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding signatures." + } return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { return@lastOrNull try { - SignatureVerifier.verifySubkeyBindingSignature(it, primaryKey, subkey, policy, referenceTime) + SignatureVerifier.verifySubkeyBindingSignature( + it, primaryKey, subkey, policy, referenceTime) true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } } } /** - * Pick the at validationDate latest subkey binding signature for the given subkey. - * This method might return an expired signature. - * If a non-expired subkey binding signature exists, the latest non-expired yet already effective - * subkey binding signature for the given subkey will be returned. + * Pick the at validationDate latest subkey binding signature for the given subkey. This + * method might return an expired signature. If a non-expired subkey binding signature + * exists, the latest non-expired yet already effective subkey binding signature for the + * given subkey will be returned. * * @param keyRing key ring * @param subkey subkey @@ -277,9 +339,16 @@ class SignaturePicker { * @return subkey binding signature */ @JvmStatic - fun pickLatestSubkeyBindingSignature(keyRing: PGPKeyRing, subkey: PGPPublicKey, policy: Policy, referenceTime: Date): PGPSignature? { + fun pickLatestSubkeyBindingSignature( + keyRing: PGPKeyRing, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): PGPSignature? { val primaryKey = keyRing.publicKey - require(primaryKey.keyID != subkey.keyID) { "Primary key cannot have subkey binding signatures." } + require(primaryKey.keyID != subkey.keyID) { + "Primary key cannot have subkey binding signatures." + } var latest: PGPSignature? = null return getSortedSignaturesOfType(subkey, SignatureType.SUBKEY_BINDING).lastOrNull { return@lastOrNull try { @@ -287,24 +356,28 @@ class SignaturePicker { SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(it) SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(it) SignatureValidator.signatureIsAlreadyEffective(referenceTime).verify(it) - // if the currently latest signature is not yet expired, check if the next candidate is not yet expired + // if the currently latest signature is not yet expired, check if the next + // candidate is not yet expired if (latest != null && !latest!!.isExpired(referenceTime)) { SignatureValidator.signatureIsNotYetExpired(referenceTime).verify(it) } SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(it) latest = it true - } catch (e : SignatureValidationException) { + } catch (e: SignatureValidationException) { false } } } @JvmStatic - private fun getSortedSignaturesOfType(key: PGPPublicKey, type: SignatureType): List = - key.getSignaturesOfType(type.code).asSequence() - .sortedWith(SignatureCreationDateComparator()) - .toList() + private fun getSortedSignaturesOfType( + key: PGPPublicKey, + type: SignatureType + ): List = + key.getSignaturesOfType(type.code) + .asSequence() + .sortedWith(SignatureCreationDateComparator()) + .toList() } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt index f2b586ae..38c409b7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt @@ -10,15 +10,17 @@ import org.bouncycastle.openpgp.PGPSignature /** * Comparator which sorts signatures based on an ordering and on revocation hardness. * - * If a list of signatures gets ordered using this comparator, hard revocations will always - * come first. - * Further, signatures are ordered by date according to the [SignatureCreationDateComparator.Order]. + * If a list of signatures gets ordered using this comparator, hard revocations will always come + * first. Further, signatures are ordered by date according to the + * [SignatureCreationDateComparator.Order]. */ class SignatureValidityComparator( - order: SignatureCreationDateComparator.Order = SignatureCreationDateComparator.Order.OLD_TO_NEW + order: SignatureCreationDateComparator.Order = SignatureCreationDateComparator.Order.OLD_TO_NEW ) : Comparator { - private val creationDateComparator: SignatureCreationDateComparator = SignatureCreationDateComparator(order) + private val creationDateComparator: SignatureCreationDateComparator = + SignatureCreationDateComparator(order) + override fun compare(one: PGPSignature, two: PGPSignature): Int { return if (one.isHardRevocation == two.isHardRevocation) { // Both have the same hardness, so compare creation time @@ -27,5 +29,4 @@ class SignatureValidityComparator( // else favor the "harder" signature else if (one.isHardRevocation) -1 else 1 } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt index 66618b23..3586eecf 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt @@ -4,14 +4,14 @@ package org.pgpainless.signature.subpackets +import java.io.IOException +import java.net.URL +import java.util.* import org.bouncycastle.bcpg.sig.* import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm -import java.io.IOException -import java.net.URL -import java.util.* interface BaseSignatureSubpackets { @@ -22,9 +22,8 @@ interface BaseSignatureSubpackets { * * @param key key * @return this - * - * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain any - * [IssuerKeyID] packets. + * @deprecated this method MUST NOT be used for OpenPGP v6, since v6 signatures MUST NOT contain + * any [IssuerKeyID] packets. */ fun setIssuerFingerprintAndKeyId(key: PGPPublicKey): BaseSignatureSubpackets @@ -46,13 +45,22 @@ interface BaseSignatureSubpackets { fun setSignatureCreationTime(creationTime: SignatureCreationTime?): BaseSignatureSubpackets - fun setSignatureExpirationTime(creationTime: Date, expirationTime: Date?): BaseSignatureSubpackets + fun setSignatureExpirationTime( + creationTime: Date, + expirationTime: Date? + ): BaseSignatureSubpackets - fun setSignatureExpirationTime(isCritical: Boolean, creationTime: Date, expirationTime: Date?): BaseSignatureSubpackets + fun setSignatureExpirationTime( + isCritical: Boolean, + creationTime: Date, + expirationTime: Date? + ): BaseSignatureSubpackets fun setSignatureExpirationTime(isCritical: Boolean, seconds: Long): BaseSignatureSubpackets - fun setSignatureExpirationTime(expirationTime: SignatureExpirationTime?): BaseSignatureSubpackets + fun setSignatureExpirationTime( + expirationTime: SignatureExpirationTime? + ): BaseSignatureSubpackets fun setSignerUserId(userId: CharSequence): BaseSignatureSubpackets @@ -60,9 +68,18 @@ interface BaseSignatureSubpackets { fun setSignerUserId(signerUserID: SignerUserID?): BaseSignatureSubpackets - fun addNotationData(isCritical: Boolean, notationName: String, notationValue: String): BaseSignatureSubpackets + fun addNotationData( + isCritical: Boolean, + notationName: String, + notationValue: String + ): BaseSignatureSubpackets - fun addNotationData(isCritical: Boolean, isHumanReadable: Boolean, notationName: String, notationValue: String): BaseSignatureSubpackets + fun addNotationData( + isCritical: Boolean, + isHumanReadable: Boolean, + notationName: String, + notationValue: String + ): BaseSignatureSubpackets fun addNotationData(notationData: NotationData): BaseSignatureSubpackets @@ -70,9 +87,14 @@ interface BaseSignatureSubpackets { fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): BaseSignatureSubpackets - fun addIntendedRecipientFingerprint(isCritical: Boolean, recipientKey: PGPPublicKey): BaseSignatureSubpackets + fun addIntendedRecipientFingerprint( + isCritical: Boolean, + recipientKey: PGPPublicKey + ): BaseSignatureSubpackets - fun addIntendedRecipientFingerprint(intendedRecipient: IntendedRecipientFingerprint): BaseSignatureSubpackets + fun addIntendedRecipientFingerprint( + intendedRecipient: IntendedRecipientFingerprint + ): BaseSignatureSubpackets fun clearIntendedRecipientFingerprints(): BaseSignatureSubpackets @@ -104,9 +126,18 @@ interface BaseSignatureSubpackets { fun setRevocable(revocable: Revocable?): BaseSignatureSubpackets - fun setSignatureTarget(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): BaseSignatureSubpackets + fun setSignatureTarget( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): BaseSignatureSubpackets - fun setSignatureTarget(isCritical: Boolean, keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): BaseSignatureSubpackets + fun setSignatureTarget( + isCritical: Boolean, + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): BaseSignatureSubpackets fun setSignatureTarget(signatureTarget: SignatureTarget?): BaseSignatureSubpackets @@ -125,4 +156,4 @@ interface BaseSignatureSubpackets { fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): BaseSignatureSubpackets fun clearEmbeddedSignatures(): BaseSignatureSubpackets -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt index 423edd8e..c3edf2c4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt @@ -7,5 +7,4 @@ package org.pgpainless.signature.subpackets interface CertificationSubpackets : BaseSignatureSubpackets { interface Callback : SignatureSubpacketCallback - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt index 327ed4a9..2e152f9e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt @@ -11,11 +11,20 @@ interface RevocationSignatureSubpackets : BaseSignatureSubpackets { interface Callback : SignatureSubpacketCallback - fun setRevocationReason(revocationAttributes: RevocationAttributes): RevocationSignatureSubpackets + fun setRevocationReason( + revocationAttributes: RevocationAttributes + ): RevocationSignatureSubpackets - fun setRevocationReason(isCritical: Boolean, revocationAttributes: RevocationAttributes): RevocationSignatureSubpackets + fun setRevocationReason( + isCritical: Boolean, + revocationAttributes: RevocationAttributes + ): RevocationSignatureSubpackets - fun setRevocationReason(isCritical: Boolean, reason: RevocationAttributes.Reason, description: CharSequence): RevocationSignatureSubpackets + fun setRevocationReason( + isCritical: Boolean, + reason: RevocationAttributes.Reason, + description: CharSequence + ): RevocationSignatureSubpackets fun setRevocationReason(reason: RevocationReason?): RevocationSignatureSubpackets -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt index ce8e09a9..d1b2b428 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt @@ -4,6 +4,7 @@ package org.pgpainless.signature.subpackets +import java.util.* import org.bouncycastle.bcpg.sig.Features import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.bcpg.sig.KeyFlags @@ -12,7 +13,6 @@ import org.bouncycastle.bcpg.sig.PrimaryUserID import org.bouncycastle.bcpg.sig.RevocationKey import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.algorithm.* -import java.util.* interface SelfSignatureSubpackets : BaseSignatureSubpackets { @@ -34,35 +34,66 @@ interface SelfSignatureSubpackets : BaseSignatureSubpackets { fun setKeyExpirationTime(key: PGPPublicKey, keyExpirationTime: Date?): SelfSignatureSubpackets - fun setKeyExpirationTime(keyCreationTime: Date, keyExpirationTime: Date?): SelfSignatureSubpackets + fun setKeyExpirationTime( + keyCreationTime: Date, + keyExpirationTime: Date? + ): SelfSignatureSubpackets - fun setKeyExpirationTime(isCritical: Boolean, keyCreationTime: Date, keyExpirationTime: Date?): SelfSignatureSubpackets + fun setKeyExpirationTime( + isCritical: Boolean, + keyCreationTime: Date, + keyExpirationTime: Date? + ): SelfSignatureSubpackets - fun setKeyExpirationTime(isCritical: Boolean, secondsFromCreationToExpiration: Long): SelfSignatureSubpackets + fun setKeyExpirationTime( + isCritical: Boolean, + secondsFromCreationToExpiration: Long + ): SelfSignatureSubpackets fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SelfSignatureSubpackets - fun setPreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): SelfSignatureSubpackets + fun setPreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): SelfSignatureSubpackets - fun setPreferredCompressionAlgorithms(algorithms: Collection): SelfSignatureSubpackets + fun setPreferredCompressionAlgorithms( + algorithms: Collection + ): SelfSignatureSubpackets - fun setPreferredCompressionAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + fun setPreferredCompressionAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets - fun setPreferredCompressionAlgorithms(preferredAlgorithms: PreferredAlgorithms?): SelfSignatureSubpackets + fun setPreferredCompressionAlgorithms( + preferredAlgorithms: PreferredAlgorithms? + ): SelfSignatureSubpackets - fun setPreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): SelfSignatureSubpackets + fun setPreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): SelfSignatureSubpackets - fun setPreferredSymmetricKeyAlgorithms(algorithms: Collection): SelfSignatureSubpackets + fun setPreferredSymmetricKeyAlgorithms( + algorithms: Collection + ): SelfSignatureSubpackets - fun setPreferredSymmetricKeyAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + fun setPreferredSymmetricKeyAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets - fun setPreferredSymmetricKeyAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets + fun setPreferredSymmetricKeyAlgorithms( + algorithms: PreferredAlgorithms? + ): SelfSignatureSubpackets fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SelfSignatureSubpackets fun setPreferredHashAlgorithms(algorithms: Collection): SelfSignatureSubpackets - fun setPreferredHashAlgorithms(isCritical: Boolean, algorithms: Collection): SelfSignatureSubpackets + fun setPreferredHashAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SelfSignatureSubpackets fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SelfSignatureSubpackets @@ -70,7 +101,11 @@ interface SelfSignatureSubpackets : BaseSignatureSubpackets { fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets - fun addRevocationKey(isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey): SelfSignatureSubpackets + fun addRevocationKey( + isCritical: Boolean, + isSensitive: Boolean, + revocationKey: PGPPublicKey + ): SelfSignatureSubpackets fun addRevocationKey(revocationKey: RevocationKey): SelfSignatureSubpackets @@ -81,4 +116,4 @@ interface SelfSignatureSubpackets : BaseSignatureSubpackets { fun setFeatures(isCritical: Boolean, vararg features: Feature): SelfSignatureSubpackets fun setFeatures(features: Features?): SelfSignatureSubpackets -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt index d147dc97..fbc56035 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketCallback.kt @@ -23,4 +23,4 @@ interface SignatureSubpacketCallback { fun modifyUnhashedSubpackets(unhashedSubpackets: S) { // Empty default implementation to allow for cleaner overriding } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt index a88e4e8a..e8fe2d94 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -4,6 +4,10 @@ package org.pgpainless.signature.subpackets +import java.lang.IllegalArgumentException +import java.net.URL +import java.util.* +import kotlin.experimental.or import openpgp.secondsTill import openpgp.toSecondsPrecision import org.bouncycastle.bcpg.SignatureSubpacket @@ -14,13 +18,12 @@ import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPSignatureSubpacketVector import org.pgpainless.algorithm.* import org.pgpainless.key.util.RevocationAttributes -import java.lang.IllegalArgumentException -import java.net.URL -import java.util.* -import kotlin.experimental.or -class SignatureSubpackets - : BaseSignatureSubpackets, SelfSignatureSubpackets, CertificationSubpackets, RevocationSignatureSubpackets { +class SignatureSubpackets : + BaseSignatureSubpackets, + SelfSignatureSubpackets, + CertificationSubpackets, + RevocationSignatureSubpackets { interface Callback : SignatureSubpacketCallback @@ -52,7 +55,10 @@ class SignatureSubpackets companion object { @JvmStatic - fun refreshHashedSubpackets(issuer: PGPPublicKey, oldSignature: PGPSignature): SignatureSubpackets { + fun refreshHashedSubpackets( + issuer: PGPPublicKey, + oldSignature: PGPSignature + ): SignatureSubpackets { return createHashedSubpacketsFrom(issuer, oldSignature.hashedSubPackets) } @@ -62,10 +68,11 @@ class SignatureSubpackets } @JvmStatic - fun createHashedSubpacketsFrom(issuer: PGPPublicKey, base: PGPSignatureSubpacketVector): SignatureSubpackets { - return createSubpacketsFrom(base).apply { - setIssuerFingerprintAndKeyId(issuer) - } + fun createHashedSubpacketsFrom( + issuer: PGPPublicKey, + base: PGPSignatureSubpacketVector + ): SignatureSubpackets { + return createSubpacketsFrom(base).apply { setIssuerFingerprintAndKeyId(issuer) } } @JvmStatic @@ -84,15 +91,23 @@ class SignatureSubpackets } } - override fun setRevocationReason(revocationAttributes: RevocationAttributes): SignatureSubpackets = apply { - setRevocationReason(false, revocationAttributes) + override fun setRevocationReason( + revocationAttributes: RevocationAttributes + ): SignatureSubpackets = apply { setRevocationReason(false, revocationAttributes) } + + override fun setRevocationReason( + isCritical: Boolean, + revocationAttributes: RevocationAttributes + ): SignatureSubpackets = apply { + setRevocationReason( + isCritical, revocationAttributes.reason, revocationAttributes.description) } - override fun setRevocationReason(isCritical: Boolean, revocationAttributes: RevocationAttributes): SignatureSubpackets = apply { - setRevocationReason(isCritical, revocationAttributes.reason, revocationAttributes.description) - } - - override fun setRevocationReason(isCritical: Boolean, reason: RevocationAttributes.Reason, description: CharSequence): SignatureSubpackets = apply { + override fun setRevocationReason( + isCritical: Boolean, + reason: RevocationAttributes.Reason, + description: CharSequence + ): SignatureSubpackets = apply { setRevocationReason(RevocationReason(isCritical, reason.code, description.toString())) } @@ -108,17 +123,16 @@ class SignatureSubpackets setKeyFlags(true, *keyFlags.toTypedArray()) } - override fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SignatureSubpackets = apply { - setKeyFlags(KeyFlags(isCritical, KeyFlag.toBitmask(*keyFlags))) - } + override fun setKeyFlags(isCritical: Boolean, vararg keyFlags: KeyFlag): SignatureSubpackets = + apply { + setKeyFlags(KeyFlags(isCritical, KeyFlag.toBitmask(*keyFlags))) + } override fun setKeyFlags(keyFlags: KeyFlags?): SignatureSubpackets = apply { this.keyFlagsSubpacket = keyFlags } - override fun setPrimaryUserId(): SignatureSubpackets = apply { - setPrimaryUserId(true) - } + override fun setPrimaryUserId(): SignatureSubpackets = apply { setPrimaryUserId(true) } override fun setPrimaryUserId(isCritical: Boolean): SignatureSubpackets = apply { setPrimaryUserId(PrimaryUserID(isCritical, true)) @@ -128,15 +142,23 @@ class SignatureSubpackets this.primaryUserIdSubpacket = primaryUserID } - override fun setKeyExpirationTime(key: PGPPublicKey, keyExpirationTime: Date?): SignatureSubpackets = apply { - setKeyExpirationTime(key.creationTime, keyExpirationTime) - } + override fun setKeyExpirationTime( + key: PGPPublicKey, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { setKeyExpirationTime(key.creationTime, keyExpirationTime) } - override fun setKeyExpirationTime(keyCreationTime: Date, keyExpirationTime: Date?): SignatureSubpackets = apply { + override fun setKeyExpirationTime( + keyCreationTime: Date, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { setKeyExpirationTime(true, keyCreationTime, keyExpirationTime) } - override fun setKeyExpirationTime(isCritical: Boolean, keyCreationTime: Date, keyExpirationTime: Date?): SignatureSubpackets = apply { + override fun setKeyExpirationTime( + isCritical: Boolean, + keyCreationTime: Date, + keyExpirationTime: Date? + ): SignatureSubpackets = apply { if (keyExpirationTime == null) { setKeyExpirationTime(isCritical, 0) } else { @@ -144,94 +166,124 @@ class SignatureSubpackets } } - override fun setKeyExpirationTime(isCritical: Boolean, secondsFromCreationToExpiration: Long): SignatureSubpackets = apply { + override fun setKeyExpirationTime( + isCritical: Boolean, + secondsFromCreationToExpiration: Long + ): SignatureSubpackets = apply { enforceExpirationBounds(secondsFromCreationToExpiration) setKeyExpirationTime(KeyExpirationTime(isCritical, secondsFromCreationToExpiration)) } - override fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SignatureSubpackets = apply { - this.keyExpirationTimeSubpacket = keyExpirationTime - } + override fun setKeyExpirationTime(keyExpirationTime: KeyExpirationTime?): SignatureSubpackets = + apply { + this.keyExpirationTimeSubpacket = keyExpirationTime + } - override fun setPreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): SignatureSubpackets = apply { - setPreferredCompressionAlgorithms(setOf(*algorithms)) - } + override fun setPreferredCompressionAlgorithms( + vararg algorithms: CompressionAlgorithm + ): SignatureSubpackets = apply { setPreferredCompressionAlgorithms(setOf(*algorithms)) } - override fun setPreferredCompressionAlgorithms(algorithms: Collection): SignatureSubpackets = apply { - setPreferredCompressionAlgorithms(false, algorithms) - } + override fun setPreferredCompressionAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredCompressionAlgorithms(false, algorithms) } - override fun setPreferredCompressionAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { - setPreferredCompressionAlgorithms(PreferredAlgorithms( + override fun setPreferredCompressionAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredCompressionAlgorithms( + PreferredAlgorithms( SignatureSubpacketTags.PREFERRED_COMP_ALGS, isCritical, algorithms.map { it.algorithmId }.toIntArray())) } - override fun setPreferredCompressionAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { - require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_COMP_ALGS) { - "Invalid preferred compression algorithms type." - } + override fun setPreferredCompressionAlgorithms( + algorithms: PreferredAlgorithms? + ): SignatureSubpackets = apply { + require( + algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_COMP_ALGS) { + "Invalid preferred compression algorithms type." + } this.preferredCompressionAlgorithmsSubpacket = algorithms } - override fun setPreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): SignatureSubpackets = apply { - setPreferredSymmetricKeyAlgorithms(setOf(*algorithms)) - } + override fun setPreferredSymmetricKeyAlgorithms( + vararg algorithms: SymmetricKeyAlgorithm + ): SignatureSubpackets = apply { setPreferredSymmetricKeyAlgorithms(setOf(*algorithms)) } - override fun setPreferredSymmetricKeyAlgorithms(algorithms: Collection): SignatureSubpackets = apply { - setPreferredSymmetricKeyAlgorithms(false, algorithms) - } + override fun setPreferredSymmetricKeyAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredSymmetricKeyAlgorithms(false, algorithms) } - override fun setPreferredSymmetricKeyAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { - setPreferredSymmetricKeyAlgorithms(PreferredAlgorithms( + override fun setPreferredSymmetricKeyAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredSymmetricKeyAlgorithms( + PreferredAlgorithms( SignatureSubpacketTags.PREFERRED_SYM_ALGS, isCritical, - algorithms.map { it.algorithmId }.toIntArray() - )) + algorithms.map { it.algorithmId }.toIntArray())) } - override fun setPreferredSymmetricKeyAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { - require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_SYM_ALGS) { - "Invalid preferred symmetric algorithms type." - } + override fun setPreferredSymmetricKeyAlgorithms( + algorithms: PreferredAlgorithms? + ): SignatureSubpackets = apply { + require( + algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_SYM_ALGS) { + "Invalid preferred symmetric algorithms type." + } this.preferredSymmetricKeyAlgorithmsSubpacket = algorithms } - override fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SignatureSubpackets = apply { - setPreferredHashAlgorithms(setOf(*algorithms)) - } + override fun setPreferredHashAlgorithms(vararg algorithms: HashAlgorithm): SignatureSubpackets = + apply { + setPreferredHashAlgorithms(setOf(*algorithms)) + } - override fun setPreferredHashAlgorithms(algorithms: Collection): SignatureSubpackets = apply { - setPreferredHashAlgorithms(false, algorithms) - } + override fun setPreferredHashAlgorithms( + algorithms: Collection + ): SignatureSubpackets = apply { setPreferredHashAlgorithms(false, algorithms) } - override fun setPreferredHashAlgorithms(isCritical: Boolean, algorithms: Collection): SignatureSubpackets = apply { - setPreferredHashAlgorithms(PreferredAlgorithms( + override fun setPreferredHashAlgorithms( + isCritical: Boolean, + algorithms: Collection + ): SignatureSubpackets = apply { + setPreferredHashAlgorithms( + PreferredAlgorithms( SignatureSubpacketTags.PREFERRED_HASH_ALGS, isCritical, - algorithms.map { it.algorithmId }.toIntArray() - )) + algorithms.map { it.algorithmId }.toIntArray())) } - override fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = apply { - require(algorithms == null || algorithms.type == SignatureSubpacketTags.PREFERRED_HASH_ALGS) { - "Invalid preferred hash algorithms type." + override fun setPreferredHashAlgorithms(algorithms: PreferredAlgorithms?): SignatureSubpackets = + apply { + require( + algorithms == null || + algorithms.type == SignatureSubpacketTags.PREFERRED_HASH_ALGS) { + "Invalid preferred hash algorithms type." + } + this.preferredHashAlgorithmsSubpacket = algorithms } - this.preferredHashAlgorithmsSubpacket = algorithms - } override fun addRevocationKey(revocationKey: PGPPublicKey): SignatureSubpackets = apply { addRevocationKey(true, revocationKey) } - override fun addRevocationKey(isCritical: Boolean, revocationKey: PGPPublicKey): SignatureSubpackets = apply { - addRevocationKey(isCritical, false, revocationKey) - } + override fun addRevocationKey( + isCritical: Boolean, + revocationKey: PGPPublicKey + ): SignatureSubpackets = apply { addRevocationKey(isCritical, false, revocationKey) } - override fun addRevocationKey(isCritical: Boolean, isSensitive: Boolean, revocationKey: PGPPublicKey): SignatureSubpackets = apply { + override fun addRevocationKey( + isCritical: Boolean, + isSensitive: Boolean, + revocationKey: PGPPublicKey + ): SignatureSubpackets = apply { val clazz = 0x80.toByte() or if (isSensitive) 0x40.toByte() else 0x00.toByte() - addRevocationKey(RevocationKey(isCritical, clazz, revocationKey.algorithm, revocationKey.fingerprint)) + addRevocationKey( + RevocationKey(isCritical, clazz, revocationKey.algorithm, revocationKey.fingerprint)) } override fun addRevocationKey(revocationKey: RevocationKey): SignatureSubpackets = apply { @@ -246,9 +298,10 @@ class SignatureSubpackets setFeatures(true, *features) } - override fun setFeatures(isCritical: Boolean, vararg features: Feature): SignatureSubpackets = apply { - setFeatures(Features(isCritical, Feature.toBitmask(*features))) - } + override fun setFeatures(isCritical: Boolean, vararg features: Feature): SignatureSubpackets = + apply { + setFeatures(Features(isCritical, Feature.toBitmask(*features))) + } override fun setFeatures(features: Features?): SignatureSubpackets = apply { this.featuresSubpacket = features @@ -271,7 +324,10 @@ class SignatureSubpackets this.issuerKeyIdSubpacket = issuerKeyID } - override fun setIssuerFingerprint(isCritical: Boolean, issuer: PGPPublicKey): SignatureSubpackets = apply { + override fun setIssuerFingerprint( + isCritical: Boolean, + issuer: PGPPublicKey + ): SignatureSubpackets = apply { setIssuerFingerprint(IssuerFingerprint(isCritical, issuer.version, issuer.fingerprint)) } @@ -279,44 +335,60 @@ class SignatureSubpackets setIssuerFingerprint(false, issuer) } - override fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): SignatureSubpackets = apply { - this.issuerFingerprintSubpacket = fingerprint - } + override fun setIssuerFingerprint(fingerprint: IssuerFingerprint?): SignatureSubpackets = + apply { + this.issuerFingerprintSubpacket = fingerprint + } override fun setSignatureCreationTime(creationTime: Date): SignatureSubpackets = apply { setSignatureCreationTime(true, creationTime) } - override fun setSignatureCreationTime(isCritical: Boolean, creationTime: Date): SignatureSubpackets = apply { + override fun setSignatureCreationTime( + isCritical: Boolean, + creationTime: Date + ): SignatureSubpackets = apply { setSignatureCreationTime(SignatureCreationTime(isCritical, creationTime)) } - override fun setSignatureCreationTime(creationTime: SignatureCreationTime?): SignatureSubpackets = apply { - this.signatureCreationTimeSubpacket = creationTime - } - override fun setSignatureExpirationTime(creationTime: Date, expirationTime: Date?): SignatureSubpackets = apply { + override fun setSignatureCreationTime( + creationTime: SignatureCreationTime? + ): SignatureSubpackets = apply { this.signatureCreationTimeSubpacket = creationTime } + + override fun setSignatureExpirationTime( + creationTime: Date, + expirationTime: Date? + ): SignatureSubpackets = apply { setSignatureExpirationTime(true, creationTime, expirationTime) } - override fun setSignatureExpirationTime(isCritical: Boolean, creationTime: Date, expirationTime: Date?): SignatureSubpackets = apply { + override fun setSignatureExpirationTime( + isCritical: Boolean, + creationTime: Date, + expirationTime: Date? + ): SignatureSubpackets = apply { if (expirationTime != null) { require(creationTime.toSecondsPrecision() < expirationTime.toSecondsPrecision()) { "Expiration time MUST NOT be less or equal the creation time." } - setSignatureExpirationTime(SignatureExpirationTime(isCritical, creationTime.secondsTill(expirationTime))) + setSignatureExpirationTime( + SignatureExpirationTime(isCritical, creationTime.secondsTill(expirationTime))) } else { setSignatureExpirationTime(SignatureExpirationTime(isCritical, 0)) } } - override fun setSignatureExpirationTime(isCritical: Boolean, seconds: Long): SignatureSubpackets = apply { + override fun setSignatureExpirationTime( + isCritical: Boolean, + seconds: Long + ): SignatureSubpackets = apply { enforceExpirationBounds(seconds) setSignatureExpirationTime(SignatureExpirationTime(isCritical, seconds)) } /** - * Enforce that
seconds
is within bounds of an unsigned 32bit number. - * Values less than 0 are illegal, as well as values greater 0xffffffff. + * Enforce that
seconds
is within bounds of an unsigned 32bit number. Values less + * than 0 are illegal, as well as values greater 0xffffffff. * * @param seconds number to check * @throws IllegalArgumentException in case of an under- or overflow @@ -325,32 +397,40 @@ class SignatureSubpackets require(seconds <= 0xffffffffL) { "Integer overflow. Seconds from creation to expiration (${seconds}) cannot be larger than ${0xffffffffL}." } - require(seconds >= 0) { - "Seconds from creation to expiration cannot be less than 0." - } + require(seconds >= 0) { "Seconds from creation to expiration cannot be less than 0." } } - override fun setSignatureExpirationTime(expirationTime: SignatureExpirationTime?): SignatureSubpackets = apply { - this.signatureExpirationTimeSubpacket = expirationTime - } + override fun setSignatureExpirationTime( + expirationTime: SignatureExpirationTime? + ): SignatureSubpackets = apply { this.signatureExpirationTimeSubpacket = expirationTime } override fun setSignerUserId(userId: CharSequence): SignatureSubpackets = apply { setSignerUserId(false, userId) } - override fun setSignerUserId(isCritical: Boolean, userId: CharSequence): SignatureSubpackets = apply { - setSignerUserId(SignerUserID(isCritical, userId.toString())) - } + override fun setSignerUserId(isCritical: Boolean, userId: CharSequence): SignatureSubpackets = + apply { + setSignerUserId(SignerUserID(isCritical, userId.toString())) + } override fun setSignerUserId(signerUserID: SignerUserID?): SignatureSubpackets = apply { this.signerUserIdSubpacket = signerUserID } - override fun addNotationData(isCritical: Boolean, notationName: String, notationValue: String): SignatureSubpackets = apply { + override fun addNotationData( + isCritical: Boolean, + notationName: String, + notationValue: String + ): SignatureSubpackets = apply { addNotationData(isCritical, true, notationName, notationValue) } - override fun addNotationData(isCritical: Boolean, isHumanReadable: Boolean, notationName: String, notationValue: String): SignatureSubpackets = apply { + override fun addNotationData( + isCritical: Boolean, + isHumanReadable: Boolean, + notationName: String, + notationValue: String + ): SignatureSubpackets = apply { addNotationData(NotationData(isCritical, isHumanReadable, notationName, notationValue)) } @@ -362,15 +442,23 @@ class SignatureSubpackets (this.notationDataSubpackets as MutableList).clear() } - override fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): SignatureSubpackets = apply { - addIntendedRecipientFingerprint(false, recipientKey) + override fun addIntendedRecipientFingerprint(recipientKey: PGPPublicKey): SignatureSubpackets = + apply { + addIntendedRecipientFingerprint(false, recipientKey) + } + + override fun addIntendedRecipientFingerprint( + isCritical: Boolean, + recipientKey: PGPPublicKey + ): SignatureSubpackets = apply { + addIntendedRecipientFingerprint( + IntendedRecipientFingerprint( + isCritical, recipientKey.version, recipientKey.fingerprint)) } - override fun addIntendedRecipientFingerprint(isCritical: Boolean, recipientKey: PGPPublicKey): SignatureSubpackets = apply { - addIntendedRecipientFingerprint(IntendedRecipientFingerprint(isCritical, recipientKey.version, recipientKey.fingerprint)) - } - - override fun addIntendedRecipientFingerprint(intendedRecipient: IntendedRecipientFingerprint): SignatureSubpackets = apply { + override fun addIntendedRecipientFingerprint( + intendedRecipient: IntendedRecipientFingerprint + ): SignatureSubpackets = apply { (this.intendedRecipientFingerprintSubpackets as MutableList).add(intendedRecipient) } @@ -378,17 +466,16 @@ class SignatureSubpackets (this.intendedRecipientFingerprintSubpackets as MutableList).clear() } - override fun setExportable(): SignatureSubpackets = apply { - setExportable(true) - } + override fun setExportable(): SignatureSubpackets = apply { setExportable(true) } override fun setExportable(isExportable: Boolean): SignatureSubpackets = apply { setExportable(true, isExportable) } - override fun setExportable(isCritical: Boolean, isExportable: Boolean): SignatureSubpackets = apply { - setExportable(Exportable(isCritical, isExportable)) - } + override fun setExportable(isCritical: Boolean, isExportable: Boolean): SignatureSubpackets = + apply { + setExportable(Exportable(isCritical, isExportable)) + } override fun setExportable(exportable: Exportable?): SignatureSubpackets = apply { this.exportableSubpacket = exportable @@ -410,7 +497,10 @@ class SignatureSubpackets setRegularExpression(false, regex) } - override fun setRegularExpression(isCritical: Boolean, regex: CharSequence): SignatureSubpackets = apply { + override fun setRegularExpression( + isCritical: Boolean, + regex: CharSequence + ): SignatureSubpackets = apply { setRegularExpression(RegularExpression(isCritical, regex.toString())) } @@ -418,41 +508,53 @@ class SignatureSubpackets this.regularExpressionSubpacket = regex } - override fun setRevocable(): SignatureSubpackets = apply { - setRevocable(true) - } + override fun setRevocable(): SignatureSubpackets = apply { setRevocable(true) } override fun setRevocable(isRevocable: Boolean): SignatureSubpackets = apply { setRevocable(true, isRevocable) } - override fun setRevocable(isCritical: Boolean, isRevocable: Boolean): SignatureSubpackets = apply { - setRevocable(Revocable(isCritical, isRevocable)) - } + override fun setRevocable(isCritical: Boolean, isRevocable: Boolean): SignatureSubpackets = + apply { + setRevocable(Revocable(isCritical, isRevocable)) + } override fun setRevocable(revocable: Revocable?): SignatureSubpackets = apply { this.revocableSubpacket = revocable } - override fun setSignatureTarget(keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): SignatureSubpackets = apply { + override fun setSignatureTarget( + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): SignatureSubpackets = apply { setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData) } - override fun setSignatureTarget(isCritical: Boolean, keyAlgorithm: PublicKeyAlgorithm, hashAlgorithm: HashAlgorithm, hashData: ByteArray): SignatureSubpackets = apply { - setSignatureTarget(SignatureTarget(isCritical, keyAlgorithm.algorithmId, hashAlgorithm.algorithmId, hashData)) + override fun setSignatureTarget( + isCritical: Boolean, + keyAlgorithm: PublicKeyAlgorithm, + hashAlgorithm: HashAlgorithm, + hashData: ByteArray + ): SignatureSubpackets = apply { + setSignatureTarget( + SignatureTarget( + isCritical, keyAlgorithm.algorithmId, hashAlgorithm.algorithmId, hashData)) } - override fun setSignatureTarget(signatureTarget: SignatureTarget?): SignatureSubpackets = apply { - this.signatureTargetSubpacket = signatureTarget - } + override fun setSignatureTarget(signatureTarget: SignatureTarget?): SignatureSubpackets = + apply { + this.signatureTargetSubpacket = signatureTarget + } override fun setTrust(depth: Int, amount: Int): SignatureSubpackets = apply { setTrust(true, depth, amount) } - override fun setTrust(isCritical: Boolean, depth: Int, amount: Int): SignatureSubpackets = apply { - setTrust(TrustSignature(isCritical, depth, amount)) - } + override fun setTrust(isCritical: Boolean, depth: Int, amount: Int): SignatureSubpackets = + apply { + setTrust(TrustSignature(isCritical, depth, amount)) + } override fun setTrust(trust: TrustSignature?): SignatureSubpackets = apply { this.trustSubpacket = trust @@ -462,26 +564,31 @@ class SignatureSubpackets addEmbeddedSignature(true, signature) } - override fun addEmbeddedSignature(isCritical: Boolean, signature: PGPSignature): SignatureSubpackets = apply { + override fun addEmbeddedSignature( + isCritical: Boolean, + signature: PGPSignature + ): SignatureSubpackets = apply { val sig = signature.encoded - val data = if (sig.size - 1 > 256) { - ByteArray(sig.size - 3) - } else { - ByteArray(sig.size - 2) - } + val data = + if (sig.size - 1 > 256) { + ByteArray(sig.size - 3) + } else { + ByteArray(sig.size - 2) + } System.arraycopy(sig, sig.size - data.size, data, 0, data.size) addEmbeddedSignature(EmbeddedSignature(isCritical, false, data)) } - override fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): SignatureSubpackets = apply { - (this.embeddedSignatureSubpackets as MutableList).add(embeddedSignature) - } + override fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): SignatureSubpackets = + apply { + (this.embeddedSignatureSubpackets as MutableList).add(embeddedSignature) + } override fun clearEmbeddedSignatures(): SignatureSubpackets = apply { (this.embeddedSignatureSubpackets as MutableList).clear() } - fun addResidualSubpacket(subpacket: org.bouncycastle.bcpg.SignatureSubpacket): SignatureSubpackets = apply { - (residualSubpackets as MutableList).add(subpacket) - } -} \ No newline at end of file + fun addResidualSubpacket( + subpacket: org.bouncycastle.bcpg.SignatureSubpacket + ): SignatureSubpackets = apply { (residualSubpackets as MutableList).add(subpacket) } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt index 6767b0af..6c39432e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt @@ -14,87 +14,135 @@ class SignatureSubpacketsHelper { companion object { @JvmStatic - fun applyFrom(vector: PGPSignatureSubpacketVector, subpackets: SignatureSubpackets) = subpackets.apply { - for (subpacket in vector.toArray()) { - val type = SignatureSubpacket.requireFromCode(subpacket.type) - when (type) { - SignatureSubpacket.signatureCreationTime, - SignatureSubpacket.issuerKeyId, - SignatureSubpacket.issuerFingerprint -> { /* ignore, we override this anyway */ } - SignatureSubpacket.signatureExpirationTime -> (subpacket as SignatureExpirationTime).let { - subpackets.setSignatureExpirationTime(it.isCritical, it.time) - } - SignatureSubpacket.exportableCertification -> (subpacket as Exportable).let { - subpackets.setExportable(it.isCritical, it.isExportable) - } - SignatureSubpacket.trustSignature -> (subpacket as TrustSignature).let { - subpackets.setTrust(it.isCritical, it.depth, it.trustAmount) - } - SignatureSubpacket.revocable -> (subpacket as Revocable).let { - subpackets.setRevocable(it.isCritical, it.isRevocable) - } - SignatureSubpacket.keyExpirationTime -> (subpacket as KeyExpirationTime).let { - subpackets.setKeyExpirationTime(it.isCritical, it.time) - } - SignatureSubpacket.preferredSymmetricAlgorithms -> (subpacket as PreferredAlgorithms).let { - subpackets.setPreferredSymmetricKeyAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) - } - SignatureSubpacket.preferredHashAlgorithms -> (subpacket as PreferredAlgorithms).let { - subpackets.setPreferredHashAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) - } - SignatureSubpacket.preferredCompressionAlgorithms -> (subpacket as PreferredAlgorithms).let { - subpackets.setPreferredCompressionAlgorithms(PreferredAlgorithms(it.type, it.isCritical, it.isLongLength, it.data)) - } - SignatureSubpacket.revocationKey -> (subpacket as RevocationKey).let { - subpackets.addRevocationKey(RevocationKey(it.isCritical, it.signatureClass, it.algorithm, it.fingerprint)) - } - SignatureSubpacket.notationData -> (subpacket as NotationData).let { - subpackets.addNotationData(it.isCritical, it.isHumanReadable, it.notationName, it.notationValue) - } - SignatureSubpacket.primaryUserId -> (subpacket as PrimaryUserID).let { - subpackets.setPrimaryUserId(PrimaryUserID(it.isCritical, it.isPrimaryUserID)) - } - SignatureSubpacket.keyFlags -> (subpacket as KeyFlags).let { - subpackets.setKeyFlags(it.isCritical, *(KeyFlag.fromBitmask(it.flags).toTypedArray())) - } - SignatureSubpacket.signerUserId -> (subpacket as SignerUserID).let { - subpackets.setSignerUserId(it.isCritical, it.id) - } - SignatureSubpacket.revocationReason -> (subpacket as RevocationReason).let { - subpackets.setRevocationReason(it.isCritical, RevocationAttributes.Reason.fromCode(it.revocationReason), it.revocationDescription) - } - SignatureSubpacket.features -> (subpacket as Features).let { - subpackets.setFeatures(it.isCritical, *(Feature.fromBitmask(it.features.toInt()).toTypedArray())) - } - SignatureSubpacket.signatureTarget -> (subpacket as SignatureTarget).let { - subpackets.setSignatureTarget(it.isCritical, - PublicKeyAlgorithm.requireFromId(it.publicKeyAlgorithm), - HashAlgorithm.requireFromId(it.hashAlgorithm), - it.hashData) - } - SignatureSubpacket.embeddedSignature -> (subpacket as EmbeddedSignature).let { - subpackets.addEmbeddedSignature(it) - } - SignatureSubpacket.intendedRecipientFingerprint -> (subpacket as IntendedRecipientFingerprint).let { - subpackets.addIntendedRecipientFingerprint(it) - } - SignatureSubpacket.policyUrl -> (subpacket as PolicyURI).let { - subpackets.setPolicyUrl(it) - } - SignatureSubpacket.regularExpression -> (subpacket as RegularExpression).let { - subpackets.setRegularExpression(it) - } - SignatureSubpacket.keyServerPreferences, + fun applyFrom(vector: PGPSignatureSubpacketVector, subpackets: SignatureSubpackets) = + subpackets.apply { + for (subpacket in vector.toArray()) { + val type = SignatureSubpacket.requireFromCode(subpacket.type) + when (type) { + SignatureSubpacket.signatureCreationTime, + SignatureSubpacket.issuerKeyId, + SignatureSubpacket.issuerFingerprint -> { + /* ignore, we override this anyway */ + } + SignatureSubpacket.signatureExpirationTime -> + (subpacket as SignatureExpirationTime).let { + subpackets.setSignatureExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.exportableCertification -> + (subpacket as Exportable).let { + subpackets.setExportable(it.isCritical, it.isExportable) + } + SignatureSubpacket.trustSignature -> + (subpacket as TrustSignature).let { + subpackets.setTrust(it.isCritical, it.depth, it.trustAmount) + } + SignatureSubpacket.revocable -> + (subpacket as Revocable).let { + subpackets.setRevocable(it.isCritical, it.isRevocable) + } + SignatureSubpacket.keyExpirationTime -> + (subpacket as KeyExpirationTime).let { + subpackets.setKeyExpirationTime(it.isCritical, it.time) + } + SignatureSubpacket.preferredSymmetricAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredSymmetricKeyAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredHashAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredHashAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.preferredCompressionAlgorithms -> + (subpacket as PreferredAlgorithms).let { + subpackets.setPreferredCompressionAlgorithms( + PreferredAlgorithms( + it.type, it.isCritical, it.isLongLength, it.data)) + } + SignatureSubpacket.revocationKey -> + (subpacket as RevocationKey).let { + subpackets.addRevocationKey( + RevocationKey( + it.isCritical, + it.signatureClass, + it.algorithm, + it.fingerprint)) + } + SignatureSubpacket.notationData -> + (subpacket as NotationData).let { + subpackets.addNotationData( + it.isCritical, + it.isHumanReadable, + it.notationName, + it.notationValue) + } + SignatureSubpacket.primaryUserId -> + (subpacket as PrimaryUserID).let { + subpackets.setPrimaryUserId( + PrimaryUserID(it.isCritical, it.isPrimaryUserID)) + } + SignatureSubpacket.keyFlags -> + (subpacket as KeyFlags).let { + subpackets.setKeyFlags( + it.isCritical, *(KeyFlag.fromBitmask(it.flags).toTypedArray())) + } + SignatureSubpacket.signerUserId -> + (subpacket as SignerUserID).let { + subpackets.setSignerUserId(it.isCritical, it.id) + } + SignatureSubpacket.revocationReason -> + (subpacket as RevocationReason).let { + subpackets.setRevocationReason( + it.isCritical, + RevocationAttributes.Reason.fromCode(it.revocationReason), + it.revocationDescription) + } + SignatureSubpacket.features -> + (subpacket as Features).let { + subpackets.setFeatures( + it.isCritical, + *(Feature.fromBitmask(it.features.toInt()).toTypedArray())) + } + SignatureSubpacket.signatureTarget -> + (subpacket as SignatureTarget).let { + subpackets.setSignatureTarget( + it.isCritical, + PublicKeyAlgorithm.requireFromId(it.publicKeyAlgorithm), + HashAlgorithm.requireFromId(it.hashAlgorithm), + it.hashData) + } + SignatureSubpacket.embeddedSignature -> + (subpacket as EmbeddedSignature).let { + subpackets.addEmbeddedSignature(it) + } + SignatureSubpacket.intendedRecipientFingerprint -> + (subpacket as IntendedRecipientFingerprint).let { + subpackets.addIntendedRecipientFingerprint(it) + } + SignatureSubpacket.policyUrl -> + (subpacket as PolicyURI).let { subpackets.setPolicyUrl(it) } + SignatureSubpacket.regularExpression -> + (subpacket as RegularExpression).let { + subpackets.setRegularExpression(it) + } + SignatureSubpacket.keyServerPreferences, SignatureSubpacket.preferredKeyServers, SignatureSubpacket.placeholder, SignatureSubpacket.preferredAEADAlgorithms, - SignatureSubpacket.attestedCertification -> subpackets.addResidualSubpacket(subpacket) + SignatureSubpacket.attestedCertification -> + subpackets.addResidualSubpacket(subpacket) + } } } - } @JvmStatic - fun applyTo(subpackets: SignatureSubpackets, generator: PGPSignatureSubpacketGenerator): PGPSignatureSubpacketGenerator { + fun applyTo( + subpackets: SignatureSubpackets, + generator: PGPSignatureSubpacketGenerator + ): PGPSignatureSubpacketGenerator { return generator.apply { addSubpacket(subpackets.issuerKeyIdSubpacket) addSubpacket(subpackets.issuerFingerprintSubpacket) @@ -134,7 +182,9 @@ class SignatureSubpacketsHelper { } @JvmStatic - private fun PGPSignatureSubpacketGenerator.addSubpacket(subpacket: org.bouncycastle.bcpg.SignatureSubpacket?) { + private fun PGPSignatureSubpacketGenerator.addSubpacket( + subpacket: org.bouncycastle.bcpg.SignatureSubpacket? + ) { if (subpacket != null) { this.addCustomSubpacket(subpacket) } @@ -156,4 +206,4 @@ class SignatureSubpacketsHelper { } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index 2cf79d72..31e1f53c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -4,6 +4,7 @@ package org.pgpainless.signature.subpackets +import java.util.* import openpgp.openPgpKeyId import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.* @@ -19,27 +20,26 @@ import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.OpenPgpV5Fingerprint import org.pgpainless.key.OpenPgpV6Fingerprint import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.signature.SignatureUtils -import java.util.* class SignatureSubpacketsUtil { companion object { /** - * Return the issuer-fingerprint subpacket of the signature. - * Since this packet is self-authenticating, we expect it to be in the unhashed area, - * however as it cannot hurt we search for it in the hashed area first. + * Return the issuer-fingerprint subpacket of the signature. Since this packet is + * self-authenticating, we expect it to be in the unhashed area, however as it cannot hurt + * we search for it in the hashed area first. * * @param signature signature * @return issuer fingerprint or null */ @JvmStatic fun getIssuerFingerprint(signature: PGPSignature): IssuerFingerprint? = - hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint) + hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint) /** - * Return the [IssuerFingerprint] subpacket of the signature into a [org.pgpainless.key.OpenPgpFingerprint]. - * If no v4, v5 or v6 issuer fingerprint is present in the signature, return null. + * Return the [IssuerFingerprint] subpacket of the signature into a + * [org.pgpainless.key.OpenPgpFingerprint]. If no v4, v5 or v6 issuer fingerprint is present + * in the signature, return null. * * @param signature signature * @return fingerprint of the issuer, or null @@ -47,7 +47,7 @@ class SignatureSubpacketsUtil { @JvmStatic fun getIssuerFingerprintAsOpenPgpFingerprint(signature: PGPSignature): OpenPgpFingerprint? { val subpacket = getIssuerFingerprint(signature) ?: return null - return when(subpacket.keyVersion) { + return when (subpacket.keyVersion) { 4 -> OpenPgpV4Fingerprint(subpacket.fingerprint) 5 -> OpenPgpV5Fingerprint(subpacket.fingerprint) 6 -> OpenPgpV6Fingerprint(subpacket.fingerprint) @@ -57,85 +57,83 @@ class SignatureSubpacketsUtil { @JvmStatic fun getIssuerKeyId(signature: PGPSignature): IssuerKeyID? = - hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId) + hashedOrUnhashed(signature, SignatureSubpacket.issuerKeyId) /** - * Inspect the given signature's [IssuerKeyID] packet to determine the issuer key-id. - * If no such packet is present, return null. + * Inspect the given signature's [IssuerKeyID] packet to determine the issuer key-id. If no + * such packet is present, return null. * * @param signature signature * @return issuer key-id as {@link Long} */ @JvmStatic - fun getIssuerKeyIdAsLong(signature: PGPSignature): Long? = - getIssuerKeyId(signature)?.keyID + fun getIssuerKeyIdAsLong(signature: PGPSignature): Long? = getIssuerKeyId(signature)?.keyID /** - * Return the revocation reason subpacket of the signature. - * Since this packet is rather important for revocations, we only search for it in the - * hashed area of the signature. + * Return the revocation reason subpacket of the signature. Since this packet is rather + * important for revocations, we only search for it in the hashed area of the signature. * * @param signature signature * @return revocation reason */ @JvmStatic fun getRevocationReason(signature: PGPSignature): RevocationReason? = - hashed(signature, SignatureSubpacket.revocationReason) + hashed(signature, SignatureSubpacket.revocationReason) /** - * Return the signature creation time subpacket. - * Since this packet is rather important, we only search for it in the hashed area - * of the signature. + * Return the signature creation time subpacket. Since this packet is rather important, we + * only search for it in the hashed area of the signature. * * @param signature signature * @return signature creation time subpacket */ @JvmStatic fun getSignatureCreationTime(signature: PGPSignature): SignatureCreationTime? = - if (signature.version == 3) SignatureCreationTime(false, signature.creationTime) - else hashed(signature, SignatureSubpacket.signatureCreationTime) + if (signature.version == 3) SignatureCreationTime(false, signature.creationTime) + else hashed(signature, SignatureSubpacket.signatureCreationTime) /** - * Return the signature expiration time subpacket of the signature. - * Since this packet is rather important, we only search for it in the hashed area of the signature. + * Return the signature expiration time subpacket of the signature. Since this packet is + * rather important, we only search for it in the hashed area of the signature. * * @param signature signature * @return signature expiration time */ @JvmStatic fun getSignatureExpirationTime(signature: PGPSignature): SignatureExpirationTime? = - hashed(signature, SignatureSubpacket.signatureExpirationTime) + hashed(signature, SignatureSubpacket.signatureExpirationTime) /** - * Return the signatures' expiration time as a date. - * The expiration date is computed by adding the expiration time to the signature creation date. - * If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null. + * Return the signatures' expiration time as a date. The expiration date is computed by + * adding the expiration time to the signature creation date. If the signature has no + * expiration time subpacket, or the expiration time is set to '0', this message returns + * null. * * @param signature signature * @return expiration time as date */ @JvmStatic fun getSignatureExpirationTimeAsDate(signature: PGPSignature): Date? = - getSignatureExpirationTime(signature)?.let { - signature.creationTime.plusSeconds(it.time) - } + getSignatureExpirationTime(signature)?.let { + signature.creationTime.plusSeconds(it.time) + } /** - * Return the key expiration time subpacket of this signature. - * We only look for it in the hashed area of the signature. + * Return the key expiration time subpacket of this signature. We only look for it in the + * hashed area of the signature. * * @param signature signature * @return key expiration time */ @JvmStatic fun getKeyExpirationTime(signature: PGPSignature): KeyExpirationTime? = - hashed(signature, SignatureSubpacket.keyExpirationTime) + hashed(signature, SignatureSubpacket.keyExpirationTime) /** - * Return the signatures key-expiration time as a date. - * The expiration date is computed by adding the signatures' key-expiration time to the signing keys - * creation date. - * If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null. + * Return the signatures key-expiration time as a date. The expiration date is computed by + * adding the signatures' key-expiration time to the signing keys creation date. If the + * signature does not have a key-expiration time subpacket, or its value is '0', this method + * returns null. * * @param signature self-signature carrying the key-expiration time subpacket * @param signingKey signature creation key @@ -143,9 +141,10 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getKeyExpirationTimeAsDate(signature: PGPSignature, signingKey: PGPPublicKey): Date? = - require(signature.keyID == signingKey.keyID) { + require(signature.keyID == signingKey.keyID) { "Provided key (${signingKey.keyID.openPgpKeyId()}) did not create the signature (${signature.keyID.openPgpKeyId()})" - }.run { + } + .run { getKeyExpirationTime(signature)?.let { signingKey.creationTime.plusSeconds(it.time) } @@ -160,25 +159,25 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getKeyLifetimeInSeconds(creationTime: Date, expirationTime: Date?): Long = - expirationTime?.let { - require(creationTime <= it) { + expirationTime?.let { + require(creationTime <= it) { "Key MUST NOT expire before being created.\n" + - "(creation: $creationTime, expiration: $it)" - }.run { - (it.time - creationTime.time) / 1000 + "(creation: $creationTime, expiration: $it)" } - } ?: 0 // 0 means "no expiration" + .run { (it.time - creationTime.time) / 1000 } + } + ?: 0 // 0 means "no expiration" /** - * Return the revocable subpacket of this signature. - * We only look for it in the hashed area of the signature. + * Return the revocable subpacket of this signature. We only look for it in the hashed area + * of the signature. * * @param signature signature * @return revocable subpacket */ @JvmStatic fun getRevocable(signature: PGPSignature): Revocable? = - hashed(signature, SignatureSubpacket.revocable) + hashed(signature, SignatureSubpacket.revocable) /** * Return the symmetric algorithm preferences from the signatures hashed area. @@ -188,23 +187,28 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getPreferredSymmetricAlgorithms(signature: PGPSignature): PreferredAlgorithms? = - hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms) + hashed(signature, SignatureSubpacket.preferredSymmetricAlgorithms) /** - * Return the preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] as present in the signature. - * If no preference is given with regard to symmetric encryption algorithms, return an empty set. + * Return the preferred [SymmetricKeyAlgorithms][SymmetricKeyAlgorithm] as present in the + * signature. If no preference is given with regard to symmetric encryption algorithms, + * return an empty set. * * In any case, the resulting set is ordered by occurrence. + * * @param signature signature * @return ordered set of symmetric key algorithm preferences */ @JvmStatic - fun parsePreferredSymmetricKeyAlgorithms(signature: PGPSignature): Set = - getPreferredSymmetricAlgorithms(signature) - ?.preferences - ?.map { SymmetricKeyAlgorithm.fromId(it) } - ?.filterNotNull() - ?.toSet() ?: setOf() + fun parsePreferredSymmetricKeyAlgorithms( + signature: PGPSignature + ): Set = + getPreferredSymmetricAlgorithms(signature) + ?.preferences + ?.map { SymmetricKeyAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() /** * Return the hash algorithm preferences from the signatures hashed area. @@ -214,23 +218,25 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getPreferredHashAlgorithms(signature: PGPSignature): PreferredAlgorithms? = - hashed(signature, SignatureSubpacket.preferredHashAlgorithms) + hashed(signature, SignatureSubpacket.preferredHashAlgorithms) /** - * Return the preferred [HashAlgorithms][HashAlgorithm] as present in the signature. - * If no preference is given with regard to hash algorithms, return an empty set. + * Return the preferred [HashAlgorithms][HashAlgorithm] as present in the signature. If no + * preference is given with regard to hash algorithms, return an empty set. * * In any case, the resulting set is ordered by occurrence. + * * @param signature signature * @return ordered set of hash algorithm preferences */ @JvmStatic fun parsePreferredHashAlgorithms(signature: PGPSignature): Set = - getPreferredHashAlgorithms(signature) - ?.preferences - ?.map { HashAlgorithm.fromId(it) } - ?.filterNotNull() - ?.toSet() ?: setOf() + getPreferredHashAlgorithms(signature) + ?.preferences + ?.map { HashAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() /** * Return the compression algorithm preferences from the signatures hashed area. @@ -240,27 +246,32 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getPreferredCompressionAlgorithms(signature: PGPSignature): PreferredAlgorithms? = - hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms) + hashed(signature, SignatureSubpacket.preferredCompressionAlgorithms) /** - * Return the preferred [CompressionAlgorithms][CompressionAlgorithm] as present in the signature. - * If no preference is given with regard to compression algorithms, return an empty set. + * Return the preferred [CompressionAlgorithms][CompressionAlgorithm] as present in the + * signature. If no preference is given with regard to compression algorithms, return an + * empty set. * * In any case, the resulting set is ordered by occurrence. + * * @param signature signature * @return ordered set of compression algorithm preferences */ @JvmStatic - fun parsePreferredCompressionAlgorithms(signature: PGPSignature): Set = - getPreferredCompressionAlgorithms(signature) - ?.preferences - ?.map { CompressionAlgorithm.fromId(it) } - ?.filterNotNull() - ?.toSet() ?: setOf() + fun parsePreferredCompressionAlgorithms( + signature: PGPSignature + ): Set = + getPreferredCompressionAlgorithms(signature) + ?.preferences + ?.map { CompressionAlgorithm.fromId(it) } + ?.filterNotNull() + ?.toSet() + ?: setOf() @JvmStatic fun getPreferredAeadAlgorithms(signature: PGPSignature): PreferredAEADCiphersuites? = - hashed(signature, SignatureSubpacket.preferredAEADAlgorithms) + hashed(signature, SignatureSubpacket.preferredAEADAlgorithms) /** * Return the primary user-id subpacket from the signatures hashed area. @@ -270,7 +281,7 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getPrimaryUserId(signature: PGPSignature): PrimaryUserID? = - hashed(signature, SignatureSubpacket.primaryUserId) + hashed(signature, SignatureSubpacket.primaryUserId) /** * Return the key flags subpacket from the signatures hashed area. @@ -280,22 +291,18 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getKeyFlags(signature: PGPSignature): KeyFlags? = - hashed(signature, SignatureSubpacket.keyFlags) + hashed(signature, SignatureSubpacket.keyFlags) /** - * Return a list of key flags carried by the signature. - * If the signature is null, or has no [KeyFlags] subpacket, return null. + * Return a list of key flags carried by the signature. If the signature is null, or has no + * [KeyFlags] subpacket, return null. * * @param signature signature * @return list of key flags */ @JvmStatic fun parseKeyFlags(signature: PGPSignature?): List? = - signature?.let { sig -> - getKeyFlags(sig)?.let { - KeyFlag.fromBitmask(it.flags) - } - } + signature?.let { sig -> getKeyFlags(sig)?.let { KeyFlag.fromBitmask(it.flags) } } /** * Return the features subpacket from the signatures hashed area. @@ -305,32 +312,29 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getFeatures(signature: PGPSignature): Features? = - hashed(signature, SignatureSubpacket.features) + hashed(signature, SignatureSubpacket.features) /** - * Parse out the features subpacket of a signature. - * If the signature has no features subpacket, return null. - * Otherwise, return the features as a feature set. + * Parse out the features subpacket of a signature. If the signature has no features + * subpacket, return null. Otherwise, return the features as a feature set. * * @param signature signature * @return features as set */ @JvmStatic fun parseFeatures(signature: PGPSignature): Set? = - getFeatures(signature)?.let { - Feature.fromBitmask(it.features.toInt()).toSet() - } + getFeatures(signature)?.let { Feature.fromBitmask(it.features.toInt()).toSet() } /** - * Return the signature target subpacket from the signature. - * We search for this subpacket in the hashed and unhashed area (in this order). + * Return the signature target subpacket from the signature. We search for this subpacket in + * the hashed and unhashed area (in this order). * * @param signature signature * @return signature target */ @JvmStatic fun getSignatureTarget(signature: PGPSignature): SignatureTarget? = - hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget) + hashedOrUnhashed(signature, SignatureSubpacket.signatureTarget) /** * Return the notation data subpackets from the signatures hashed area. @@ -340,20 +344,22 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getHashedNotationData(signature: PGPSignature): List = - signature.hashedSubPackets.notationDataOccurrences.toList() + signature.hashedSubPackets.notationDataOccurrences.toList() /** - * Return a list of all [NotationData] objects from the hashed area of the signature that have a - * notation name equal to the given notationName argument. + * Return a list of all [NotationData] objects from the hashed area of the signature that + * have a notation name equal to the given notationName argument. * * @param signature signature * @param notationName notation name * @return list of matching notation data objects */ @JvmStatic - fun getHashedNotationData(signature: PGPSignature, notationName: String): List = - getHashedNotationData(signature) - .filter { it.notationName == notationName } + fun getHashedNotationData( + signature: PGPSignature, + notationName: String + ): List = + getHashedNotationData(signature).filter { it.notationName == notationName } /** * Return the notation data subpackets from the signatures unhashed area. @@ -363,11 +369,11 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getUnhashedNotationData(signature: PGPSignature): List = - signature.unhashedSubPackets.notationDataOccurrences.toList() + signature.unhashedSubPackets.notationDataOccurrences.toList() /** - * Return a list of all [NotationData] objects from the unhashed area of the signature that have a - * notation name equal to the given notationName argument. + * Return a list of all [NotationData] objects from the unhashed area of the signature that + * have a notation name equal to the given notationName argument. * * @param signature signature * @param notationName notation name @@ -375,8 +381,7 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getUnhashedNotationData(signature: PGPSignature, notationName: String) = - getUnhashedNotationData(signature) - .filter { it.notationName == notationName } + getUnhashedNotationData(signature).filter { it.notationName == notationName } /** * Return the revocation key subpacket from the signatures hashed area. @@ -386,28 +391,32 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getRevocationKey(signature: PGPSignature): RevocationKey? = - hashed(signature, SignatureSubpacket.revocationKey) + hashed(signature, SignatureSubpacket.revocationKey) /** * Return the signers user-id from the hashed area of the signature. - * TODO: Can this subpacket also be found in the unhashed area? * * @param signature signature * @return signers user-id + * + * TODO: Can this subpacket also be found in the unhashed area? */ @JvmStatic fun getSignerUserID(signature: PGPSignature): SignerUserID? = - hashed(signature, SignatureSubpacket.signerUserId) + hashed(signature, SignatureSubpacket.signerUserId) /** - * Return the intended recipients fingerprint subpackets from the hashed area of this signature. + * Return the intended recipients fingerprint subpackets from the hashed area of this + * signature. * * @param signature signature * @return intended recipient fingerprint subpackets */ @JvmStatic - fun getIntendedRecipientFingerprints(signature: PGPSignature): List = - signature.hashedSubPackets.intendedRecipientFingerprints.toList() + fun getIntendedRecipientFingerprints( + signature: PGPSignature + ): List = + signature.hashedSubPackets.intendedRecipientFingerprints.toList() /** * Return the embedded signature subpacket from the signatures hashed area or unhashed area. @@ -417,10 +426,9 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getEmbeddedSignature(signature: PGPSignature): PGPSignatureList = - signature.hashedSubPackets.embeddedSignatures.let { - if (it.isEmpty) signature.unhashedSubPackets.embeddedSignatures - else it - } + signature.hashedSubPackets.embeddedSignatures.let { + if (it.isEmpty) signature.unhashedSubPackets.embeddedSignatures else it + } /** * Return the signatures exportable certification subpacket from the hashed area. @@ -430,14 +438,12 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getExportableCertification(signature: PGPSignature): Exportable? = - hashed(signature, SignatureSubpacket.exportableCertification) + hashed(signature, SignatureSubpacket.exportableCertification) - /** - * Return true, if the signature is not explicitly marked as non-exportable. - */ + /** Return true, if the signature is not explicitly marked as non-exportable. */ @JvmStatic fun isExportable(signature: PGPSignature): Boolean = - getExportableCertification(signature)?.isExportable ?: true + getExportableCertification(signature)?.isExportable ?: true /** * Return the trust signature packet from the signatures hashed area. @@ -447,11 +453,11 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getTrustSignature(signature: PGPSignature): TrustSignature? = - hashed(signature, SignatureSubpacket.trustSignature) + hashed(signature, SignatureSubpacket.trustSignature) /** - * Return the trust depth set in the signatures [TrustSignature] packet, or [defaultDepth] if no such packet - * is found. + * Return the trust depth set in the signatures [TrustSignature] packet, or [defaultDepth] + * if no such packet is found. * * @param signature signature * @param defaultDepth default value that is returned if no trust signature packet is found @@ -459,11 +465,11 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getTrustDepthOr(signature: PGPSignature, defaultDepth: Int): Int = - getTrustSignature(signature)?.depth ?: defaultDepth + getTrustSignature(signature)?.depth ?: defaultDepth /** - * Return the trust amount set in the signatures [TrustSignature] packet, or [defaultAmount] if no such packet - * is found. + * Return the trust amount set in the signatures [TrustSignature] packet, or [defaultAmount] + * if no such packet is found. * * @param signature signature * @param defaultAmount default value that is returned if no trust signature packet is found @@ -471,7 +477,7 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getTrustAmountOr(signature: PGPSignature, defaultAmount: Int): Int = - getTrustSignature(signature)?.trustAmount ?: defaultAmount + getTrustSignature(signature)?.trustAmount ?: defaultAmount /** * Return all regular expression subpackets from the hashed area of the given signature. @@ -481,11 +487,11 @@ class SignatureSubpacketsUtil { */ @JvmStatic fun getRegularExpressions(signature: PGPSignature): List = - signature.hashedSubPackets.regularExpressions.toList() + signature.hashedSubPackets.regularExpressions.toList() /** - * Select a list of all signature subpackets of the given type, which are present in either the hashed - * or the unhashed area of the given signature. + * Select a list of all signature subpackets of the given type, which are present in either + * the hashed or the unhashed area of the given signature. * * @param signature signature * @param type subpacket type @@ -493,13 +499,16 @@ class SignatureSubpacketsUtil { * @return list of subpackets from the hashed/unhashed area */ @JvmStatic - fun

hashedOrUnhashed(signature: PGPSignature, type: SignatureSubpacket): P? { + fun

hashedOrUnhashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { return hashed(signature, type) ?: unhashed(signature, type) } /** - * Select a list of all signature subpackets of the given type, which are present in the hashed area of - * the given signature. + * Select a list of all signature subpackets of the given type, which are present in the + * hashed area of the given signature. * * @param signature signature * @param type subpacket type @@ -507,13 +516,16 @@ class SignatureSubpacketsUtil { * @return list of subpackets from the hashed area */ @JvmStatic - fun

hashed(signature: PGPSignature, type: SignatureSubpacket): P? { + fun

hashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { return getSignatureSubpacket(signature.hashedSubPackets, type) } /** - * Select a list of all signature subpackets of the given type, which are present in the unhashed area of - * the given signature. + * Select a list of all signature subpackets of the given type, which are present in the + * unhashed area of the given signature. * * @param signature signature * @param type subpacket type @@ -521,7 +533,10 @@ class SignatureSubpacketsUtil { * @return list of subpackets from the unhashed area */ @JvmStatic - fun

unhashed(signature: PGPSignature, type: SignatureSubpacket): P? { + fun

unhashed( + signature: PGPSignature, + type: SignatureSubpacket + ): P? { return getSignatureSubpacket(signature.unhashedSubPackets, type) } @@ -534,13 +549,13 @@ class SignatureSubpacketsUtil { * @return last occurrence of the subpacket in the vector */ @JvmStatic - fun

getSignatureSubpacket(vector: PGPSignatureSubpacketVector?, type: SignatureSubpacket): P? { + fun

getSignatureSubpacket( + vector: PGPSignatureSubpacketVector?, + type: SignatureSubpacket + ): P? { val allPackets = vector?.getSubpackets(type.code) ?: return null - return if (allPackets.isEmpty()) - null - else - @Suppress("UNCHECKED_CAST") - allPackets.last() as P + return if (allPackets.isEmpty()) null + else @Suppress("UNCHECKED_CAST") allPackets.last() as P } @JvmStatic @@ -553,24 +568,29 @@ class SignatureSubpacketsUtil { val mask = toBitmask(*flags) if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.CERTIFY_OTHER)) { - throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag CERTIFY_OTHER.") + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag CERTIFY_OTHER.") } if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.SIGN_DATA)) { - throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag SIGN_DATA.") + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag SIGN_DATA.") } if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_COMMS)) { - throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag ENCRYPT_COMMS.") + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag ENCRYPT_COMMS.") } if (!algorithm.isEncryptionCapable() && hasKeyFlag(mask, KeyFlag.ENCRYPT_STORAGE)) { - throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag ENCRYPT_STORAGE.") + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag ENCRYPT_STORAGE.") } if (!algorithm.isSigningCapable() && hasKeyFlag(mask, KeyFlag.AUTHENTICATION)) { - throw IllegalArgumentException("Algorithm $algorithm cannot be used with key flag AUTHENTICATION.") + throw IllegalArgumentException( + "Algorithm $algorithm cannot be used with key flag AUTHENTICATION.") } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index 53bdca0b..70146e0f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -4,6 +4,10 @@ package org.pgpainless.util +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import org.bouncycastle.bcpg.ArmoredInputStream import org.bouncycastle.bcpg.ArmoredOutputStream import org.bouncycastle.openpgp.PGPKeyRing @@ -20,35 +24,21 @@ import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.decryption_verification.OpenPgpInputStream import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.KeyRingUtils -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream class ArmorUtils { companion object { // MessageIDs are 32 printable characters private val PATTER_MESSAGE_ID = "^\\S{32}$".toRegex() - /** - * Constant armor key for comments. - */ + /** Constant armor key for comments. */ const val HEADER_COMMENT = "Comment" - /** - * Constant armor key for program versions. - */ + /** Constant armor key for program versions. */ const val HEADER_VERSION = "Version" - /** - * Constant armor key for message IDs. Useful for split messages. - */ + /** Constant armor key for message IDs. Useful for split messages. */ const val HEADER_MESSAGEID = "MessageID" - /** - * Constant armor key for used hash algorithms in clearsigned messages. - */ + /** Constant armor key for used hash algorithms in clearsigned messages. */ const val HEADER_HASH = "Hash" - /** - * Constant armor key for message character sets. - */ + /** Constant armor key for message character sets. */ const val HEADER_CHARSET = "Charset" /** @@ -56,116 +46,111 @@ class ArmorUtils { * * @param secretKey secret key * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(secretKey: PGPSecretKey): String = - toAsciiArmoredString(secretKey.encoded, keyToHeader(secretKey.publicKey)) + toAsciiArmoredString(secretKey.encoded, keyToHeader(secretKey.publicKey)) /** * Return the ASCII armored encoding of the given [PGPPublicKey]. * * @param publicKey public key * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(publicKey: PGPPublicKey): String = - toAsciiArmoredString(publicKey.encoded, keyToHeader(publicKey)) + toAsciiArmoredString(publicKey.encoded, keyToHeader(publicKey)) /** * Return the ASCII armored encoding of the given [PGPSecretKeyRing]. * * @param secretKeys secret key ring * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(secretKeys: PGPSecretKeyRing): String = - toAsciiArmoredString(secretKeys.encoded, keyToHeader(secretKeys.publicKey)) + toAsciiArmoredString(secretKeys.encoded, keyToHeader(secretKeys.publicKey)) /** * Return the ASCII armored encoding of the given [PGPPublicKeyRing]. * * @param certificate public key ring * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(certificate: PGPPublicKeyRing): String = - toAsciiArmoredString(certificate.encoded, keyToHeader(certificate.publicKey)) + toAsciiArmoredString(certificate.encoded, keyToHeader(certificate.publicKey)) /** - * Return the ASCII armored encoding of the given [PGPSecretKeyRingCollection]. - * The encoding will use per-key ASCII armors protecting each [PGPSecretKeyRing] individually. - * Those armors are then concatenated with newlines in between. + * Return the ASCII armored encoding of the given [PGPSecretKeyRingCollection]. The encoding + * will use per-key ASCII armors protecting each [PGPSecretKeyRing] individually. Those + * armors are then concatenated with newlines in between. * * @param secretKeysCollection secret key ring collection * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(secretKeysCollection: PGPSecretKeyRingCollection): String = - secretKeysCollection.keyRings.asSequence() - .joinToString("\n") { toAsciiArmoredString(it) } + secretKeysCollection.keyRings.asSequence().joinToString("\n") { + toAsciiArmoredString(it) + } /** - * Return the ASCII armored encoding of the given [PGPPublicKeyRingCollection]. - * The encoding will use per-key ASCII armors protecting each [PGPPublicKeyRing] individually. - * Those armors are then concatenated with newlines in between. + * Return the ASCII armored encoding of the given [PGPPublicKeyRingCollection]. The encoding + * will use per-key ASCII armors protecting each [PGPPublicKeyRing] individually. Those + * armors are then concatenated with newlines in between. * * @param certificates public key ring collection * @return ascii armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @Throws(IOException::class) fun toAsciiArmoredString(certificates: PGPPublicKeyRingCollection): String = - certificates.joinToString("\n") { toAsciiArmoredString(it) } + certificates.joinToString("\n") { toAsciiArmoredString(it) } /** - * Return the ASCII armored representation of the given detached signature. - * If [export] is true, the signature will be stripped of non-exportable subpackets or trust-packets. - * If it is false, the signature will be encoded as-is. + * Return the ASCII armored representation of the given detached signature. If [export] is + * true, the signature will be stripped of non-exportable subpackets or trust-packets. If it + * is false, the signature will be encoded as-is. * * @param signature signature * @param export whether to exclude non-exportable subpackets or trust-packets. * @return ascii armored string - * * @throws IOException in case of an error in the [ArmoredOutputStream] */ @JvmStatic @JvmOverloads @Throws(IOException::class) fun toAsciiArmoredString(signature: PGPSignature, export: Boolean = false): String = - toAsciiArmoredString(signature.getEncoded(export)) + toAsciiArmoredString(signature.getEncoded(export)) /** - * Return the ASCII armored encoding of the given OpenPGP data bytes. - * The ASCII armor will include headers from the header map. + * Return the ASCII armored encoding of the given OpenPGP data bytes. The ASCII armor will + * include headers from the header map. * * @param bytes OpenPGP data * @param header header map * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun toAsciiArmoredString(bytes: ByteArray, header: Map>? = null): String = - toAsciiArmoredString(bytes.inputStream(), header) + fun toAsciiArmoredString( + bytes: ByteArray, + header: Map>? = null + ): String = toAsciiArmoredString(bytes.inputStream(), header) /** * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. @@ -174,26 +159,30 @@ class ArmorUtils { * @param inputStream input stream of OpenPGP data * @param header ASCII armor header map * @return ASCII armored encoding - * * @throws IOException in case of an io error */ @JvmStatic @JvmOverloads @Throws(IOException::class) - fun toAsciiArmoredString(inputStream: InputStream, header: Map>? = null): String = - ByteArrayOutputStream().apply { + fun toAsciiArmoredString( + inputStream: InputStream, + header: Map>? = null + ): String = + ByteArrayOutputStream() + .apply { toAsciiArmoredStream(this, header).run { Streams.pipeAll(inputStream, this) this.close() } - }.toString() + } + .toString() /** - * Return an [ArmoredOutputStream] prepared with headers for the given key ring, which wraps the given - * {@link OutputStream}. + * Return an [ArmoredOutputStream] prepared with headers for the given key ring, which wraps + * the given {@link OutputStream}. * - * The armored output stream can be used to encode the key ring by calling [PGPKeyRing.encode] - * with the armored output stream as an argument. + * The armored output stream can be used to encode the key ring by calling + * [PGPKeyRing.encode] with the armored output stream as an argument. * * @param keys OpenPGP key or certificate * @param outputStream wrapped output stream @@ -201,16 +190,18 @@ class ArmorUtils { */ @JvmStatic @Throws(IOException::class) - fun toAsciiArmoredStream(keys: PGPKeyRing, outputStream: OutputStream): ArmoredOutputStream = - toAsciiArmoredStream(outputStream, keyToHeader(keys.publicKey)) + fun toAsciiArmoredStream( + keys: PGPKeyRing, + outputStream: OutputStream + ): ArmoredOutputStream = toAsciiArmoredStream(outputStream, keyToHeader(keys.publicKey)) /** - * Create an [ArmoredOutputStream] wrapping the given [OutputStream]. - * The armored output stream will be prepared with armor headers given by header. + * Create an [ArmoredOutputStream] wrapping the given [OutputStream]. The armored output + * stream will be prepared with armor headers given by header. * * Note: Since the armored output stream is retrieved from [ArmoredOutputStreamFactory.get], - * it may already come with custom headers. Hence, the header entries given by header are appended below those - * already populated headers. + * it may already come with custom headers. Hence, the header entries given by header are + * appended below those already populated headers. * * @param outputStream output stream to wrap * @param header map of header entries @@ -219,19 +210,20 @@ class ArmorUtils { @JvmStatic @JvmOverloads @Throws(IOException::class) - fun toAsciiArmoredStream(outputStream: OutputStream, header: Map>? = null): ArmoredOutputStream = - ArmoredOutputStreamFactory.get(outputStream).apply { - header?.forEach { entry -> - entry.value.forEach { value -> - addHeader(entry.key, value) - } - } + fun toAsciiArmoredStream( + outputStream: OutputStream, + header: Map>? = null + ): ArmoredOutputStream = + ArmoredOutputStreamFactory.get(outputStream).apply { + header?.forEach { entry -> + entry.value.forEach { value -> addHeader(entry.key, value) } } + } /** - * Generate a header map for ASCII armor from the given [PGPPublicKey]. - * The header map consists of a comment field of the keys pretty-printed fingerprint, - * as well as the primary or first user-id plus the count of remaining user-ids. + * Generate a header map for ASCII armor from the given [PGPPublicKey]. The header map + * consists of a comment field of the keys pretty-printed fingerprint, as well as the + * primary or first user-id plus the count of remaining user-ids. * * @param publicKey public key * @return header map @@ -241,124 +233,142 @@ class ArmorUtils { val headerMap = mutableMapOf>() val userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) val first: String? = userIds.firstOrNull() - val primary: String? = userIds.firstOrNull { - publicKey.getSignaturesForID(it)?.asSequence()?.any { sig -> - sig.hashedSubPackets.isPrimaryUserID - } ?: false - } + val primary: String? = + userIds.firstOrNull { + publicKey.getSignaturesForID(it)?.asSequence()?.any { sig -> + sig.hashedSubPackets.isPrimaryUserID + } + ?: false + } // Fingerprint - headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(OpenPgpFingerprint.of(publicKey).prettyPrint()) + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) // Primary / First User ID - (primary ?: first)?.let { headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) } + (primary ?: first)?.let { + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) + } // X-1 further identities when (userIds.size) { - 0, 1 -> {} + 0, + 1 -> {} 2 -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("1 further identity") - else -> headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add("${userIds.size - 1} further identities") + else -> + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add("${userIds.size - 1} further identities") } return headerMap } /** - * Set the version header entry in the ASCII armor. - * If the version info is null or only contains whitespace characters, then the version header will be removed. + * Set the version header entry in the ASCII armor. If the version info is null or only + * contains whitespace characters, then the version header will be removed. * * @param armor armored output stream * @param version version header. */ @JvmStatic - @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + "Use ArmoredOutputStream builder instead.") fun setVersionHeader(armor: ArmoredOutputStream, version: String?) = - armor.setHeader(HEADER_VERSION, version?.let { it.ifBlank { null } }) + armor.setHeader(HEADER_VERSION, version?.let { it.ifBlank { null } }) /** - * Add an ASCII armor header entry about the used hash algorithm into the [ArmoredOutputStream]. + * Add an ASCII armor header entry about the used hash algorithm into the + * [ArmoredOutputStream]. * * @param armor armored output stream * @param hashAlgorithm hash algorithm - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor */ @JvmStatic - @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + "Use ArmoredOutputStream builder instead.") fun addHashAlgorithmHeader(armor: ArmoredOutputStream, hashAlgorithm: HashAlgorithm) = - armor.addHeader(HEADER_HASH, hashAlgorithm.algorithmName) + armor.addHeader(HEADER_HASH, hashAlgorithm.algorithmName) /** * Add an ASCII armor comment header entry into the [ArmoredOutputStream]. * * @param armor armored output stream * @param comment free-text comment - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor */ @JvmStatic - @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + "Use ArmoredOutputStream builder instead.") fun addCommentHeader(armor: ArmoredOutputStream, comment: String) = - armor.addHeader(HEADER_COMMENT, comment) + armor.addHeader(HEADER_COMMENT, comment) /** * Add an ASCII armor message-id header entry into the [ArmoredOutputStream]. * * @param armor armored output stream * @param messageId message id - * - * @see - * RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor + * @see RFC 4880 - + * OpenPGP Message Format §6.2. Forming ASCII Armor */ @JvmStatic - @Deprecated("Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + + @Deprecated( + "Changing ASCII armor headers after ArmoredOutputStream creation is deprecated. " + "Use ArmoredOutputStream builder instead.") fun addMessageIdHeader(armor: ArmoredOutputStream, messageId: String) { - require(PATTER_MESSAGE_ID.matches(messageId)) { "MessageIDs MUST consist of 32 printable characters." } + require(PATTER_MESSAGE_ID.matches(messageId)) { + "MessageIDs MUST consist of 32 printable characters." + } armor.addHeader(HEADER_MESSAGEID, messageId) } /** - * Extract all ASCII armor header values of type comment from the given [ArmoredInputStream]. + * Extract all ASCII armor header values of type comment from the given + * [ArmoredInputStream]. * * @param armor armored input stream * @return list of comment headers */ @JvmStatic fun getCommentHeaderValues(armor: ArmoredInputStream): List = - getArmorHeaderValues(armor, HEADER_COMMENT) + getArmorHeaderValues(armor, HEADER_COMMENT) /** - * Extract all ASCII armor header values of type message id from the given [ArmoredInputStream]. + * Extract all ASCII armor header values of type message id from the given + * [ArmoredInputStream]. * * @param armor armored input stream * @return list of message-id headers */ @JvmStatic fun getMessageIdHeaderValues(armor: ArmoredInputStream): List = - getArmorHeaderValues(armor, HEADER_MESSAGEID) + getArmorHeaderValues(armor, HEADER_MESSAGEID) /** - * Return all ASCII armor header values of type hash-algorithm from the given [ArmoredInputStream]. + * Return all ASCII armor header values of type hash-algorithm from the given + * [ArmoredInputStream]. * * @param armor armored input stream * @return list of hash headers */ @JvmStatic fun getHashHeaderValues(armor: ArmoredInputStream): List = - getArmorHeaderValues(armor, HEADER_HASH) + getArmorHeaderValues(armor, HEADER_HASH) /** - * Return a list of [HashAlgorithm] enums extracted from the hash header entries of the given [ArmoredInputStream]. + * Return a list of [HashAlgorithm] enums extracted from the hash header entries of the + * given [ArmoredInputStream]. * * @param armor armored input stream * @return list of hash algorithms from the ASCII header */ @JvmStatic fun getHashAlgorithms(armor: ArmoredInputStream): List = - getHashHeaderValues(armor).mapNotNull { HashAlgorithm.fromName(it) } + getHashHeaderValues(armor).mapNotNull { HashAlgorithm.fromName(it) } /** * Return all ASCII armor header values of type version from the given [ArmoredInputStream]. @@ -368,7 +378,7 @@ class ArmorUtils { */ @JvmStatic fun getVersionHeaderValues(armor: ArmoredInputStream): List = - getArmorHeaderValues(armor, HEADER_VERSION) + getArmorHeaderValues(armor, HEADER_VERSION) /** * Return all ASCII armor header values of type charset from the given [ArmoredInputStream]. @@ -378,10 +388,11 @@ class ArmorUtils { */ @JvmStatic fun getCharsetHeaderValues(armor: ArmoredInputStream): List = - getArmorHeaderValues(armor, HEADER_CHARSET) + getArmorHeaderValues(armor, HEADER_CHARSET) /** - * Return all ASCII armor header values of the given headerKey from the given [ArmoredInputStream]. + * Return all ASCII armor header values of the given headerKey from the given + * [ArmoredInputStream]. * * @param armor armored input stream * @param key ASCII armor header key @@ -389,34 +400,33 @@ class ArmorUtils { */ @JvmStatic fun getArmorHeaderValues(armor: ArmoredInputStream, key: String): List = - armor.armorHeaders - .filter { it.startsWith("$key: ") } - .map { it.substring(key.length + 2) } // key.len + ": ".len + armor.armorHeaders + .filter { it.startsWith("$key: ") } + .map { it.substring(key.length + 2) } // key.len + ": ".len /** - * Hacky workaround for #96. - * For `PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)` - * or `PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)` - * to read all PGPKeyRings properly, we apparently have to make sure that the [InputStream] that is given - * as constructor argument is a [PGPUtil.BufferedInputStreamExt]. - * Since [PGPUtil.getDecoderStream] will return an [org.bouncycastle.bcpg.ArmoredInputStream] - * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the - * end-result is a [PGPUtil.BufferedInputStreamExt]. + * Hacky workaround for #96. For `PGPPublicKeyRingCollection(InputStream, + * KeyFingerPrintCalculator)` or `PGPSecretKeyRingCollection(InputStream, + * KeyFingerPrintCalculator)` to read all PGPKeyRings properly, we apparently have to make + * sure that the [InputStream] that is given as constructor argument is a + * [PGPUtil.BufferedInputStreamExt]. Since [PGPUtil.getDecoderStream] will return an + * [org.bouncycastle.bcpg.ArmoredInputStream] if the underlying input stream contains + * armored data, we first dearmor the data ourselves to make sure that the end-result is a + * [PGPUtil.BufferedInputStreamExt]. * * @param inputStream input stream * @return BufferedInputStreamExt - * * @throws IOException in case of an IO error */ @JvmStatic @Throws(IOException::class) fun getDecoderStream(inputStream: InputStream): InputStream = - OpenPgpInputStream(inputStream).let { - if (it.isAsciiArmored) { - PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) - } else { - it - } + OpenPgpInputStream(inputStream).let { + if (it.isAsciiArmored) { + PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) + } else { + it } + } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt index 254b5c88..8198214d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt @@ -4,14 +4,14 @@ package org.pgpainless.util -import org.bouncycastle.bcpg.ArmoredInputStream -import org.pgpainless.decryption_verification.ConsumerOptions import java.io.IOException import java.io.InputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.pgpainless.decryption_verification.ConsumerOptions /** - * Factory class for instantiating preconfigured [ArmoredInputStream] instances. - * [get] will return an [ArmoredInputStream] that is set up to properly detect CRC errors v4 style. + * Factory class for instantiating preconfigured [ArmoredInputStream] instances. [get] will return + * an [ArmoredInputStream] that is set up to properly detect CRC errors v4 style. */ class ArmoredInputStreamFactory { @@ -31,14 +31,15 @@ class ArmoredInputStreamFactory { return when (inputStream) { is CRCingArmoredInputStreamWrapper -> inputStream is ArmoredInputStream -> CRCingArmoredInputStreamWrapper(inputStream) - else -> CRCingArmoredInputStreamWrapper( - ArmoredInputStream.builder().apply { - setParseForHeaders(true) - options?.let { - setIgnoreCRC(it.isDisableAsciiArmorCRC) + else -> + CRCingArmoredInputStreamWrapper( + ArmoredInputStream.builder() + .apply { + setParseForHeaders(true) + options?.let { setIgnoreCRC(it.isDisableAsciiArmorCRC) } } - }.build(inputStream)) + .build(inputStream)) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt index 69a5520f..caf14e53 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredOutputStreamFactory.kt @@ -4,25 +4,25 @@ package org.pgpainless.util +import java.io.OutputStream import org.bouncycastle.bcpg.ArmoredOutputStream import org.pgpainless.encryption_signing.ProducerOptions -import java.io.OutputStream /** - * Factory to create configured [ArmoredOutputStream] instances. - * The configuration entails setting custom version and comment headers. + * Factory to create configured [ArmoredOutputStream] instances. The configuration entails setting + * custom version and comment headers. */ class ArmoredOutputStreamFactory { companion object { private const val PGPAINLESS = "PGPainless" - @JvmStatic - private var version: String? = PGPAINLESS + @JvmStatic private var version: String? = PGPAINLESS private var comment: String? = null /** - * Return an instance of the [ArmoredOutputStream] which might have pre-populated armor headers. + * Return an instance of the [ArmoredOutputStream] which might have pre-populated armor + * headers. * * @param outputStream output stream * @param options options @@ -31,39 +31,43 @@ class ArmoredOutputStreamFactory { @JvmStatic @JvmOverloads fun get(outputStream: OutputStream, options: ProducerOptions? = null): ArmoredOutputStream { - val builder = ArmoredOutputStream.builder().apply { - // set fields defined in ArmoredOutputStreamFactory - if (!version.isNullOrBlank()) setVersion(version) - if (!comment.isNullOrBlank()) setComment(comment) + val builder = + ArmoredOutputStream.builder().apply { + // set fields defined in ArmoredOutputStreamFactory + if (!version.isNullOrBlank()) setVersion(version) + if (!comment.isNullOrBlank()) setComment(comment) - // set (and potentially overwrite with) values from ProducerOptions - options?.let { - enableCRC(!it.isDisableAsciiArmorCRC) - if (it.isHideArmorHeaders) clearHeaders() - if (it.hasVersion()) setVersion(it.version) - if (it.hasComment()) addComment(it.comment) - // TODO: configure CRC + // set (and potentially overwrite with) values from ProducerOptions + options?.let { + enableCRC(!it.isDisableAsciiArmorCRC) + if (it.isHideArmorHeaders) clearHeaders() + if (it.hasVersion()) setVersion(it.version) + if (it.hasComment()) addComment(it.comment) + // TODO: configure CRC + } } - } return get(outputStream, builder) } /** - * Build an [ArmoredOutputStream] around the given [outputStream], configured according to the passed in - * [ArmoredOutputStream.Builder] instance. + * Build an [ArmoredOutputStream] around the given [outputStream], configured according to + * the passed in [ArmoredOutputStream.Builder] instance. * * @param outputStream output stream * @param builder builder instance */ @JvmStatic - fun get(outputStream: OutputStream, builder: ArmoredOutputStream.Builder): ArmoredOutputStream { + fun get( + outputStream: OutputStream, + builder: ArmoredOutputStream.Builder + ): ArmoredOutputStream { return builder.build(outputStream) } /** - * Overwrite the version header of ASCII armors with a custom value. - * Newlines in the version info string result in multiple version header entries. - * If this is set to

null
, then the version header is omitted altogether. + * Overwrite the version header of ASCII armors with a custom value. Newlines in the version + * info string result in multiple version header entries. If this is set to
null
, + * then the version header is omitted altogether. * * @param versionString version string */ @@ -72,25 +76,24 @@ class ArmoredOutputStreamFactory { version = if (versionString.isNullOrBlank()) null else versionString.trim() } - /** - * Reset the version header to its default value of [PGPAINLESS]. - */ + /** Reset the version header to its default value of [PGPAINLESS]. */ @JvmStatic fun resetVersionInfo() { version = PGPAINLESS } /** - * Set a comment header value in the ASCII armor header. - * If the comment contains newlines, it will be split into multiple header entries. - * - * @see [ProducerOptions.setComment] for how to set comments for individual messages. + * Set a comment header value in the ASCII armor header. If the comment contains newlines, + * it will be split into multiple header entries. * * @param commentString comment + * @see [ProducerOptions.setComment] for how to set comments for individual messages. */ @JvmStatic fun setComment(commentString: String) { - require(commentString.isNotBlank()) { "Comment cannot be empty. See resetComment() to clear the comment." } + require(commentString.isNotBlank()) { + "Comment cannot be empty. See resetComment() to clear the comment." + } comment = commentString.trim() } @@ -99,4 +102,4 @@ class ArmoredOutputStreamFactory { comment = null } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt index de2052d6..712ac262 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/DateUtil.kt @@ -4,10 +4,10 @@ package org.pgpainless.util +import java.util.* import openpgp.formatUTC import openpgp.parseUTC import openpgp.toSecondsPrecision -import java.util.* class DateUtil { @@ -19,8 +19,7 @@ class DateUtil { * @param dateString timestamp * @return date */ - @JvmStatic - fun parseUTCDate(dateString: String): Date = dateString.parseUTC() + @JvmStatic fun parseUTCDate(dateString: String): Date = dateString.parseUTC() /** * Format a date as UTC timestamp. @@ -28,23 +27,21 @@ class DateUtil { * @param date date * @return timestamp */ - @JvmStatic - fun formatUTCDate(date: Date): String = date.formatUTC() + @JvmStatic fun formatUTCDate(date: Date): String = date.formatUTC() /** * Floor a date down to seconds precision. + * * @param date date * @return floored date */ - @JvmStatic - fun toSecondsPrecision(date: Date): Date = date.toSecondsPrecision() + @JvmStatic fun toSecondsPrecision(date: Date): Date = date.toSecondsPrecision() /** * Return the current date "floored" to UTC precision. * * @return now */ - @JvmStatic - fun now() = toSecondsPrecision(Date()) + @JvmStatic fun now() = toSecondsPrecision(Date()) } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt index 9ca193a6..3aa22d0d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/MultiMap.kt @@ -8,13 +8,13 @@ class MultiMap : Iterable>> { private val map: Map> - constructor(): this(mutableMapOf()) - constructor(other: MultiMap): this(other.map) + constructor() : this(mutableMapOf()) + + constructor(other: MultiMap) : this(other.map) + constructor(content: Map>) { map = mutableMapOf() - content.forEach { - map[it.key] = it.value.toMutableSet() - } + content.forEach { map[it.key] = it.value.toMutableSet() } } override fun iterator(): Iterator>> { @@ -23,45 +23,58 @@ class MultiMap : Iterable>> { val size: Int get() = map.size + fun size() = size + val keys: Set get() = map.keys + fun keySet() = keys + val values: Collection> get() = map.values + fun values() = values + val entries: Set>> get() = map.entries + fun entrySet() = entries + fun isEmpty(): Boolean = map.isEmpty() + fun containsKey(key: K): Boolean = map.containsKey(key) + fun containsValue(value: V): Boolean = map.values.any { it.contains(value) } + fun contains(key: K, value: V): Boolean = map[key]?.contains(value) ?: false + operator fun get(key: K): Set? = map[key] - fun put(key: K, value: V) = - (map as MutableMap).put(key, mutableSetOf(value)) - fun plus(key: K, value: V) = - (map as MutableMap).getOrPut(key) { mutableSetOf() }.add(value) - fun put(key: K, values: Set) = - (map as MutableMap).put(key, values.toMutableSet()) + + fun put(key: K, value: V) = (map as MutableMap).put(key, mutableSetOf(value)) + + fun plus(key: K, value: V) = (map as MutableMap).getOrPut(key) { mutableSetOf() }.add(value) + + fun put(key: K, values: Set) = (map as MutableMap).put(key, values.toMutableSet()) + fun plus(key: K, values: Set) = - (map as MutableMap).getOrPut(key) { mutableSetOf() }.addAll(values) - fun putAll(other: MultiMap) = other.map.entries.forEach { - put(it.key, it.value) - } - fun plusAll(other: MultiMap) = other.map.entries.forEach { - plus(it.key, it.value) - } + (map as MutableMap).getOrPut(key) { mutableSetOf() }.addAll(values) + + fun putAll(other: MultiMap) = other.map.entries.forEach { put(it.key, it.value) } + + fun plusAll(other: MultiMap) = other.map.entries.forEach { plus(it.key, it.value) } + fun removeAll(key: K) = (map as MutableMap).remove(key) + fun remove(key: K, value: V) = (map as MutableMap)[key]?.remove(value) + fun clear() = (map as MutableMap).clear() + fun flatten() = map.flatMap { it.value }.toSet() override fun equals(other: Any?): Boolean { - return if (other == null) - false - else if (other !is MultiMap<*, *>) - false + return if (other == null) false + else if (other !is MultiMap<*, *>) false else if (this === other) { true } else { @@ -72,4 +85,4 @@ class MultiMap : Iterable>> { override fun hashCode(): Int { return map.hashCode() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt index 15dbe886..d2295f32 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt @@ -5,9 +5,9 @@ package org.pgpainless.util /** - * Registry for known notations. - * Since signature verification must reject signatures with critical notations that are not known to the application, - * there must be some way to tell PGPainless which notations actually are known. + * Registry for known notations. Since signature verification must reject signatures with critical + * notations that are not known to the application, there must be some way to tell PGPainless which + * notations actually are known. * * To add a notation name, call {@link #addKnownNotation(String)}. */ @@ -19,8 +19,8 @@ class NotationRegistry constructor(notations: Set = setOf()) { } /** - * Add a known notation name into the registry. - * This will cause critical notations with that name to no longer invalidate the signature. + * Add a known notation name into the registry. This will cause critical notations with that + * name to no longer invalidate the signature. * * @param notationName name of the notation */ @@ -36,10 +36,8 @@ class NotationRegistry constructor(notations: Set = setOf()) { */ fun isKnownNotation(notationName: String): Boolean = knownNotations.contains(notationName) - /** - * Clear all known notations from the registry. - */ + /** Clear all known notations from the registry. */ fun clear() { knownNotations.clear() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt index a64fda53..4d1e49d2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -11,9 +11,7 @@ import org.bouncycastle.util.Arrays * * @param chars may be null for empty passwords. */ -class Passphrase( - chars: CharArray? -) { +class Passphrase(chars: CharArray?) { private val lock = Any() private var valid = true private val chars: CharArray? @@ -23,17 +21,17 @@ class Passphrase( } /** - * Return a copy of the underlying char array. - * A return value of null represents an empty password. + * Return a copy of the underlying char array. A return value of null represents an empty + * password. * * @return passphrase chars. - * * @throws IllegalStateException in case the password has been cleared at this point. */ - fun getChars(): CharArray? = synchronized(lock) { - check(valid) { "Passphrase has been cleared." } - chars?.copyOf() - } + fun getChars(): CharArray? = + synchronized(lock) { + check(valid) { "Passphrase has been cleared." } + chars?.copyOf() + } /** * Return true if the passphrase has not yet been cleared. @@ -51,23 +49,20 @@ class Passphrase( val isEmpty: Boolean get() = synchronized(lock) { valid && chars == null } - /** - * Overwrite the char array with spaces and mark the [Passphrase] as invalidated. - */ - fun clear() = synchronized(lock) { - chars?.fill(' ') - valid = false - } + /** Overwrite the char array with spaces and mark the [Passphrase] as invalidated. */ + fun clear() = + synchronized(lock) { + chars?.fill(' ') + valid = false + } override fun equals(other: Any?): Boolean { - return if (other == null) - false - else if (this === other) - true - else if (other !is Passphrase) - false + return if (other == null) false + else if (this === other) true + else if (other !is Passphrase) false else - getChars() == null && other.getChars() == null || Arrays.constantTimeAreEqual(getChars(), other.getChars()) + getChars() == null && other.getChars() == null || + Arrays.constantTimeAreEqual(getChars(), other.getChars()) } override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() @@ -83,23 +78,23 @@ class Passphrase( @JvmStatic fun fromPassword(password: CharSequence) = Passphrase(password.toString().toCharArray()) - @JvmStatic - fun emptyPassphrase() = Passphrase(null) + @JvmStatic fun emptyPassphrase() = Passphrase(null) /** - * Return a copy of the passed in char array, with leading and trailing whitespace characters removed. - * If the passed in char array is null, return null. - * If the resulting char array is empty, return null as well. + * Return a copy of the passed in char array, with leading and trailing whitespace + * characters removed. If the passed in char array is null, return null. If the resulting + * char array is empty, return null as well. * * @param chars char array * @return copy of char array with leading and trailing whitespace characters removed */ @JvmStatic private fun trimWhitespace(chars: CharArray?): CharArray? { - return chars?.dropWhile { it.isWhitespace() } - ?.dropLastWhile { it.isWhitespace() } - ?.toCharArray() - ?.let { if (it.isEmpty()) null else it } + return chars + ?.dropWhile { it.isWhitespace() } + ?.dropLastWhile { it.isWhitespace() } + ?.toCharArray() + ?.let { if (it.isEmpty()) null else it } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt index 894d0869..ef8eb9e7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/SessionKey.kt @@ -10,21 +10,22 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm /** * A [SessionKey] is the symmetric key that is used to encrypt/decrypt an OpenPGP message payload. - * The OpenPGP message header contains a copy of the session key, encrypted for the public key of each recipient. + * The OpenPGP message header contains a copy of the session key, encrypted for the public key of + * each recipient. * * @param algorithm symmetric key algorithm * @param key bytes of the key */ -data class SessionKey(val algorithm: SymmetricKeyAlgorithm, - val key: ByteArray) { +data class SessionKey(val algorithm: SymmetricKeyAlgorithm, val key: ByteArray) { /** * Constructor to create a session key from a BC [PGPSessionKey] object. * * @param sessionKey BC session key */ - constructor(sessionKey: PGPSessionKey): - this(SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm), sessionKey.key) + constructor( + sessionKey: PGPSessionKey + ) : this(SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm), sessionKey.key) override fun toString(): String { return "${algorithm.algorithmId}:${Hex.toHexString(key)}" @@ -45,4 +46,4 @@ data class SessionKey(val algorithm: SymmetricKeyAlgorithm, override fun hashCode(): Int { return 31 * algorithm.hashCode() + key.contentHashCode() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt index 3c215d80..f2794925 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/selection/userid/SelectUserId.kt @@ -4,15 +4,13 @@ package org.pgpainless.util.selection.userid +import java.util.function.Predicate import org.bouncycastle.openpgp.PGPKeyRing import org.pgpainless.PGPainless -import java.util.function.Predicate abstract class SelectUserId : Predicate, (String) -> Boolean { - /** - * Legacy glue code to forward accept() calls to invoke() instead. - */ + /** Legacy glue code to forward accept() calls to invoke() instead. */ @Deprecated("Use invoke() instead.", ReplaceWith("invoke(userId)")) protected fun accept(userId: String): Boolean = invoke(userId) @@ -27,10 +25,10 @@ abstract class SelectUserId : Predicate, (String) -> Boolean { * @return filter */ @JvmStatic - fun exactMatch(query: CharSequence) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - userId == query - } + fun exactMatch(query: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId == query + } /** * Filter for user-ids which start with the given [substring]. @@ -39,10 +37,10 @@ abstract class SelectUserId : Predicate, (String) -> Boolean { * @return filter */ @JvmStatic - fun startsWith(substring: CharSequence) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - userId.startsWith(substring) - } + fun startsWith(substring: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId.startsWith(substring) + } /** * Filter for user-ids which contain the given [substring]. @@ -51,54 +49,53 @@ abstract class SelectUserId : Predicate, (String) -> Boolean { * @return filter */ @JvmStatic - fun containsSubstring(substring: CharSequence) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - userId.contains(substring) - } + fun containsSubstring(substring: CharSequence) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = userId.contains(substring) + } /** - * Filter for user-ids which contain the given [email] address. - * Note: This only accepts user-ids which properly have the email address surrounded by angle brackets. + * Filter for user-ids which contain the given [email] address. Note: This only accepts + * user-ids which properly have the email address surrounded by angle brackets. * - * The argument [email] can both be a plain email address (`foo@bar.baz`), - * or surrounded by angle brackets (``), the result of the filter will be the same. + * The argument [email] can both be a plain email address (`foo@bar.baz`), or surrounded by + * angle brackets (``), the result of the filter will be the same. * * @param email email address * @return filter */ @JvmStatic fun containsEmailAddress(email: CharSequence) = - if (email.startsWith('<') && email.endsWith('>')) - containsSubstring(email) - else - containsSubstring("<$email>") + if (email.startsWith('<') && email.endsWith('>')) containsSubstring(email) + else containsSubstring("<$email>") @JvmStatic fun byEmail(email: CharSequence) = or(exactMatch(email), containsEmailAddress(email)) @JvmStatic - fun validUserId(keyRing: PGPKeyRing) = object : SelectUserId() { - private val info = PGPainless.inspectKeyRing(keyRing) - override fun invoke(userId: String): Boolean = - info.isUserIdValid(userId) - } + fun validUserId(keyRing: PGPKeyRing) = + object : SelectUserId() { + private val info = PGPainless.inspectKeyRing(keyRing) + + override fun invoke(userId: String): Boolean = info.isUserIdValid(userId) + } @JvmStatic - fun and(vararg filters: SelectUserId) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - filters.all { it.invoke(userId) } - } + fun and(vararg filters: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = filters.all { it.invoke(userId) } + } @JvmStatic - fun or(vararg filters: SelectUserId) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - filters.any { it.invoke(userId) } - } + fun or(vararg filters: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = filters.any { it.invoke(userId) } + } @JvmStatic - fun not(filter: SelectUserId) = object : SelectUserId() { - override fun invoke(userId: String): Boolean = - !filter.invoke(userId) - } + fun not(filter: SelectUserId) = + object : SelectUserId() { + override fun invoke(userId: String): Boolean = !filter.invoke(userId) + } } -} \ No newline at end of file +} From 29fe4faeed3bb486f1627f7c2479c8fda58d9dc9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 23 Oct 2023 14:27:54 +0200 Subject: [PATCH 188/351] Add .git-blame-ignore-revs file --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..447355cc --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Ignore initial spotlessApply using ktfmt +51e9bfc67f19e16a69790a8d92bd6b1c86a76a5f From 4f64868914f333940eefc443e663a3e21a50a8d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 Oct 2023 10:15:16 +0200 Subject: [PATCH 189/351] Add dropbox-style editorconfig Source: https://github.com/facebook/ktfmt/blob/main/docs/editorconfig/.editorconfig-dropbox --- .editorconfig | 589 +++----------------------------------------------- 1 file changed, 30 insertions(+), 559 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9bf812d1..bc214f67 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,513 +1,51 @@ -# SPDX-FileCopyrightText: 2021 Paul Schaub +# This .editorconfig section approximates ktfmt's formatting rules. You can include it in an +# existing .editorconfig file or use it standalone by copying it to /.editorconfig +# and making sure your editor is set to read settings from .editorconfig files. # -# SPDX-License-Identifier: CC0-1.0 +# It includes editor-specific config options for IntelliJ IDEA. +# +# If any option is wrong, PR are welcome -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 +[{*.kt,*.kts}] indent_style = space -insert_final_newline = false -max_line_length = 120 -tab_width = 4 +insert_final_newline = true +max_line_length = 100 +indent_size = 4 ij_continuation_indent_size = 8 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = false -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[*.java] -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = false -ij_java_align_multiline_annotation_parameters = false -ij_java_align_multiline_array_initializer_expression = false -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = false -ij_java_align_multiline_chained_methods = false -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = true -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = false -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = true -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = false -ij_java_align_multiline_throws_list = false -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_annotation_parameter_wrap = off -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = off -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = off -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = off -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = false -ij_java_class_annotation_wrap = split_into_lines -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 10000 -ij_java_class_names_in_javadoc = 1 -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_while_brace_force = never -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = false -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = off -ij_java_extends_list_wrap = off -ij_java_field_annotation_wrap = split_into_lines -ij_java_finally_on_new_line = false -ij_java_for_brace_force = never -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = off -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = $*,|,java.**,javax.**,|,* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 2 -ij_java_keep_blank_lines_between_package_declaration_and_header = 2 -ij_java_keep_blank_lines_in_code = 2 -ij_java_keep_blank_lines_in_declarations = 2 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = true -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_at_first_column = true -ij_java_method_annotation_wrap = split_into_lines -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = off -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = off -ij_java_modifier_list_wrap = false -ij_java_names_count_to_use_import_on_demand = 1000 -ij_java_new_line_after_lparen_in_record_header = false -ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* -ij_java_parameter_annotation_wrap = off -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = normal -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = off -ij_java_rparen_on_new_line_in_record_header = false -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = off -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = off -ij_java_throws_list_wrap = off -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = off -ij_java_visibility = public -ij_java_while_brace_force = never -ij_java_while_on_new_line = false -ij_java_wrap_comments = false -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.properties] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals -ij_properties_spaces_around_key_value_delimiter = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true - -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_xml_align_attributes = true -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal -ij_xml_use_custom_settings = false - -[{*.bash,*.sh,*.zsh}] -indent_size = 2 -tab_width = 2 -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false -ij_shell_use_unix_line_separator = true - -[{*.gant,*.gradle,*.groovy,*.gy}] -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true -ij_groovy_align_multiline_list_or_map = true -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = true -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 5 -ij_groovy_do_while_brace_force = never -ij_groovy_else_on_new_line = false -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_if_brace_force = never -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 3 -ij_groovy_parameter_annotation_wrap = off -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = true -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never -ij_groovy_while_on_new_line = false -ij_groovy_wrap_long_lines = false - -[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}] +ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_assignment_wrap = off +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = off +ij_kotlin_call_parameters_wrap = on_every_item ij_kotlin_catch_on_new_line = false ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_for_chained_calls = true ij_kotlin_continuation_indent_for_expression_bodies = true ij_kotlin_continuation_indent_in_argument_lists = true -ij_kotlin_continuation_indent_in_elvis = true -ij_kotlin_continuation_indent_in_if_conditions = true -ij_kotlin_continuation_indent_in_parameter_lists = true -ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false ij_kotlin_else_on_new_line = false ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = off +ij_kotlin_extends_list_wrap = normal ij_kotlin_field_annotation_wrap = split_into_lines ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = false ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 2 ij_kotlin_keep_blank_lines_in_code = 2 @@ -519,13 +57,13 @@ ij_kotlin_lbrace_on_next_line = false ij_kotlin_line_comment_add_space = false ij_kotlin_line_comment_at_first_column = true ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = off -ij_kotlin_method_parameters_new_line_after_left_paren = false -ij_kotlin_method_parameters_right_paren_on_new_line = false -ij_kotlin_method_parameters_wrap = off -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 3 -ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 9999 +ij_kotlin_name_count_to_use_star_import_for_members = 9999 +ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true @@ -552,72 +90,5 @@ ij_kotlin_spaces_around_when_arrow = true ij_kotlin_variable_annotation_wrap = off ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.har,*.json}] -indent_size = 2 -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = true -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal -ij_html_uniform_ident = false - -[{*.markdown,*.md}] -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 - -[{*.yaml,*.yml}] -indent_size = 2 -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true From 733d5d6b82e8ff036a827b0ab5ee79823b135c47 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 24 Oct 2023 10:21:10 +0200 Subject: [PATCH 190/351] Decrease continuation_indent from 8 to 4 --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index bc214f67..a9ff980b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ indent_style = space insert_final_newline = true max_line_length = 100 indent_size = 4 -ij_continuation_indent_size = 8 +ij_continuation_indent_size = 4 # was 8 ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false From e9d8ddc57b5a4f5f5fd48b88c4d9fbec31dc5d2e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 25 Oct 2023 19:07:52 +0200 Subject: [PATCH 191/351] Kotlin conversion: SignatureValidator --- .../consumer/SignatureValidator.java | 681 ----------------- .../extensions/PGPSignatureExtensions.kt | 11 + .../signature/consumer/SignatureValidator.kt | 693 ++++++++++++++++++ 3 files changed, 704 insertions(+), 681 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java deleted file mode 100644 index 18bf5883..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ /dev/null @@ -1,681 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentHashMap; - -import org.bouncycastle.bcpg.sig.KeyFlags; -import org.bouncycastle.bcpg.sig.NotationData; -import org.bouncycastle.bcpg.sig.SignatureCreationTime; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureSubpacket; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; -import org.pgpainless.util.DateUtil; -import org.pgpainless.util.NotationRegistry; - -/** - * A collection of validators that perform validation steps over signatures. - */ -public abstract class SignatureValidator { - - public abstract void verify(PGPSignature signature) throws SignatureValidationException; - - /** - * Check, whether there is the possibility that the given signature was created by the given key. - * {@link #verify(PGPSignature)} throws a {@link SignatureValidationException} if we can say with certainty that - * the signature was not created by the given key (e.g. if the sig carries another issuer, issuer fingerprint packet). - * - * If there is no information found in the signature about who created it (no issuer, no fingerprint), - * {@link #verify(PGPSignature)} will simply return since it is plausible that the given key created the sig. - * - * @param signingKey signing key - * @return validator that throws a {@link SignatureValidationException} if the signature was not possibly made by - * the given key. - */ - public static SignatureValidator wasPossiblyMadeByKey(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - OpenPgpFingerprint signingKeyFingerprint = OpenPgpFingerprint.of(signingKey); - - Long issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature); - if (issuer != null) { - if (issuer != signingKey.getKeyID()) { - throw new SignatureValidationException("Signature was not created by " + - signingKeyFingerprint + " (signature issuer: " + Long.toHexString(issuer) + ")"); - } - } - - OpenPgpFingerprint fingerprint = - SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(signature); - if (fingerprint != null) { - if (!fingerprint.equals(signingKeyFingerprint)) { - throw new SignatureValidationException("Signature was not created by " + - signingKeyFingerprint + " (signature fingerprint: " + fingerprint + ")"); - } - } - - // No issuer information found, so we cannot rule out that we did not create the sig - } - }; - - } - - /** - * Verify that a subkey binding signature - if the subkey is signing-capable - contains a valid primary key - * binding signature. - * - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator hasValidPrimaryKeyBindingSignatureIfRequired(PGPPublicKey primaryKey, - PGPPublicKey subkey, Policy policy, - Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (!PublicKeyAlgorithm.requireFromId(signature.getKeyAlgorithm()).isSigningCapable()) { - // subkey is not signing capable -> No need to process embedded sigs - return; - } - - KeyFlags keyFlags = SignatureSubpacketsUtil.getKeyFlags(signature); - if (keyFlags == null) { - return; - } - if (!KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.SIGN_DATA) - && !KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER)) { - return; - } - - try { - PGPSignatureList embeddedSignatures = SignatureSubpacketsUtil.getEmbeddedSignature(signature); - if (embeddedSignatures == null) { - throw new SignatureValidationException( - "Missing primary key binding signature on signing capable subkey " + - Long.toHexString(subkey.getKeyID()), Collections.emptyMap()); - } - - boolean hasValidPrimaryKeyBinding = false; - Map rejectedEmbeddedSigs = new ConcurrentHashMap<>(); - for (PGPSignature embedded : embeddedSignatures) { - - if (SignatureType.valueOf(embedded.getSignatureType()) == SignatureType.PRIMARYKEY_BINDING) { - - try { - signatureStructureIsAcceptable(subkey, policy).verify(embedded); - signatureIsEffective(referenceDate).verify(embedded); - correctPrimaryKeyBindingSignature(primaryKey, subkey).verify(embedded); - - hasValidPrimaryKeyBinding = true; - break; - } catch (SignatureValidationException e) { - rejectedEmbeddedSigs.put(embedded, e); - } - } - } - - if (!hasValidPrimaryKeyBinding) { - throw new SignatureValidationException( - "Missing primary key binding signature on signing capable subkey " + - Long.toHexString(subkey.getKeyID()), rejectedEmbeddedSigs); - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot process list of embedded signatures.", e); - } - } - }; - } - - /** - * Verify that a signature has an acceptable structure. - * - * @param signingKey signing key - * @param policy policy - * @return validator - */ - public static SignatureValidator signatureStructureIsAcceptable(PGPPublicKey signingKey, Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsNotMalformed(signingKey).verify(signature); - if (signature.getVersion() >= 4) { - signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); - signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); - } - signatureUsesAcceptableHashAlgorithm(policy).verify(signature); - signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); - } - }; - } - - /** - * Verify that a signature was made using an acceptable {@link PublicKeyAlgorithm}. - * - * @param policy policy - * @param signingKey signing key - * @return validator - */ - public static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, - PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.requireFromId(signingKey.getAlgorithm()); - int bitStrength = signingKey.getBitStrength(); - if (bitStrength == -1) { - throw new SignatureValidationException("Cannot determine bit strength of signing key."); - } - if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { - throw new SignatureValidationException("Signature was made using unacceptable key. " + - algorithm + " (" + bitStrength + - " bits) is not acceptable according to the public key algorithm policy."); - } - } - }; - } - - /** - * Verify that a signature uses an acceptable {@link HashAlgorithm}. - * - * @param policy policy - * @return validator - */ - public static SignatureValidator signatureUsesAcceptableHashAlgorithm(Policy policy) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy = - getHashAlgorithmPolicyForSignature(signature, policy); - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + - hashAlgorithm + " (Signature creation time: " + - DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); - } - } catch (NoSuchElementException e) { - throw new SignatureValidationException("Signature uses unknown hash algorithm " + - signature.getHashAlgorithm()); - } - } - }; - } - - /** - * Return the applicable {@link Policy.HashAlgorithmPolicy} for the given {@link PGPSignature}. - * Revocation signatures are being policed using a different policy than non-revocation signatures. - * - * @param signature signature - * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs - * @return policy - */ - private static Policy.HashAlgorithmPolicy getHashAlgorithmPolicyForSignature(PGPSignature signature, - Policy policy) { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - Policy.HashAlgorithmPolicy hashAlgorithmPolicy; - if (type == SignatureType.CERTIFICATION_REVOCATION || type == SignatureType.KEY_REVOCATION || - type == SignatureType.SUBKEY_REVOCATION) { - hashAlgorithmPolicy = policy.getRevocationSignatureHashAlgorithmPolicy(); - } else { - hashAlgorithmPolicy = policy.getSignatureHashAlgorithmPolicy(); - } - return hashAlgorithmPolicy; - } - - /** - * Verify that a signature does not carry critical unknown notations. - * - * @param registry notation registry of known notations - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownNotations(NotationRegistry registry) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - List hashedNotations = SignatureSubpacketsUtil.getHashedNotationData(signature); - for (NotationData notation : hashedNotations) { - if (!notation.isCritical()) { - continue; - } - if (!registry.isKnownNotation(notation.getNotationName())) { - throw new SignatureValidationException("Signature contains unknown critical notation '" + - notation.getNotationName() + "' in its hashed area."); - } - } - } - }; - } - - /** - * Verify that a signature does not contain critical unknown subpackets. - * - * @return validator - */ - public static SignatureValidator signatureDoesNotHaveCriticalUnknownSubpackets() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); - for (int criticalTag : hashedSubpackets.getCriticalTags()) { - try { - SignatureSubpacket.requireFromCode(criticalTag); - } catch (NoSuchElementException e) { - throw new SignatureValidationException( - "Signature contains unknown critical subpacket of type " + - Long.toHexString(criticalTag)); - } - } - } - }; - } - - /** - * Verify that a signature is effective right now. - * - * @return validator - */ - public static SignatureValidator signatureIsEffective() { - return signatureIsEffective(new Date()); - } - - /** - * Verify that a signature is effective at the given reference date. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsEffective(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - signatureIsAlreadyEffective(referenceDate).verify(signature); - signatureIsNotYetExpired(referenceDate).verify(signature); - } - }; - } - - /** - * Verify that a signature was created prior to the given reference date. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsAlreadyEffective(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date signatureCreationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature).getTime(); - // Hard revocations are always effective - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - if (signatureCreationTime.after(referenceDate)) { - throw new SignatureValidationException("Signature was created at " + signatureCreationTime + - " and is therefore not yet valid at " + referenceDate); - } - } - }; - } - - /** - * Verify that a signature is not yet expired. - * - * @param referenceDate reference date for signature verification - * @return validator - */ - public static SignatureValidator signatureIsNotYetExpired(Date referenceDate) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - // Hard revocations do not expire - if (SignatureUtils.isHardRevocation(signature)) { - return; - } - - Date signatureExpirationTime = SignatureSubpacketsUtil.getSignatureExpirationTimeAsDate(signature); - if (signatureExpirationTime != null && signatureExpirationTime.before(referenceDate)) { - throw new SignatureValidationException("Signature is already expired (expiration: " + - signatureExpirationTime + ", validation: " + referenceDate + ")"); - } - } - }; - } - - /** - * Verify that a signature is not malformed. - * A signature is malformed if it has no hashed creation time subpacket, - * it predates the creation time of the signing key, or it predates the creation date - * of the signing key binding signature. - * - * @param creator signing key - * @return validator - */ - public static SignatureValidator signatureIsNotMalformed(PGPPublicKey creator) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (signature.getVersion() >= 4) { - signatureHasHashedCreationTime().verify(signature); - } - signatureDoesNotPredateSigningKey(creator).verify(signature); - if (signature.getSignatureType() != SignatureType.PRIMARYKEY_BINDING.getCode()) { - signatureDoesNotPredateSigningKeyBindingDate(creator).verify(signature); - } - } - }; - } - - public static SignatureValidator signatureDoesNotPredateSignee(PGPPublicKey signee) { - return signatureDoesNotPredateKeyCreation(signee); - } - - /** - * Verify that a signature has a hashed creation time subpacket. - * - * @return validator - */ - public static SignatureValidator signatureHasHashedCreationTime() { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureCreationTime creationTime = SignatureSubpacketsUtil.getSignatureCreationTime(signature); - if (creationTime == null) { - throw new SignatureValidationException( - "Malformed signature. Signature has no signature creation time subpacket in its hashed area."); - } - } - }; - } - - /** - * Verify that a signature does not predate the creation time of the signing key. - * - * @param key signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKey(PGPPublicKey key) { - return signatureDoesNotPredateKeyCreation(key); - } - - /** - * Verify that a signature does not predate the creation time of the given key. - * - * @param key key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateKeyCreation(PGPPublicKey key) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date keyCreationTime = key.getCreationTime(); - Date signatureCreationTime = signature.getCreationTime(); - - if (keyCreationTime.after(signatureCreationTime)) { - throw new SignatureValidationException("Signature predates key (key creation: " + - keyCreationTime + ", signature creation: " + signatureCreationTime + ")"); - } - } - }; - } - - /** - * Verify that a signature does not predate the binding date of the signing key. - * - * @param signingKey signing key - * @return validator - */ - public static SignatureValidator signatureDoesNotPredateSigningKeyBindingDate(PGPPublicKey signingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (signingKey.isMasterKey()) { - return; - } - boolean predatesBindingSig = true; - Iterator bindingSignatures = - signingKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); - if (!bindingSignatures.hasNext()) { - throw new SignatureValidationException("Signing subkey does not have a subkey binding signature."); - } - while (bindingSignatures.hasNext()) { - PGPSignature bindingSig = bindingSignatures.next(); - if (!bindingSig.getCreationTime().after(signature.getCreationTime())) { - predatesBindingSig = false; - } - } - if (predatesBindingSig) { - throw new SignatureValidationException( - "Signature was created before the signing key was bound to the key ring."); - } - } - }; - } - - /** - * Verify that a subkey binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctSubkeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - if (primaryKey.getKeyID() == subkey.getKeyID()) { - throw new SignatureValidationException("Primary key cannot be its own subkey."); - } - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), primaryKey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify subkey binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a primary key binding signature is correct. - * - * @param primaryKey primary key - * @param subkey subkey - * @return validator - */ - public static SignatureValidator correctPrimaryKeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), subkey); - boolean valid = signature.verifyCertification(primaryKey, subkey); - if (!valid) { - throw new SignatureValidationException("Primary Key Binding Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException( - "Cannot verify primary key binding signature correctness", e); - } - } - }; - } - - /** - * Verify that a direct-key signature is correct. - * - * @param signer signing key - * @param signee signed key - * @return validator - */ - public static SignatureValidator correctSignatureOverKey(PGPPublicKey signer, PGPPublicKey signee) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), signer); - boolean valid; - if (signer.getKeyID() == signee.getKeyID() || signature.getSignatureType() == PGPSignature.DIRECT_KEY) { - valid = signature.verifyCertification(signee); - } else { - valid = signature.verifyCertification(signer, signee); - } - if (!valid) { - throw new SignatureValidationException("Signature is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify direct-key signature correctness", e); - } - } - }; - } - - /** - * Verify that a signature is a certification signature. - * - * @return validator - */ - public static SignatureValidator signatureIsCertification() { - return signatureIsOfType( - SignatureType.POSITIVE_CERTIFICATION, - SignatureType.CASUAL_CERTIFICATION, - SignatureType.GENERIC_CERTIFICATION, - SignatureType.NO_CERTIFICATION); - } - - /** - * Verify that a signature type equals one of the given {@link SignatureType SignatureTypes}. - * - * @param signatureTypes one or more signature types - * @return validator - */ - public static SignatureValidator signatureIsOfType(SignatureType... signatureTypes) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - boolean valid = false; - for (SignatureType allowed : signatureTypes) { - if (type == allowed) { - valid = true; - break; - } - } - if (!valid) { - throw new SignatureValidationException("Signature is of type " + type + " while only " + - Arrays.toString(signatureTypes) + " are allowed here."); - } - } - }; - } - - /** - * Verify that a signature over a user-id is correct. - * - * @param userId user-id - * @param certifiedKey key carrying the user-id - * @param certifyingKey key that created the signature. - * @return validator - */ - public static SignatureValidator correctSignatureOverUserId(String userId, PGPPublicKey certifiedKey, - PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userId, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-id '" + userId + - "' is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify signature over user-id '" + - userId + "'.", e); - } - } - }; - } - - /** - * Verify that a signature over a user-attribute packet is correct. - * - * @param userAttributes user attributes - * @param certifiedKey key carrying the user-attributes - * @param certifyingKey key that created the certification signature - * @return validator - */ - public static SignatureValidator correctSignatureOverUserAttributes(PGPUserAttributeSubpacketVector userAttributes, - PGPPublicKey certifiedKey, - PGPPublicKey certifyingKey) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance() - .getPgpContentVerifierBuilderProvider(), certifyingKey); - boolean valid = signature.verifyCertification(userAttributes, certifiedKey); - if (!valid) { - throw new SignatureValidationException("Signature over user-attribute vector is not correct."); - } - } catch (PGPException | ClassCastException e) { - throw new SignatureValidationException("Cannot verify signature over user-attribute vector.", e); - } - } - }; - } - - public static SignatureValidator signatureWasCreatedInBounds(Date notBefore, Date notAfter) { - return new SignatureValidator() { - @Override - public void verify(PGPSignature signature) throws SignatureValidationException { - Date timestamp = signature.getCreationTime(); - if (notBefore != null && timestamp.before(notBefore)) { - throw new SignatureValidationException( - "Signature was made before the earliest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Earliest allowed: " + - DateUtil.formatUTCDate(notBefore)); - } - if (notAfter != null && timestamp.after(notAfter)) { - throw new SignatureValidationException( - "Signature was made after the latest allowed signature creation time. Created: " + - DateUtil.formatUTCDate(timestamp) + " Latest allowed: " + - DateUtil.formatUTCDate(notAfter)); - } - } - }; - } - -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt index 2be011bd..5547e48b 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -8,6 +8,8 @@ import java.util.* import openpgp.plusSeconds import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.algorithm.RevocationState import org.pgpainless.algorithm.SignatureType import org.pgpainless.key.OpenPgpFingerprint @@ -94,3 +96,12 @@ fun PGPSignature?.toRevocationState() = val PGPSignature.fingerprint: OpenPgpFingerprint? get() = SignatureSubpacketsUtil.getIssuerFingerprintAsOpenPgpFingerprint(this) + +val PGPSignature.publicKeyAlgorithm: PublicKeyAlgorithm + get() = PublicKeyAlgorithm.requireFromId(keyAlgorithm) + +val PGPSignature.signatureHashAlgorithm: HashAlgorithm + get() = HashAlgorithm.requireFromId(hashAlgorithm) + +fun PGPSignature.isOfType(type: SignatureType): Boolean = + SignatureType.requireFromCode(signatureType) == type diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt new file mode 100644 index 00000000..c7cbd6fd --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -0,0 +1,693 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.lang.Exception +import java.util.Date +import openpgp.formatUTC +import openpgp.openPgpKeyId +import org.bouncycastle.extensions.fingerprint +import org.bouncycastle.extensions.isHardRevocation +import org.bouncycastle.extensions.isOfType +import org.bouncycastle.extensions.publicKeyAlgorithm +import org.bouncycastle.extensions.signatureExpirationDate +import org.bouncycastle.extensions.signatureHashAlgorithm +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.algorithm.SignatureSubpacket +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.policy.Policy +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil +import org.pgpainless.util.NotationRegistry + +abstract class SignatureValidator { + + @Throws(SignatureValidationException::class) abstract fun verify(signature: PGPSignature) + + companion object { + + /** + * Check, whether there is the possibility that the given signature was created by the given + * key. [verify] throws a [SignatureValidationException] if we can say with certainty that + * the signature was not created by the given key (e.g. if the sig carries another issuer, + * issuer fingerprint packet). + * + * If there is no information found in the signature about who created it (no issuer, no + * fingerprint), [verify] will simply return since it is plausible that the given key + * created the sig. + * + * @param signingKey signing key + * @return validator that throws a [SignatureValidationException] if the signature was not + * possibly made by the given key. + */ + @JvmStatic + fun wasPossiblyMadeByKey(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val signingKeyFingerprint = OpenPgpFingerprint.of(signingKey) + val issuer = SignatureSubpacketsUtil.getIssuerKeyIdAsLong(signature) + + if (issuer != null) { + if (issuer != signingKey.keyID) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature issuer: ${issuer.openPgpKeyId()})") + } + } + + if (signature.fingerprint != null && + signature.fingerprint != signingKeyFingerprint) { + throw SignatureValidationException( + "Signature was not created by" + + " $signingKeyFingerprint (signature fingerprint: ${signature.fingerprint})") + } + } + + // No issuer information found, so we cannot rule out that we did not create the sig + } + } + + /** + * Verify that a subkey binding signature - if the subkey is signing-capable - contains a + * valid primary key binding signature. + * + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceDate reference date for signature verification + * @return validator + */ + @JvmStatic + fun hasValidPrimaryKeyBindingSignatureIfRequired( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (!signature.publicKeyAlgorithm.isSigningCapable()) { + // subkey is not signing capable -> No need to process embedded signatures + return + } + + // Make sure we have key flags + SignatureSubpacketsUtil.getKeyFlags(signature)?.let { + if (!KeyFlag.hasKeyFlag(it.flags, KeyFlag.SIGN_DATA) && + !KeyFlag.hasKeyFlag(it.flags, KeyFlag.CERTIFY_OTHER)) { + return + } + } + ?: return + + try { + val embeddedSignatures = + SignatureSubpacketsUtil.getEmbeddedSignature(signature) + if (embeddedSignatures.isEmpty) { + throw SignatureValidationException( + "Missing primary key binding" + + " signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + mapOf()) + } + + val rejectedEmbeddedSignatures = mutableMapOf() + if (!embeddedSignatures.any { embedded -> + if (embedded.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + try { + signatureStructureIsAcceptable(subkey, policy).verify(embedded) + signatureIsEffective(referenceTime).verify(embedded) + correctPrimaryKeyBindingSignature(primaryKey, subkey) + .verify(embedded) + return@any true + } catch (e: SignatureValidationException) { + rejectedEmbeddedSignatures[embedded] = e + } + } + false + }) { + throw SignatureValidationException( + "Missing primary key binding signature on signing capable subkey ${subkey.keyID.openPgpKeyId()}", + rejectedEmbeddedSignatures) + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot process list of embedded signatures.", e) + } + } + } + } + + /** + * Verify that a signature has an acceptable structure. + * + * @param signingKey signing key + * @param policy policy + * @return validator + */ + @JvmStatic + fun signatureStructureIsAcceptable( + signingKey: PGPPublicKey, + policy: Policy + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsNotMalformed(signingKey).verify(signature) + if (signature.version >= 4) { + signatureDoesNotHaveCriticalUnknownNotations(policy.notationRegistry) + .verify(signature) + signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature) + } + signatureUsesAcceptableHashAlgorithm(policy).verify(signature) + signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature) + } + } + } + + /** + * Verify that a signature was made using an acceptable [PublicKeyAlgorithm]. + * + * @param policy policy + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureUsesAcceptablePublicKeyAlgorithm( + policy: Policy, + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.bitStrength == -1) { + throw SignatureValidationException( + "Cannot determine bit strength of signing key.") + } + if (!policy.publicKeyAlgorithmPolicy.isAcceptable( + signingKey.publicKeyAlgorithm, signingKey.bitStrength)) { + throw SignatureValidationException( + "Signature was made using unacceptable key. " + + "${signingKey.publicKeyAlgorithm} (${signingKey.bitStrength} bits) is " + + "not acceptable according to the public key algorithm policy.") + } + } + } + } + + @JvmStatic + fun signatureUsesAcceptableHashAlgorithm(policy: Policy): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + val algorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy) + if (!algorithmPolicy.isAcceptable( + signature.signatureHashAlgorithm, signature.creationTime)) { + throw SignatureValidationException( + "Signature uses unacceptable" + + " hash algorithm ${signature.signatureHashAlgorithm}" + + " (Signature creation time: ${signature.creationTime.formatUTC()})") + } + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature uses unknown hash" + " algorithm ${signature.hashAlgorithm}") + } + } + } + } + + /** + * Return the applicable [Policy.HashAlgorithmPolicy] for the given [PGPSignature]. + * Revocation signatures are being policed using a different policy than non-revocation + * signatures. + * + * @param signature signature + * @param policy revocation policy for revocation sigs, normal policy for non-rev sigs + * @return policy + */ + @JvmStatic + private fun getHashAlgorithmPolicyForSignature( + signature: PGPSignature, + policy: Policy + ): Policy.HashAlgorithmPolicy { + val type = SignatureType.requireFromCode(signature.signatureType) + return when (type) { + SignatureType.CERTIFICATION_REVOCATION, + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy + else -> policy.signatureHashAlgorithmPolicy + } + } + + /** + * Verify that a signature does not carry critical unknown notations. + * + * @param registry notation registry of known notations + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownNotations( + registry: NotationRegistry + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + SignatureSubpacketsUtil.getHashedNotationData(signature) + .filter { it.isCritical && !registry.isKnownNotation(it.notationName) } + .forEach { + throw SignatureValidationException( + "Signature contains unknown critical notation '${it.notationName}' in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not contain critical unknown subpackets. + * + * @return validator + */ + @JvmStatic + fun signatureDoesNotHaveCriticalUnknownSubpackets(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signature.hashedSubPackets.criticalTags.forEach { + try { + SignatureSubpacket.requireFromCode(it) + } catch (e: NoSuchElementException) { + throw SignatureValidationException( + "Signature contains unknown critical subpacket of type 0x${Integer.toHexString(it)}") + } + } + } + } + } + + /** + * Verify that a signature is effective at the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + @JvmOverloads + fun signatureIsEffective(referenceTime: Date = Date()): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + signatureIsAlreadyEffective(referenceTime).verify(signature) + signatureIsNotYetExpired(referenceTime).verify(signature) + } + } + } + + /** + * Verify that a signature was created prior to the given reference date. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsAlreadyEffective(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + if (signature.creationTime > referenceTime) { + throw SignatureValidationException( + "Signature was created at ${signature.creationTime.formatUTC()} and" + + " is therefore not yet valid at ${referenceTime.formatUTC()}") + } + } + } + } + + /** + * Verify that a signature is not yet expired. + * + * @param referenceTime reference date for signature verification + * @return validator + */ + @JvmStatic + fun signatureIsNotYetExpired(referenceTime: Date): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.isHardRevocation) { + return + } + val expirationDate = signature.signatureExpirationDate + if (expirationDate != null && expirationDate < referenceTime) { + throw SignatureValidationException( + "Signature is already expired " + + "(expiration: ${expirationDate.formatUTC()}," + + " validation: ${referenceTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature is not malformed. A signature is malformed if it has no hashed + * creation time subpacket, it predates the creation time of the signing key, or it predates + * the creation date of the signing key binding signature. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureIsNotMalformed(signingKey: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signature.version >= 4) { + signatureHasHashedCreationTime().verify(signature) + } + signatureDoesNotPredateSigningKey(signingKey).verify(signature) + if (!signature.isOfType(SignatureType.PRIMARYKEY_BINDING)) { + signatureDoesNotPredateSigningKeyBindingDate(signingKey).verify(signature) + } + } + } + } + + @JvmStatic + fun signatureDoesNotPredateSignee(signee: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signee) + } + + /** + * Verify that a signature does not predate the creation time of the signing key. + * + * @param key signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKey(signingKey: PGPPublicKey): SignatureValidator { + return signatureDoesNotPredateKeyCreation(signingKey) + } + + /** + * Verify that a signature does not predate the creation time of the given key. + * + * @param key key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateKeyCreation(key: PGPPublicKey): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (key.creationTime > signature.creationTime) { + throw SignatureValidationException( + "Signature predates key" + + " (key creation: ${key.creationTime.formatUTC()}," + + " signature creation: ${signature.creationTime.formatUTC()})") + } + } + } + } + + /** + * Verify that a signature has a hashed creation time subpacket. + * + * @return validator + */ + @JvmStatic + fun signatureHasHashedCreationTime(): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (SignatureSubpacketsUtil.getSignatureCreationTime(signature) == null) { + throw SignatureValidationException( + "Malformed signature." + + "Signature has no signature creation time subpacket in its hashed area.") + } + } + } + } + + /** + * Verify that a signature does not predate the binding date of the signing key. + * + * @param signingKey signing key + * @return validator + */ + @JvmStatic + fun signatureDoesNotPredateSigningKeyBindingDate( + signingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signingKey.isMasterKey) { + return + } + if (signingKey + .getSignaturesOfType(SignatureType.SUBKEY_BINDING.code) + .asSequence() + .map { + if (signature.creationTime < it.creationTime) { + throw SignatureValidationException( + "Signature was created " + + "before the signing key was bound to the certificate.") + } + } + .none()) { + throw SignatureValidationException( + "Signing subkey does not have a subkey binding signature.") + } + } + } + } + + /** + * Verify that a subkey binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctSubkeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + primaryKey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify subkey binding signature correctness", e) + } + } + } + } + + /** + * Verify that a primary key binding signature is correct. + * + * @param primaryKey primary key + * @param subkey subkey + * @return validator + */ + @JvmStatic + fun correctPrimaryKeyBindingSignature( + primaryKey: PGPPublicKey, + subkey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (primaryKey.keyID == subkey.keyID) { + throw SignatureValidationException("Primary key cannot be its own subkey.") + } + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + subkey) + if (!signature.verifyCertification(primaryKey, subkey)) { + throw SignatureValidationException( + "Primary Key Binding Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify primary key binding signature correctness", e) + } + } + } + } + + /** + * Verify that a direct-key signature is correct. + * + * @param signingKey signing key + * @param signedKey signed key + * @return validator + */ + @JvmStatic + fun correctSignatureOverKey( + signingKey: PGPPublicKey, + signedKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + signingKey) + val valid = + if (signingKey.keyID == signedKey.keyID || + signature.isOfType(SignatureType.DIRECT_KEY)) { + signature.verifyCertification(signedKey) + } else { + signature.verifyCertification(signingKey, signedKey) + } + if (!valid) { + throw SignatureValidationException("Signature is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify direct-key signature correctness", e) + } + } + } + } + + @JvmStatic + fun signatureIsCertification(): SignatureValidator { + return signatureIsOfType( + SignatureType.POSITIVE_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION) + } + + /** + * Verify that a signature type equals one of the given [SignatureType]. + * + * @param signatureType one or more signature types + * @return validator + */ + @JvmStatic + fun signatureIsOfType(vararg signatureType: SignatureType): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + if (signatureType.none { signature.isOfType(it) }) { + throw SignatureValidationException( + "Signature is of type" + + " ${SignatureType.requireFromCode(signature.signatureType)}, " + + "while only ${signatureType.contentToString()} are allowed here.") + } + } + } + } + + /** + * Verify that a signature over a user-id is correct. + * + * @param userId user-id + * @param certifiedKey key carrying the user-id + * @param certifyingKey key that created the signature. + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserId( + userId: CharSequence, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userId.toString(), certifiedKey)) { + throw SignatureValidationException( + "Signature over user-id '$userId' is not valid.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-id '$userId'.", e) + } + } + } + } + + /** + * Verify that a signature over a user-attribute packet is correct. + * + * @param userAttributes user attributes + * @param certifiedKey key carrying the user-attributes + * @param certifyingKey key that created the certification signature + * @return validator + */ + @JvmStatic + fun correctSignatureOverUserAttributes( + userAttributes: PGPUserAttributeSubpacketVector, + certifiedKey: PGPPublicKey, + certifyingKey: PGPPublicKey + ): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + try { + signature.init( + ImplementationFactory.getInstance().pgpContentVerifierBuilderProvider, + certifyingKey) + if (!signature.verifyCertification(userAttributes, certifiedKey)) { + throw SignatureValidationException( + "Signature over user-attributes is not correct.") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } catch (e: ClassCastException) { + throw SignatureValidationException( + "Cannot verify signature over user-attribute vector.", e) + } + } + } + } + + @JvmStatic + fun signatureWasCreatedInBounds(notBefore: Date?, notAfter: Date?): SignatureValidator { + return object : SignatureValidator() { + override fun verify(signature: PGPSignature) { + val timestamp = signature.creationTime + if (notBefore != null && timestamp < notBefore) { + throw SignatureValidationException( + "Signature was made before the earliest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " earliest allowed: ${notBefore.formatUTC()}") + } + if (notAfter != null && timestamp > notAfter) { + throw SignatureValidationException( + "Signature was made before the latest allowed signature creation time." + + " Created: ${timestamp.formatUTC()}," + + " latest allowed: ${notAfter.formatUTC()}") + } + } + } + } + } +} From dc05a492f5940012dcb5076a92c4ffdc1d5afb93 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 25 Oct 2023 19:08:03 +0200 Subject: [PATCH 192/351] Fix reuse --- .reuse/dep5 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index 96efa937..c03bbcae 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -9,6 +9,11 @@ Source: https://pgpainless.org # Copyright: $YEAR $NAME <$CONTACT> # License: ... +# GitBlameIgnore +Files: .git-blame-ignore-revs +Copyright: 2023 Paul Schaub +License: CC0-1.0 + # Documentation Files: docs/* Copyright: 2022 Paul Schaub @@ -23,6 +28,11 @@ Files: gradle* Copyright: 2015 the original author or authors. License: Apache-2.0 +# Editorconfig +Files: .editorconfig +Copyright: Facebook +License: Apache-2.0 + # PGPainless Logo Files: assets/repository-open-graph.png Copyright: 2021 Paul Schaub From 19b45644ae73d74db487a4b65052ee3ed2526027 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 26 Oct 2023 12:52:04 +0200 Subject: [PATCH 193/351] Kotlin conversion: SignatureVerifier --- .../signature/consumer/SignatureVerifier.java | 486 -------------- .../signature/consumer/package-info.java | 8 - .../pgpainless/signature/consumer/README.md | 0 .../signature/consumer/SignatureVerifier.kt | 596 ++++++++++++++++++ 4 files changed, 596 insertions(+), 494 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java rename pgpainless-core/src/main/{java => kotlin}/org/pgpainless/signature/consumer/README.md (100%) create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java deleted file mode 100644 index a55037e5..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureVerifier.java +++ /dev/null @@ -1,486 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.consumer; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.policy.Policy; - -/** - * Collection of static methods for signature verification. - * Signature verification entails validation of certain criteria (see {@link SignatureValidator}), as well as - * cryptographic verification of signature correctness. - */ -public final class SignatureVerifier { - - private SignatureVerifier() { - - } - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId key carrying the user-id - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureType type = SignatureType.valueOf(signature.getSignatureType()); - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - return verifyUserIdCertification(userId, signature, signingKey, keyWithUserId, policy, referenceDate); - case CERTIFICATION_REVOCATION: - return verifyUserIdRevocation(userId, signature, signingKey, keyWithUserId, policy, referenceDate); - default: - throw new SignatureValidationException("Signature is not a valid user-id certification/revocation signature: " + type); - } - } - - /** - * Verify a certification self-signature over a user-id. - * - * @param userId user-id - * @param signature certification signature - * @param primaryKey primary key - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the self-signature is verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifyUserIdCertification(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * Verify a user-id certification. - * - * @param userId user-id - * @param signature certification signature - * @param signingKey key that created the certification - * @param keyWithUserId primary key that carries the user-id - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdCertification(String userId, PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); - - return true; - } - - /** - * Verify a user-id revocation self-signature. - * - * @param userId user-id - * @param signature user-id revocation signature - * @param primaryKey primary key - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the user-id revocation signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifyUserIdRevocation(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * Verify a user-id revocation signature. - * - * @param userId user-id - * @param signature revocation signature - * @param signingKey key that created the revocation signature - * @param keyWithUserId primary key carrying the user-id - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the user-id revocation signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserIdRevocation(String userId, PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserId, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature); - - return true; - } - - /** - * Verify a certification self-signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification self-signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifyUserAttributesCertification(userAttributes, signature, primaryKey, primaryKey, policy, - referenceDate); - } - - /** - * Verify a certification signature over a user-attributes packet. - * - * @param userAttributes user attributes - * @param signature certification signature - * @param signingKey key that created the user-attributes certification - * @param keyWithUserAttributes key that carries the user-attributes certification - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesCertification(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserAttributes, Policy policy, - Date referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsCertification().verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey) - .verify(signature); - - return true; - } - - /** - * Verify a user-attributes revocation self-signature. - * - * @param userAttributes user-attributes - * @param signature user-attributes revocation signature - * @param primaryKey primary key that carries the user-attributes - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifyUserAttributesRevocation(userAttributes, signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * Verify a user-attributes revocation signature. - * - * @param userAttributes user-attributes - * @param signature revocation signature - * @param signingKey revocation key - * @param keyWithUserAttributes key that carries the user-attributes - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyUserAttributesRevocation(PGPUserAttributeSubpacketVector userAttributes, - PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey keyWithUserAttributes, Policy policy, - Date referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverUserAttributes(userAttributes, keyWithUserAttributes, signingKey) - .verify(signature); - - return true; - } - - /** - * Verify a subkey binding signature. - * - * @param signature binding signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the binding signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySubkeyBindingSignature(PGPSignature signature, PGPPublicKey primaryKey, - PGPPublicKey subkey, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceDate) - .verify(signature); - SignatureValidator.correctSubkeyBindingSignature(primaryKey, subkey).verify(signature); - - return true; - } - - /** - * Verify a subkey revocation signature. - * - * @param signature subkey revocation signature - * @param primaryKey primary key - * @param subkey subkey - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the subkey revocation signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySubkeyBindingRevocation(PGPSignature signature, PGPPublicKey primaryKey, - PGPPublicKey subkey, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(subkey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverKey(primaryKey, subkey).verify(signature); - - return true; - } - - /** - * Verify a direct-key self-signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the signature can be verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifyDirectKeySignature(signature, primaryKey, primaryKey, policy, referenceDate); - } - - /** - * Verify a direct-key signature. - * - * @param signature signature - * @param signingKey signing key - * @param signedKey signed key - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyDirectKeySignature(PGPSignature signature, PGPPublicKey signingKey, - PGPPublicKey signedKey, Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureDoesNotPredateSignee(signedKey).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverKey(signingKey, signedKey).verify(signature); - - return true; - } - - /** - * Verify a key revocation signature. - * - * @param signature signature - * @param primaryKey primary key - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if signature verification is successful - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyKeyRevocationSignature(PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - SignatureValidator.signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(primaryKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - SignatureValidator.correctSignatureOverKey(primaryKey, primaryKey).verify(signature); - - return true; - } - - /** - * Initialize a signature and verify it afterwards by updating it with the signed data. - * - * @param signature OpenPGP signature - * @param signedData input stream containing the signed data - * @param signingKey the key that created the signature - * @param policy policy - * @param referenceDate reference date of signature verification - * @return true if the signature is successfully verified - * - * @throws SignatureValidationException if the signature verification fails for some reason - */ - public static boolean verifyUninitializedSignature(PGPSignature signature, InputStream signedData, - PGPPublicKey signingKey, Policy policy, Date referenceDate) - throws SignatureValidationException { - initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey); - return verifyInitializedSignature(signature, signingKey, policy, referenceDate); - } - - /** - * Initialize a signature and then update it with the signed data from the given {@link InputStream}. - * - * @param signature OpenPGP signature - * @param signedData input stream containing signed data - * @param signingKey key that created the signature - * - * @throws SignatureValidationException in case the signature cannot be verified for some reason - */ - public static void initializeSignatureAndUpdateWithSignedData(PGPSignature signature, InputStream signedData, - PGPPublicKey signingKey) - throws SignatureValidationException { - try { - signature.init(ImplementationFactory.getInstance().getPgpContentVerifierBuilderProvider(), signingKey); - int read; - byte[] buf = new byte[8192]; - byte lastByte = -1; - while ((read = signedData.read(buf)) != -1) { - // If we previously omitted a newline, but the stream is not yet empty, add it now - if (lastByte == (byte) '\n') { - signature.update(lastByte); - } - lastByte = buf[read - 1]; - - if (lastByte == (byte) '\n') { - // if last byte in buffer is newline, omit it for now - signature.update(buf, 0, read - 1); - } else { - // otherwise, write buffer as usual - signature.update(buf, 0, read); - } - } - } catch (PGPException e) { - throw new SignatureValidationException("Cannot init signature.", e); - } catch (IOException e) { - throw new SignatureValidationException("Cannot update signature.", e); - } - } - - /** - * Verify an initialized signature. - * An initialized signature was already updated with the signed data. - * - * @param signature OpenPGP signature - * @param signingKey key that created the signature - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if signature is verified successfully - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifyInitializedSignature(PGPSignature signature, PGPPublicKey signingKey, Policy policy, - Date referenceDate) - throws SignatureValidationException { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective(referenceDate).verify(signature); - - try { - if (!signature.verify()) { - throw new SignatureValidationException("Signature is not correct."); - } - return true; - } catch (PGPException e) { - throw new SignatureValidationException("Could not verify signature correctness.", e); - } - } - - public static boolean verifyOnePassSignature(PGPSignature signature, PGPPublicKey signingKey, - OnePassSignatureCheck onePassSignature, Policy policy) - throws SignatureValidationException { - try { - SignatureValidator.wasPossiblyMadeByKey(signingKey).verify(signature); - SignatureValidator.signatureStructureIsAcceptable(signingKey, policy).verify(signature); - SignatureValidator.signatureIsEffective().verify(signature); - } catch (SignatureValidationException e) { - throw new SignatureValidationException("Signature is not valid: " + e.getMessage(), e); - } - - try { - if (onePassSignature.getSignature() == null) { - throw new IllegalStateException("No comparison signature provided."); - } - if (!onePassSignature.getOnePassSignature().verify(signature)) { - throw new SignatureValidationException("Bad signature of key " + - Long.toHexString(signingKey.getKeyID())); - } - } catch (PGPException e) { - throw new SignatureValidationException("Could not verify correctness of One-Pass-Signature: " + - e.getMessage(), e); - } - - return true; - } - - /** - * Verify a signature (certification or revocation) over a user-id. - * - * @param userId user-id - * @param signature self-signature - * @param primaryKey primary key that created the signature - * @param policy policy - * @param referenceDate reference date for signature verification - * @return true if the signature is successfully verified - * - * @throws SignatureValidationException if signature verification fails for some reason - */ - public static boolean verifySignatureOverUserId(String userId, PGPSignature signature, PGPPublicKey primaryKey, - Policy policy, Date referenceDate) - throws SignatureValidationException { - return verifySignatureOverUserId(userId, signature, primaryKey, primaryKey, policy, referenceDate); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java deleted file mode 100644 index e8e53285..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signature verification. - */ -package org.pgpainless.signature.consumer; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/README.md b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md similarity index 100% rename from pgpainless-core/src/main/java/org/pgpainless/signature/consumer/README.md rename to pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/README.md diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt new file mode 100644 index 00000000..77793c90 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt @@ -0,0 +1,596 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.consumer + +import java.io.IOException +import java.io.InputStream +import java.util.* +import openpgp.openPgpKeyId +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance +import org.pgpainless.policy.Policy +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverKey +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserAttributes +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSignatureOverUserId +import org.pgpainless.signature.consumer.SignatureValidator.Companion.correctSubkeyBindingSignature +import org.pgpainless.signature.consumer.SignatureValidator.Companion.hasValidPrimaryKeyBindingSignatureIfRequired +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureDoesNotPredateSignee +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsCertification +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsEffective +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureIsOfType +import org.pgpainless.signature.consumer.SignatureValidator.Companion.signatureStructureIsAcceptable +import org.pgpainless.signature.consumer.SignatureValidator.Companion.wasPossiblyMadeByKey + +/** + * Collection of static methods for signature verification. Signature verification entails + * validation of certain criteria (see [SignatureValidator]), as well as cryptographic verification + * of signature correctness. + */ +class SignatureVerifier { + + companion object { + + /** + * Verify a signature (certification or revocation) over a user-id. + * + * @param userId user-id + * @param signature certification signature + * @param signingKey key that created the certification + * @param keyWithUserId key carrying the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySignatureOverUserId( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + val type = SignatureType.requireFromCode(signature.signatureType) + return when (type) { + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION -> + verifyUserIdCertification( + userId, signature, signingKey, keyWithUserId, policy, referenceTime) + SignatureType.CERTIFICATION_REVOCATION -> + verifyUserIdRevocation( + userId, signature, signingKey, keyWithUserId, policy, referenceTime) + else -> + throw SignatureValidationException( + "Signature is not a valid user-id certification/revocation signature: $type") + } + } + + /** + * Verify a certification self-signature over a user-id. + * + * @param userId user-id + * @param signature certification signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the self-signature is verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdCertification( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserIdCertification( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-id certification. + * + * @param userId user-id + * @param signature certification signature + * @param signingKey key that created the certification + * @param keyWithUserId primary key that carries the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdCertification( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsCertification().verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) + + return true + } + + /** + * Verify a user-id revocation self-signature. + * + * @param userId user-id + * @param signature user-id revocation signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the user-id revocation signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdRevocation( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserIdRevocation( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-id revocation signature. + * + * @param userId user-id + * @param signature revocation signature + * @param signingKey key that created the revocation signature + * @param keyWithUserId primary key carrying the user-id + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the user-id revocation signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserIdRevocation( + userId: CharSequence, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithUserId: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserId(userId, keyWithUserId, signingKey).verify(signature) + + return true + } + + /** + * Verify a certification self-signature over a user-attributes packet. + * + * @param userAttributes user attributes + * @param signature certification self-signature + * @param primaryKey primary key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesCertification( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserAttributesCertification( + userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a certification signature over a user-attributes packet. + * + * @param userAttributes user attributes + * @param signature certification signature + * @param signingKey key that created the user-attributes certification + * @param keyWithAttributes key that carries the user-attributes certification + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesCertification( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithAttributes: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsCertification().verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) + .verify(signature) + + return true + } + + /** + * Verify a user-attributes revocation self-signature. + * + * @param userAttributes user-attributes + * @param signature user-attributes revocation signature + * @param primaryKey primary key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesRevocation( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyUserAttributesRevocation( + userAttributes, signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a user-attributes revocation signature. + * + * @param userAttributes user-attributes + * @param signature revocation signature + * @param signingKey revocation key + * @param keyWithAttributes key that carries the user-attributes + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUserAttributesRevocation( + userAttributes: PGPUserAttributeSubpacketVector, + signature: PGPSignature, + signingKey: PGPPublicKey, + keyWithAttributes: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureIsOfType(SignatureType.CERTIFICATION_REVOCATION).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverUserAttributes(userAttributes, keyWithAttributes, signingKey) + .verify(signature) + + return true + } + + /** + * Verify a subkey binding signature. + * + * @param signature binding signature + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the binding signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySubkeyBindingSignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.SUBKEY_BINDING).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureDoesNotPredateSignee(subkey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + hasValidPrimaryKeyBindingSignatureIfRequired(primaryKey, subkey, policy, referenceTime) + .verify(signature) + correctSubkeyBindingSignature(primaryKey, subkey).verify(signature) + + return true + } + + /** + * Verify a subkey revocation signature. + * + * @param signature subkey revocation signature + * @param primaryKey primary key + * @param subkey subkey + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the subkey revocation signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySubkeyBindingRevocation( + signature: PGPSignature, + primaryKey: PGPPublicKey, + subkey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.SUBKEY_REVOCATION).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureDoesNotPredateSignee(subkey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(primaryKey, subkey).verify(signature) + + return true + } + + /** + * Verify a direct-key self-signature. + * + * @param signature signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature can be verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyDirectKeySignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifyDirectKeySignature( + signature, primaryKey, primaryKey, policy, referenceTime) + } + + /** + * Verify a direct-key signature. + * + * @param signature signature + * @param signingKey signing key + * @param signedKey signed key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyDirectKeySignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + signedKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.DIRECT_KEY).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureDoesNotPredateSignee(signedKey).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(signingKey, signedKey).verify(signature) + + return true + } + + /** + * Verify a key revocation signature. + * + * @param signature signature + * @param primaryKey primary key + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature verification is successful + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyKeyRevocationSignature( + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + signatureIsOfType(SignatureType.KEY_REVOCATION).verify(signature) + signatureStructureIsAcceptable(primaryKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + correctSignatureOverKey(primaryKey, primaryKey).verify(signature) + + return true + } + + /** + * Initialize a signature and verify it afterwards by updating it with the signed data. + * + * @param signature OpenPGP signature + * @param signedData input stream containing the signed data + * @param signingKey the key that created the signature + * @param policy policy + * @param referenceTime reference date of signature verification + * @return true if the signature is successfully verified + * @throws SignatureValidationException if the signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyUninitializedSignature( + signature: PGPSignature, + signedData: InputStream, + signingKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + initializeSignatureAndUpdateWithSignedData(signature, signedData, signingKey) + return verifyInitializedSignature(signature, signingKey, policy, referenceTime) + } + + /** + * Initialize a signature and then update it with the signed data from the given + * [InputStream]. + * + * @param signature OpenPGP signature + * @param signedData input stream containing signed data + * @param signingKey key that created the signature + * @throws SignatureValidationException in case the signature cannot be verified for some + * reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun initializeSignatureAndUpdateWithSignedData( + signature: PGPSignature, + signedData: InputStream, + signingKey: PGPPublicKey + ) { + try { + signature.init( + getInstance().pgpContentVerifierBuilderProvider, + signingKey, + ) + var read: Int + val buf = ByteArray(8192) + var lastByte: Byte = -1 + while (signedData.read(buf).also { read = it } != -1) { + // If we previously omitted a newline, but the stream is not yet empty, add it + // now + if (lastByte == '\n'.code.toByte()) { + signature.update(lastByte) + } + lastByte = buf[read - 1] + if (lastByte == '\n'.code.toByte()) { + // if last byte in buffer is newline, omit it for now + signature.update(buf, 0, read - 1) + } else { + // otherwise, write buffer as usual + signature.update(buf, 0, read) + } + } + } catch (e: PGPException) { + throw SignatureValidationException("Cannot init signature.", e) + } catch (e: IOException) { + throw SignatureValidationException("Cannot update signature.", e) + } + } + + /** + * Verify an initialized signature. An initialized signature was already updated with the + * signed data. + * + * @param signature OpenPGP signature + * @param signingKey key that created the signature + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if signature is verified successfully + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyInitializedSignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective(referenceTime).verify(signature) + + return try { + if (!signature.verify()) { + throw SignatureValidationException("Signature is not correct.") + } + true + } catch (e: PGPException) { + throw SignatureValidationException("Could not verify signature correctness.", e) + } + } + + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifyOnePassSignature( + signature: PGPSignature, + signingKey: PGPPublicKey, + onePassSignature: OnePassSignatureCheck, + policy: Policy + ): Boolean { + try { + wasPossiblyMadeByKey(signingKey).verify(signature) + signatureStructureIsAcceptable(signingKey, policy).verify(signature) + signatureIsEffective().verify(signature) + } catch (e: SignatureValidationException) { + throw SignatureValidationException("Signature is not valid: ${e.message}", e) + } + + try { + checkNotNull(onePassSignature.signature) { "No comparison signature provided." } + if (!onePassSignature.onePassSignature.verify(signature)) { + throw SignatureValidationException( + "Bad signature of key ${signingKey.keyID.openPgpKeyId()}") + } + } catch (e: PGPException) { + throw SignatureValidationException( + "Could not verify correctness of One-Pass-Signature: ${e.message}", e) + } + + return true + } + + /** + * Verify a signature (certification or revocation) over a user-id. + * + * @param userId user-id + * @param signature self-signature + * @param primaryKey primary key that created the signature + * @param policy policy + * @param referenceTime reference date for signature verification + * @return true if the signature is successfully verified + * @throws SignatureValidationException if signature verification fails for some reason + */ + @JvmStatic + @Throws(SignatureValidationException::class) + fun verifySignatureOverUserId( + userId: CharSequence, + signature: PGPSignature, + primaryKey: PGPPublicKey, + policy: Policy, + referenceTime: Date + ): Boolean { + return verifySignatureOverUserId( + userId, signature, primaryKey, primaryKey, policy, referenceTime) + } + } +} From f4bfb9dc04ba511695403072650354aca4f66e5e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 26 Oct 2023 12:52:21 +0200 Subject: [PATCH 194/351] Remove test with expired key --- .../KleopatraCompatibilityTest.java | 272 ------------------ 1 file changed, 272 deletions(-) delete mode 100644 pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java diff --git a/pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java b/pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java deleted file mode 100644 index cb6a26b9..00000000 --- a/pgpainless-core/src/test/java/investigations/KleopatraCompatibilityTest.java +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package investigations; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.Test; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageInspector; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.implementation.JceImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; - -public class KleopatraCompatibilityTest { - - public static final String KLEOPATRA_PUBKEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "\n" + - "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\n" + - "g94JEXpwhb9guKuzYzP/QeBp4qFu5FO87w==\n" + - "=Jz7i\n" + - "-----END PGP PUBLIC KEY BLOCK-----\n"; - public static final String KLEOPATRA_SECKEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "\n" + - "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\n" + - "=cgLL\n" + - "-----END PGP PRIVATE KEY BLOCK-----\n"; - // signed and encrypted - public static final String KLEOPATRA_MESSAGE = "-----BEGIN PGP MESSAGE-----\n" + - "\n" + - "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\n" + - "=MgH4\n" + - "-----END PGP MESSAGE-----\n"; - public static final String PGPAINLESS_MESSAGE = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "hQGMA2gglaIyvWSdAQv/Y9Wx763qAM95+teCUPPNRc5Iwqbc5uFjxfbcwsHIWdiZ\n" + - "n2wHNUmd2dUqYgpqOcBwZ/fUJuoHj/uXKZ1pbz2QSVYaL9MulKpgWiVAo0K2w0Oc\n" + - "97KfZ0d66tcZIhslVpZW06+lXuwMyjjjExe32fAkPFnYyTNORljyYlb/RDSkh7Ke\n" + - "Q+48fLR2kitV0WyRZ+d9cMfx2+D2gWYiaFGek9SrhI8L+nNd4UKvM4K4sSq4JHYf\n" + - "DCxGPWYOaTculuX8dfDh3ftHbrmL2Ca7Iv4NB0kSduG8Gin2OWyeSIPIwpF2ci9g\n" + - "cIBssAYhmS88FQit5pW9z2RZ/e9XmYIP++kz3/EdI6DqkiPUv1fiHTrJBC93LvVg\n" + - "pq75h9RNFuUlqR09SVuB/uZB6tYgv77vy5lPFo+wmLjM41aS4+qI1hBI3Ym4XTc1\n" + - "spPA0sEHtQTQ/xRNYqGpwunJniMF3ukWpOB6UNvQld+p2lj8czexhEAcne1cjey/\n" + - "f0/WUnluSt0HIg8Mnd7s0ukBhb4YxjvARjuqi6PikGz4JPshRwB8dPtS9FQiRxL7\n" + - "obaPHXlmLwohEtT3akzoIj/9C3Y7qnfreSllDgRDxRVFPXy5QnQqpsTy2JuJ4cvo\n" + - "p55RE2kyJ3vBZlB6T53pSgC00hQnNxoqgy7aejRItlec7zx5DnEg8t4rA7LYEGLT\n" + - "MBLWbTRc/njH6GTyc/3x7j9k8V83exqpF6fXrE3GP1C3fBxHY2S9/5BFAlzimplz\n" + - "Mow4S15D04EllRRk6f9HKY598xS4QlDEW/f3utwkQ8+/lNqesVuV8n76WDldMv2O\n" + - "5gTqAZ/pKhDKRLY6km4B2+2IAt2zg+V141wryHJgE/4VyUbu7zZxDIcDouuATQvt\n" + - "wNMnntqy3NTbM7DefSiYe9IUsTUz/g0VQJikoJx+rdX6YzQnRk/cmwvELnskQjSk\n" + - "aGd92A4ousaM299IOkbpLvFaJdrs7cLH0rEQTG5S3tRJSLEnjr94BUVtpIhQDo3i\n" + - "455UahKcCx/KhyIzo+8OdH0TYZf5ZFGLdTrqgi0ybAHcLrXkM+g2JOsst99CeRUq\n" + - "f/T4oFvuDSlLU56iWlLVE7gvDBibXfWIJ65YBHY4ueEzBC/3xOVj+dmTM2JfUSX7\n" + - "mqD25NaDCOuN4WhJmZHC1wyipj3KYT2bLg4gasHr/LvEI+Df/DREdXtrYAqPqZYU\n" + - "0QuubMF4n3hMqmu2wA==\n" + - "=fMRM\n" + - "-----END PGP MESSAGE-----"; - - @Test - public void testMessageDecryptionAndVerification() throws PGPException, IOException { - ImplementationFactory.setFactoryImplementation(new JceImplementationFactory()); - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KLEOPATRA_SECKEY); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(KLEOPATRA_PUBKEY); - - ConsumerOptions options = new ConsumerOptions() - .addDecryptionKey(secretKeys) - .addVerificationCert(publicKeys); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(KLEOPATRA_MESSAGE.getBytes(StandardCharsets.UTF_8))) - .withOptions(options); - - Streams.pipeAll(decryptionStream, out); - decryptionStream.close(); - } - - @Test - public void testEncryptAndSignMessage() throws PGPException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KLEOPATRA_SECKEY); - PGPPublicKeyRing publicKeys = PGPainless.readKeyRing().publicKeyRing(KLEOPATRA_PUBKEY); - - ProducerOptions options = ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications() - .addRecipient(publicKeys) - .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_128), - SigningOptions.get() - .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, DocumentSignatureType.BINARY_DOCUMENT) - ); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(out) - .withOptions(options); - - ByteArrayInputStream in = new ByteArrayInputStream("Hallo, Welt!\n\n".getBytes(StandardCharsets.UTF_8)); - Streams.pipeAll(in, encryptionStream); - encryptionStream.close(); - } - - @Test - public void testMessageInspection() throws PGPException, IOException { - MessageInspector.EncryptionInfo info = MessageInspector.determineEncryptionInfoForMessage( - new ByteArrayInputStream(KLEOPATRA_MESSAGE.getBytes(StandardCharsets.UTF_8))); - } -} From 620c1fc96a55c04d9607632b6967f66105681e55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 8 Nov 2023 15:16:41 +0100 Subject: [PATCH 195/351] Ensure proper compatibility with keys with missing direct-key or certification self-sigs --- .../org/pgpainless/key/info/KeyAccessor.kt | 6 +- .../org/pgpainless/key/info/KeyRingInfo.kt | 3 + .../pgpainless/key/KeyWithoutSelfSigsTest.kt | 110 ++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index a6891f0f..11fe1643 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -67,9 +67,11 @@ abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: S info.getLatestUserIdCertification(userId).let { if (it != null) return it } } - return checkNotNull(info.latestDirectKeySelfSignature) { - "No valid signature found." + if (info.latestDirectKeySelfSignature != null) { + return info.latestDirectKeySelfSignature } + + return info.getCurrentSubkeyBindingSignature(key.subkeyId)!! } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 9f0bbc87..ea5de18d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -172,8 +172,11 @@ class KeyRingInfo( primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { + /* throw NoSuchElementException( "No direct-key signature and no user-id signature found.") + */ + return null } if (directKeyExpirationDate != null && userIdExpirationDate == null) { return directKeyExpirationDate diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt new file mode 100644 index 00000000..bb3e2bd4 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.io.ByteArrayOutputStream +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.junit.jupiter.api.Test +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.key.protection.SecretKeyRingProtector + +class KeyWithoutSelfSigsTest { + + @Test + fun signAndVerify() { + val key = PGPainless.readKeyRing().secretKeyRing(KEY) + val cert = PGPainless.extractCertificate(key!!) + + val ciphertextOut = ByteArrayOutputStream() + val encryptionStream = + PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.signAndEncrypt( + EncryptionOptions.encryptCommunications().addRecipient(cert), + SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), key))) + encryptionStream.write("Hello, World!\n".toByteArray()) + encryptionStream.close() + + val plaintextOut = ByteArrayOutputStream() + val decryptionStream = + PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextOut.toByteArray().inputStream()) + .withOptions( + ConsumerOptions.get() + .addVerificationCert(cert) + .addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys())) + Streams.pipeAll(decryptionStream, plaintextOut) + decryptionStream.close() + } + + fun generateKey() { + val key = + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))) + .addSubkey( + KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey( + KeySpec.getBuilder( + KeyType.XDH(XDHSpec._X25519), + KeyFlag.ENCRYPT_STORAGE, + KeyFlag.ENCRYPT_COMMS)) + .build() + .let { + var cert = PGPainless.extractCertificate(it) + cert = + PGPPublicKeyRing( + buildList { + val iterator = cert.publicKeys + val primaryKey = iterator.next() + add( + PGPPublicKey.removeCertification( + primaryKey, primaryKey.signatures.next())) + while (iterator.hasNext()) { + add(iterator.next()) + } + }) + PGPSecretKeyRing.replacePublicKeys(it, cert) + } + println(PGPainless.asciiArmor(key)) + } + + companion object { + + const val KEY = + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA3E CC77 1CD6 46F0 C6C4 4FDA 86A3 7B22 7802 2FC7\n" + + "\n" + + "lFgEZUuWuhYJKwYBBAHaRw8BAQdAuXfarON/+UG1qwhVy4/VCYuEb9iLFLb8KGQt\n" + + "KfX4Se0AAQDgqGHsb2M43F+6wK5Hla+oZzFkTUsBx8HMpRx2yeQT6hFAnFgEZUuW\n" + + "uhYJKwYBBAHaRw8BAQdAx0OHISLtekltdUVGGrG/Gs3asc/jG/nqCkBEZ5uyELwA\n" + + "AP0faf8bprP3fj248/NacfynKEVnjzc1gocfhGiWrnVgAxC1iNUEGBYKAH0FAmVL\n" + + "lroCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJlS5a6AAoJED9gFx9r\n" + + "B25syqoA/0JR3Zcs6fHQ0jW7+u6330SD5h8WvG78IKsE6AfChBLXAP4hlXGidztq\n" + + "5sOHEQvXD2KPCHEJ6MuQ+rbNSSf0fQhgDwAKCRCGo3sieAIvxzmIAP9+9vRoevUM\n" + + "luQhZzQ7DgYqTCyNkeq2cpVgOfa0lyVDgwEApwrd5DlU3GorGHAQHFS6jhw1IOoG\n" + + "FGQ3zpWaOXd7XwKcXQRlS5a6EgorBgEEAZdVAQUBAQdAZIY7ISyNzp0oMoK0dgb8\n" + + "dX6t/i4Uh+l0jnxM0Z1dEB8DAQgHAAD/fhL5dzdJQ7hFhr78AmDEZKFE4txZFPvd\n" + + "ZVFvIWTthFgQ5Ih1BBgWCgAdBQJlS5a6Ap4BApsMBRYCAwEABAsJCAcFFQoJCAsA\n" + + "CgkQhqN7IngCL8cIGgEAzydjTfKvdrTvzXXu97j8TAoOxk89QnLqsM6BU0VsVmkA\n" + + "/1IzH+PXgPPW9ff+elxTi2NWmK+P033P6i5b5Jdf41YD\n" + + "=GBVS\n" + + "-----END PGP PRIVATE KEY BLOCK-----" + } +} From 3bb25a62a2eddebae7282f96fd5ffe38336cf49f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 13 Nov 2023 14:09:42 +0100 Subject: [PATCH 196/351] Remove unused CRCingArmoredInputStreamWrapper class --- .../util/CRCingArmoredInputStreamWrapper.java | 155 ------------------ .../util/ArmoredInputStreamFactory.kt | 16 +- .../util/ArmoredInputStreamFactoryTest.java | 20 --- 3 files changed, 7 insertions(+), 184 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java deleted file mode 100644 index d2393be3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/util/CRCingArmoredInputStreamWrapper.java +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.util; - -import java.io.IOException; -import java.io.InputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; - -import javax.annotation.Nonnull; - -/** - * Utility class that causes read(bytes, offset, length) to properly throw exceptions - * caused by faulty CRC checksums. - * - * Furthermore, this class swallows exceptions from BC's ArmoredInputStream that are caused - * by missing CRC checksums. - * - * TODO: Validate whether this class is still needed. - */ -public class CRCingArmoredInputStreamWrapper extends ArmoredInputStream { - - private final ArmoredInputStream inputStream; - - public CRCingArmoredInputStreamWrapper(ArmoredInputStream inputStream) throws IOException { - super(inputStream, false); - this.inputStream = inputStream; - } - - @Override - public boolean isClearText() { - return inputStream.isClearText(); - } - - @Override - public boolean isEndOfStream() { - return inputStream.isEndOfStream(); - } - - @Override - public String getArmorHeaderLine() { - return inputStream.getArmorHeaderLine(); - } - - @Override - public String[] getArmorHeaders() { - return inputStream.getArmorHeaders(); - } - - @Override - public int read() throws IOException { - try { - return inputStream.read(); - } catch (IOException e) { - if (e.getMessage().equals("no crc found in armored message.") || e.getMessage().equals("crc check not found.")) { - // swallow exception - return -1; - } else { - throw e; - } - } - } - - @Override - public int read(@Nonnull byte[] b) throws IOException { - return read(b, 0, b.length); - } - /** - * Reads up to len bytes of data from the input stream into - * an array of bytes. An attempt is made to read as many as - * len bytes, but a smaller number may be read. - * The number of bytes actually read is returned as an integer. - * - * The first byte read is stored into element b[off], the - * next one into b[off+1], and so on. The number of bytes read - * is, at most, equal to len. - * - * NOTE: We need to override the custom behavior of Java's {@link InputStream#read(byte[], int, int)}, - * as the upstream method silently swallows {@link IOException IOExceptions}. - * This would cause CRC checksum errors to go unnoticed. - * - * @see Related BC bug report - * @param b byte array - * @param off offset at which we start writing data to the array - * @param len number of bytes we write into the array - * @return total number of bytes read into the buffer - * - * @throws IOException if an exception happens AT ANY POINT - */ - @Override - public int read(byte[] b, int off, int len) throws IOException { - checkIndexSize(b.length, off, len); - - if (len == 0) { - return 0; - } - - int c = read(); - if (c == -1) { - return -1; - } - b[off] = (byte) c; - - int i = 1; - for (; i < len ; i++) { - c = read(); - if (c == -1) { - break; - } - b[off + i] = (byte) c; - } - return i; - } - - private void checkIndexSize(int size, int off, int len) { - if (off < 0 || len < 0) { - throw new IndexOutOfBoundsException("Offset and length cannot be negative."); - } - if (size < off + len) { - throw new IndexOutOfBoundsException("Invalid offset and length."); - } - } - - @Override - public long skip(long n) throws IOException { - return inputStream.skip(n); - } - - @Override - public int available() throws IOException { - return inputStream.available(); - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - - @Override - public synchronized void mark(int readlimit) { - inputStream.mark(readlimit); - } - - @Override - public synchronized void reset() throws IOException { - inputStream.reset(); - } - - @Override - public boolean markSupported() { - return inputStream.markSupported(); - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt index 8198214d..b1956cf6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmoredInputStreamFactory.kt @@ -29,16 +29,14 @@ class ArmoredInputStreamFactory { @Throws(IOException::class) fun get(inputStream: InputStream, options: ConsumerOptions? = null): ArmoredInputStream { return when (inputStream) { - is CRCingArmoredInputStreamWrapper -> inputStream - is ArmoredInputStream -> CRCingArmoredInputStreamWrapper(inputStream) + is ArmoredInputStream -> inputStream else -> - CRCingArmoredInputStreamWrapper( - ArmoredInputStream.builder() - .apply { - setParseForHeaders(true) - options?.let { setIgnoreCRC(it.isDisableAsciiArmorCRC) } - } - .build(inputStream)) + ArmoredInputStream.builder() + .apply { + setParseForHeaders(true) + options?.let { setIgnoreCRC(it.isDisableAsciiArmorCRC) } + } + .build(inputStream) } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java index 147b957d..95e3961b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/ArmoredInputStreamFactoryTest.java @@ -11,8 +11,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; public class ArmoredInputStreamFactoryTest { @@ -30,22 +28,4 @@ public class ArmoredInputStreamFactoryTest { ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(inputStream); assertNotNull(armorIn); } - - @Test - public void testGet_willWrapArmoredInputStreamWithCRC() throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); - ArmoredInputStream plainArmor = new ArmoredInputStream(inputStream); - - ArmoredInputStream armor = ArmoredInputStreamFactory.get(plainArmor); - assertTrue(armor instanceof CRCingArmoredInputStreamWrapper); - } - - @Test - public void testGet_onCRCinArmoredInputStream() throws IOException { - ByteArrayInputStream inputStream = new ByteArrayInputStream(armored.getBytes()); - CRCingArmoredInputStreamWrapper crc = new CRCingArmoredInputStreamWrapper(new ArmoredInputStream(inputStream)); - - ArmoredInputStream armor = ArmoredInputStreamFactory.get(crc); - assertSame(crc, armor); - } } From f07063d55f3216366b141762dec12fa2b9f0cb29 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 13 Nov 2023 16:21:08 +0100 Subject: [PATCH 197/351] Kotlin conversion: SignatureBuilder classes --- .../builder/AbstractSignatureBuilder.java | 140 ------------------ .../DirectKeySelfSignatureBuilder.java | 57 ------- .../PrimaryKeyBindingSignatureBuilder.java | 59 -------- .../builder/RevocationSignatureBuilder.java | 71 --------- .../builder/SelfSignatureBuilder.java | 74 --------- .../SubkeyBindingSignatureBuilder.java | 72 --------- ...irdPartyCertificationSignatureBuilder.java | 119 --------------- .../ThirdPartyDirectKeySignatureBuilder.java | 53 ------- .../builder/UniversalSignatureBuilder.java | 55 ------- .../signature/builder/package-info.java | 8 - .../pgpainless/signature/package-info.java | 8 - .../org/pgpainless/algorithm/SignatureType.kt | 4 +- .../secretkeyring/SecretKeyRingEditor.kt | 11 +- .../builder/AbstractSignatureBuilder.kt | 135 +++++++++++++++++ .../builder/DirectKeySelfSignatureBuilder.kt | 50 +++++++ .../PrimaryKeyBindingSignatureBuilder.kt | 61 ++++++++ .../builder/RevocationSignatureBuilder.kt | 70 +++++++++ .../signature/builder/SelfSignatureBuilder.kt | 69 +++++++++ .../builder/SubkeyBindingSignatureBuilder.kt | 73 +++++++++ ...ThirdPartyCertificationSignatureBuilder.kt | 117 +++++++++++++++ .../ThirdPartyDirectKeySignatureBuilder.kt | 57 +++++++ .../builder/UniversalSignatureBuilder.kt | 52 +++++++ ...irdPartyDirectKeySignatureBuilderTest.java | 6 +- 23 files changed, 693 insertions(+), 728 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java deleted file mode 100644 index 1079f92f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; -import org.pgpainless.signature.subpackets.SignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper; - -public abstract class AbstractSignatureBuilder> { - protected final PGPPrivateKey privateSigningKey; - protected final PGPPublicKey publicSigningKey; - - protected HashAlgorithm hashAlgorithm; - protected SignatureType signatureType; - - protected SignatureSubpackets unhashedSubpackets; - protected SignatureSubpackets hashedSubpackets; - - protected AbstractSignatureBuilder(SignatureType signatureType, - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - HashAlgorithm hashAlgorithm, - SignatureSubpackets hashedSubpackets, - SignatureSubpackets unhashedSubpackets) - throws PGPException { - if (!isValidSignatureType(signatureType)) { - throw new IllegalArgumentException("Invalid signature type."); - } - this.signatureType = signatureType; - this.privateSigningKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); - this.publicSigningKey = signingKey.getPublicKey(); - this.hashAlgorithm = hashAlgorithm; - this.hashedSubpackets = hashedSubpackets; - this.unhashedSubpackets = unhashedSubpackets; - } - - public AbstractSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - this( - signatureType, - signingKey, - protector, - negotiateHashAlgorithm(signingKey.getPublicKey()), - SignatureSubpackets.createHashedSubpackets(signingKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets() - ); - } - - public AbstractSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - this( - SignatureType.valueOf(archetypeSignature.getSignatureType()), - certificationKey, - protector, - negotiateHashAlgorithm(certificationKey.getPublicKey()), - SignatureSubpackets.refreshHashedSubpackets(certificationKey.getPublicKey(), archetypeSignature), - SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature) - ); - } - - /** - * Negotiate a {@link HashAlgorithm} to be used when creating the signature. - * - * @param publicKey signing public key - * @return hash algorithm - */ - protected static HashAlgorithm negotiateHashAlgorithm(PGPPublicKey publicKey) { - Set hashAlgorithmPreferences = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey); - return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) - .negotiateHashAlgorithm(hashAlgorithmPreferences); - } - - public B overrideHashAlgorithm(@Nonnull HashAlgorithm hashAlgorithm) { - this.hashAlgorithm = hashAlgorithm; - return (B) this; - } - - /** - * Set the builders {@link SignatureType}. - * Note that only those types who are valid for the concrete subclass of this {@link AbstractSignatureBuilder} - * are allowed. Invalid choices result in an {@link IllegalArgumentException} to be thrown. - * - * @param type signature type - * @return builder - */ - public B setSignatureType(SignatureType type) { - if (!isValidSignatureType(type)) { - throw new IllegalArgumentException("Invalid signature type: " + type); - } - this.signatureType = type; - return (B) this; - } - - /** - * Build an instance of {@link PGPSignatureGenerator} initialized with the signing key - * and with hashed and unhashed subpackets. - * - * @return pgp signature generator - * - * @throws PGPException if the signature generator cannot be initialized - */ - protected PGPSignatureGenerator buildAndInitSignatureGenerator() throws PGPException { - PGPSignatureGenerator generator = new PGPSignatureGenerator( - ImplementationFactory.getInstance().getPGPContentSignerBuilder( - publicSigningKey.getAlgorithm(), hashAlgorithm.getAlgorithmId() - ) - ); - generator.setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(unhashedSubpackets)); - generator.setHashedSubpackets(SignatureSubpacketsHelper.toVector(hashedSubpackets)); - generator.init(signatureType.getCode(), privateSigningKey); - return generator; - } - - /** - * Return true if the given {@link SignatureType} is a valid choice for the concrete implementation - * of {@link AbstractSignatureBuilder}. - * - * @param type type - * @return return true if valid, false otherwise - */ - protected abstract boolean isValidSignatureType(SignatureType type); -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java deleted file mode 100644 index cf99f8d6..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; - -public class DirectKeySelfSignatureBuilder extends AbstractSignatureBuilder { - - public DirectKeySelfSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - public DirectKeySelfSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(SignatureType.DIRECT_KEY, signingKey, protector); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey key) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (key.getKeyID() != publicSigningKey.getKeyID()) { - return signatureGenerator.generateCertification(publicSigningKey, key); - } else { - return signatureGenerator.generateCertification(key); - } - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.DIRECT_KEY; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java deleted file mode 100644 index 156e739f..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; - -public class PrimaryKeyBindingSignatureBuilder extends AbstractSignatureBuilder { - - public PrimaryKeyBindingSignatureBuilder(PGPSecretKey subkey, SecretKeyRingProtector subkeyProtector) - throws PGPException { - super(SignatureType.PRIMARYKEY_BINDING, subkey, subkeyProtector); - } - - public PrimaryKeyBindingSignatureBuilder(PGPSecretKey secretSubKey, - SecretKeyRingProtector subkeyProtector, - HashAlgorithm hashAlgorithm) - throws PGPException { - super(SignatureType.PRIMARYKEY_BINDING, secretSubKey, subkeyProtector, hashAlgorithm, - SignatureSubpackets.createHashedSubpackets(secretSubKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets()); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.PRIMARYKEY_BINDING; - } - - public PGPSignature build(PGPPublicKey primaryKey) throws PGPException { - return buildAndInitSignatureGenerator() - .generateCertification(primaryKey, publicSigningKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java deleted file mode 100644 index 3c2dcab9..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets; - -public class RevocationSignatureBuilder extends AbstractSignatureBuilder { - - public RevocationSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(signatureType, signingKey, protector); - getHashedSubpackets().setRevocable(false); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - switch (type) { - case KEY_REVOCATION: - case SUBKEY_REVOCATION: - case CERTIFICATION_REVOCATION: - return true; - default: - return false; - } - } - - public RevocationSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public RevocationSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable RevocationSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey revokeeSubkey) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (signatureType == SignatureType.KEY_REVOCATION) { - if (!revokeeSubkey.isMasterKey()) { - throw new IllegalArgumentException("Signature type is KEY_REVOCATION, but provided revokeeSubkey does not appear to be a primary key."); - } - return signatureGenerator.generateCertification(publicSigningKey); - } else { - return signatureGenerator.generateCertification(publicSigningKey, revokeeSubkey); - } - } - - public PGPSignature build(String revokeeUserId) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - if (signatureType != SignatureType.CERTIFICATION_REVOCATION) { - throw new IllegalArgumentException("Signature type is != CERTIFICATION_REVOCATION."); - } - return signatureGenerator.generateCertification(revokeeUserId, publicSigningKey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java deleted file mode 100644 index e6bf94c3..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SelfSignatureBuilder.java +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; - -public class SelfSignatureBuilder extends AbstractSignatureBuilder { - - public SelfSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - this(SignatureType.GENERIC_CERTIFICATION, signingKey, protector); - } - - public SelfSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(signatureType, signingKey, protector); - } - - public SelfSignatureBuilder( - PGPSecretKey primaryKey, - SecretKeyRingProtector primaryKeyProtector, - PGPSignature oldCertification) - throws PGPException { - super(primaryKey, primaryKeyProtector, oldCertification); - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey certifiedKey, String userId) throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userId, certifiedKey); - } - - public PGPSignature build(PGPPublicKey certifiedKey, PGPUserAttributeSubpacketVector userAttribute) - throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userAttribute, certifiedKey); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - case DIRECT_KEY: - return true; - default: - return false; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java deleted file mode 100644 index c15e219e..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; -import org.pgpainless.signature.subpackets.SignatureSubpackets; - -public class SubkeyBindingSignatureBuilder extends AbstractSignatureBuilder { - - public SubkeyBindingSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(SignatureType.SUBKEY_BINDING, signingKey, protector); - } - - public SubkeyBindingSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector, HashAlgorithm hashAlgorithm) - throws PGPException { - super(SignatureType.SUBKEY_BINDING, signingKey, protector, hashAlgorithm, - SignatureSubpackets.createHashedSubpackets(signingKey.getPublicKey()), - SignatureSubpackets.createEmptySubpackets()); - } - - public SubkeyBindingSignatureBuilder( - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - PGPSignature oldSubkeyBinding) - throws PGPException { - super(signingKey, protector, requireValidSignatureType(oldSubkeyBinding)); - } - - private static PGPSignature requireValidSignatureType(PGPSignature signature) { - if (signature.getSignatureType() == SignatureType.SUBKEY_BINDING.getCode()) { - return signature; - } - throw new IllegalArgumentException("Invalid signature type."); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.SUBKEY_BINDING; - } - - public SelfSignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SelfSignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SelfSignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey subkey) throws PGPException { - return buildAndInitSignatureGenerator() - .generateCertification(publicSigningKey, subkey); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java deleted file mode 100644 index d75e269d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.CertificationSubpackets; - -/** - * Certification signature builder used to certify other users keys. - */ -public class ThirdPartyCertificationSignatureBuilder extends AbstractSignatureBuilder { - - /** - * Create a new certification signature builder. - * This constructor uses {@link SignatureType#GENERIC_CERTIFICATION} as signature type. - * - * @param signingKey our own certification key - * @param protector protector to unlock the certification key - * @throws WrongPassphraseException in case of a wrong passphrase - */ - public ThirdPartyCertificationSignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - this(SignatureType.GENERIC_CERTIFICATION, signingKey, protector); - } - - /** - * Create a new certification signature builder. - * - * @param signatureType type of certification - * @param signingKey our own certification key - * @param protector protector to unlock the certification key - * @throws WrongPassphraseException in case of a wrong passphrase - */ - public ThirdPartyCertificationSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(signatureType, signingKey, protector); - } - - /** - * Create a new certification signature builder. - * - * @param signingKey our own certification key - * @param protector protector to unlock the certification key - * @param archetypeSignature signature to use as a template for the new signature - * @throws WrongPassphraseException in case of a wrong passphrase - */ - public ThirdPartyCertificationSignatureBuilder( - PGPSecretKey signingKey, - SecretKeyRingProtector protector, - PGPSignature archetypeSignature) - throws PGPException { - super(signingKey, protector, archetypeSignature); - } - - public CertificationSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public CertificationSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable CertificationSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - /** - * Create a certification signature for the given user-id and the primary key of the given key ring. - * @param certifiedKey key ring - * @param userId user-id to certify - * @return signature - * - * @throws PGPException if the signature generator cannot be initialized - */ - public PGPSignature build(PGPPublicKeyRing certifiedKey, String userId) throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userId, certifiedKey.getPublicKey()); - } - - /** - * Create a certification signature for the given user attribute and the primary key of the given key ring. - * @param certifiedKey key ring - * @param userAttribute user-attributes to certify - * @return signature - * - * @throws PGPException if the signature generator cannot be initialized - */ - public PGPSignature build(PGPPublicKeyRing certifiedKey, PGPUserAttributeSubpacketVector userAttribute) - throws PGPException { - return buildAndInitSignatureGenerator().generateCertification(userAttribute, certifiedKey.getPublicKey()); - } - - @Override - protected boolean isValidSignatureType(@Nonnull SignatureType type) { - switch (type) { - case GENERIC_CERTIFICATION: - case NO_CERTIFICATION: - case CASUAL_CERTIFICATION: - case POSITIVE_CERTIFICATION: - return true; - default: - return false; - } - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java deleted file mode 100644 index 51a14052..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.CertificationSubpackets; - -public class ThirdPartyDirectKeySignatureBuilder extends AbstractSignatureBuilder { - - public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - public ThirdPartyDirectKeySignatureBuilder(PGPSecretKey signingKey, SecretKeyRingProtector protector) throws PGPException { - super(SignatureType.DIRECT_KEY, signingKey, protector); - } - - public CertificationSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public CertificationSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable CertificationSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignature build(PGPPublicKey key) throws PGPException { - PGPSignatureGenerator signatureGenerator = buildAndInitSignatureGenerator(); - return signatureGenerator.generateCertification(key); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return type == SignatureType.DIRECT_KEY; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java deleted file mode 100644 index 7ca512bf..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/UniversalSignatureBuilder.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.signature.builder; - -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureGenerator; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.subpackets.SignatureSubpackets; - -/** - * Signature builder without restrictions on subpacket contents. - */ -public class UniversalSignatureBuilder extends AbstractSignatureBuilder { - - public UniversalSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) - throws PGPException { - super(signatureType, signingKey, protector); - } - - public UniversalSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) - throws PGPException { - super(certificationKey, protector, archetypeSignature); - } - - @Override - protected boolean isValidSignatureType(SignatureType type) { - return true; - } - - public SignatureSubpackets getHashedSubpackets() { - return hashedSubpackets; - } - - public SignatureSubpackets getUnhashedSubpackets() { - return unhashedSubpackets; - } - - public void applyCallback(@Nullable SignatureSubpackets.Callback callback) { - if (callback != null) { - callback.modifyHashedSubpackets(getHashedSubpackets()); - callback.modifyUnhashedSubpackets(getUnhashedSubpackets()); - } - } - - public PGPSignatureGenerator getSignatureGenerator() throws PGPException { - return buildAndInitSignatureGenerator(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java deleted file mode 100644 index c82eb0ec..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature.builder; diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java deleted file mode 100644 index 624a2e24..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes related to OpenPGP signatures. - */ -package org.pgpainless.signature; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt index 865549b8..3cca8655 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt @@ -157,7 +157,9 @@ enum class SignatureType(val code: Int) { * @throws IllegalArgumentException in case of an unmatched signature type code */ @JvmStatic - @Deprecated("Deprecated in favor of requireFromCode", ReplaceWith("requireFromCode")) + @Deprecated( + "Deprecated in favor of requireFromCode", + ReplaceWith("SignatureType.requireFromCode(code)")) fun valueOf(code: Int): SignatureType { try { return requireFromCode(code) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 891d64e7..7dff25f7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -84,10 +84,7 @@ class SecretKeyRingEditor( } builder.applyCallback(callback) secretKeyRing = - injectCertification( - secretKeyRing, - sanitizedUserId, - builder.build(primaryKey.publicKey, sanitizedUserId)) + injectCertification(secretKeyRing, sanitizedUserId, builder.build(sanitizedUserId)) return this } @@ -620,7 +617,7 @@ class SecretKeyRingEditor( hashedSubpackets.setPrimaryUserId(null) } }) - return builder.build(secretKeyRing.publicKey, userId) + return builder.build(userId) } @Throws(PGPException::class) @@ -648,7 +645,7 @@ class SecretKeyRingEditor( } }) } - .build(secretKeyRing.publicKey, primaryUserId) + .build(primaryUserId) } @Throws(PGPException::class) @@ -675,7 +672,7 @@ class SecretKeyRingEditor( } }) } - .build(secretKeyRing.publicKey) + .build() } private fun selectUserIds(predicate: Predicate): List = diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt new file mode 100644 index 00000000..eaf05df1 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/AbstractSignatureBuilder.kt @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.key.util.OpenPgpKeyAttributeUtil +import org.pgpainless.signature.subpackets.SignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper + +abstract class AbstractSignatureBuilder>( + protected val privateSigningKey: PGPPrivateKey, + protected val publicSigningKey: PGPPublicKey, + protected var _hashAlgorithm: HashAlgorithm, + protected var _signatureType: SignatureType, + protected val _hashedSubpackets: SignatureSubpackets, + protected val _unhashedSubpackets: SignatureSubpackets +) { + + protected abstract val signatureTypePredicate: Predicate + + init { + require(signatureTypePredicate.test(_signatureType)) { "Invalid signature type." } + } + + @Throws(PGPException::class) + protected constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm, + hashedSubpackets: SignatureSubpackets, + unhashedSubpackets: SignatureSubpackets + ) : this( + UnlockSecretKey.unlockSecretKey(signingKey, protector), + signingKey.publicKey, + hashAlgorithm, + signatureType, + hashedSubpackets, + unhashedSubpackets) + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : this( + signatureType, + signingKey, + protector, + negotiateHashAlgorithm(signingKey.publicKey), + SignatureSubpackets.createHashedSubpackets(signingKey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : this( + SignatureType.requireFromCode(archetypeSignature.signatureType), + signingKey, + protector, + negotiateHashAlgorithm(signingKey.publicKey), + SignatureSubpackets.refreshHashedSubpackets(signingKey.publicKey, archetypeSignature), + SignatureSubpackets.refreshUnhashedSubpackets(archetypeSignature)) + + val hashAlgorithm = _hashAlgorithm + + fun overrideHashAlgorithm(hashAlgorithm: HashAlgorithm) = + apply { _hashAlgorithm = hashAlgorithm } as B + + /** + * Set the builders [SignatureType]. Note that only those types who are valid for the concrete + * subclass of this [AbstractSignatureBuilder] are allowed. Invalid choices result in an + * [IllegalArgumentException] to be thrown. + * + * @param type signature type + * @return builder + */ + fun setSignatureType(type: SignatureType) = + apply { + require(signatureTypePredicate.test(type)) { "Invalid signature type: $type" } + _signatureType = type + } + as B + + /** + * Build an instance of [PGPSignatureGenerator] initialized with the signing key and with hashed + * and unhashed subpackets. + * + * @return pgp signature generator + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + protected fun buildAndInitSignatureGenerator(): PGPSignatureGenerator = + PGPSignatureGenerator( + ImplementationFactory.getInstance() + .getPGPContentSignerBuilder( + publicSigningKey.algorithm, hashAlgorithm.algorithmId)) + .apply { + setUnhashedSubpackets(SignatureSubpacketsHelper.toVector(_unhashedSubpackets)) + setHashedSubpackets(SignatureSubpacketsHelper.toVector(_hashedSubpackets)) + init(_signatureType.code, privateSigningKey) + } + + companion object { + + /** + * Negotiate a [HashAlgorithm] to be used when creating the signature. + * + * @param publicKey signing public key + * @return hash algorithm + */ + @JvmStatic + fun negotiateHashAlgorithm(publicKey: PGPPublicKey): HashAlgorithm = + HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm( + OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey)) + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt new file mode 100644 index 00000000..c4d11ea9 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/DirectKeySelfSignatureBuilder.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to direct-key self-signatures. Direct-key self-signatures are + * calculated by a primary-key over itself. + */ +class DirectKeySelfSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.DIRECT_KEY } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.DIRECT_KEY, signingKey, protector) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(): PGPSignature = + buildAndInitSignatureGenerator().let { it.generateCertification(publicSigningKey) } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt new file mode 100644 index 00000000..f34d37a7 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/PrimaryKeyBindingSignatureBuilder.kt @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * [AbstractSignatureBuilder] subclass devoted to build primary-key binding-signatures. Those + * signatures (also called "back-signatures") are binding signatures issued by signing-capable + * subkeys. + */ +class PrimaryKeyBindingSignatureBuilder : + AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.PRIMARYKEY_BINDING } + + @Throws(PGPException::class) + constructor( + signingSubkey: PGPSecretKey, + subkeyProtector: SecretKeyRingProtector + ) : super(SignatureType.PRIMARYKEY_BINDING, signingSubkey, subkeyProtector) + + @Throws(PGPException::class) + constructor( + signingSubkey: PGPSecretKey, + subkeyProtector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm + ) : super( + SignatureType.PRIMARYKEY_BINDING, + signingSubkey, + subkeyProtector, + hashAlgorithm, + SignatureSubpackets.createHashedSubpackets(signingSubkey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(primaryKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(primaryKey, publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt new file mode 100644 index 00000000..d8c594fe --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets + +/** [AbstractSignatureBuilder] subclass devoted to revocation signatures. */ +class RevocationSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.KEY_REVOCATION, + SignatureType.SUBKEY_REVOCATION, + SignatureType.CERTIFICATION_REVOCATION) + } + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) { + hashedSubpackets.setRevocable(false) + } + + val hashedSubpackets: RevocationSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: RevocationSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: RevocationSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(revokeeKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().let { + if (_signatureType == SignatureType.KEY_REVOCATION) { + require(revokeeKey.isMasterKey) { + "Signature type is KEY_REVOCATION, but provided revokee does not appear to be a primary key." + } + it.generateCertification(publicSigningKey) + } else { + it.generateCertification(publicSigningKey, revokeeKey) + } + } + + @Throws(PGPException::class) + fun build(revokeeUserId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator() + .also { + require(_signatureType == SignatureType.CERTIFICATION_REVOCATION) { + "Signature type is != CERTIFICATION_REVOCATION." + } + } + .generateCertification(revokeeUserId.toString(), publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt new file mode 100644 index 00000000..370479c8 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SelfSignatureBuilder.kt @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to all types of self-certifications. Self-certifications are + * certifications calculated by a primary key over its own user-ids. + */ +class SelfSignatureBuilder : AbstractSignatureBuilder { + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION) + } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + @Throws(PGPException::class) + constructor( + primaryKey: PGPSecretKey, + primaryKeyProtector: SecretKeyRingProtector, + oldCertification: PGPSignature + ) : super(primaryKey, primaryKeyProtector, oldCertification) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(userId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userId.toString(), publicSigningKey) + + @Throws(PGPException::class) + fun build(userAttributes: PGPUserAttributeSubpacketVector): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userAttributes, publicSigningKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt new file mode 100644 index 00000000..6e2694e3 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * [AbstractSignatureBuilder] devoted to generating subkey binding signatures. A subkey binding + * signature is calculated by a primary key over a subkey. + */ +class SubkeyBindingSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.SUBKEY_BINDING } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.SUBKEY_BINDING, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + hashAlgorithm: HashAlgorithm + ) : super( + SignatureType.SUBKEY_BINDING, + signingKey, + protector, + hashAlgorithm, + SignatureSubpackets.createHashedSubpackets(signingKey.publicKey), + SignatureSubpackets.createEmptySubpackets()) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + oldSubkeyBinding: PGPSignature + ) : super( + signingKey, + protector, + oldSubkeyBinding.also { + require(it.signatureType == SignatureType.SUBKEY_BINDING.code) { + "Invalid signature type." + } + }) + + val hashedSubpackets: SelfSignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SelfSignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SelfSignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(subkey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(publicSigningKey, subkey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt new file mode 100644 index 00000000..be45dc8a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyCertificationSignatureBuilder.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.CertificationSubpackets + +/** + * Certification signature builder used to certify other users keys. A third-party certification is + * calculated by the primary key of the issuer certificate, over a user-id on a third-party + * certificate. + */ +class ThirdPartyCertificationSignatureBuilder : + AbstractSignatureBuilder { + override val signatureTypePredicate: Predicate + get() = + Predicate { + it in + listOf( + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION) + } + + /** + * Create a new certification signature builder. This constructor uses + * [SignatureType.GENERIC_CERTIFICATION] as signature type. + * + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.GENERIC_CERTIFICATION, signingKey, protector) + + /** + * Create a new certification signature builder. + * + * @param signatureType type of certification + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + /** + * Create a new certification signature builder. + * + * @param signingKey our own certification key + * @param protector protector to unlock the certification key + * @param archetypeSignature signature to use as a template for the new signature + * @throws WrongPassphraseException in case of a wrong passphrase + */ + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: CertificationSubpackets = _hashedSubpackets + val unhashedSubpackets: CertificationSubpackets = _unhashedSubpackets + + fun applyCallback(callback: CertificationSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + /** + * Create a certification signature for the given user-id and the given third-party certificate. + * + * @param certificate third-party certificate + * @param userId user-id to certify + * @return signature + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + fun build(certificate: PGPPublicKeyRing, userId: CharSequence): PGPSignature = + buildAndInitSignatureGenerator() + .generateCertification(userId.toString(), certificate.publicKey) + + /** + * Create a certification signature for the given user attribute and the given third-party + * certificate. + * + * @param certificate third-party certificate + * @param userAttribute user-attributes to certify + * @return signature + * @throws PGPException if the signature generator cannot be initialized + */ + @Throws(PGPException::class) + fun build( + certificate: PGPPublicKeyRing, + userAttribute: PGPUserAttributeSubpacketVector + ): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(userAttribute, certificate.publicKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt new file mode 100644 index 00000000..32d677d5 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilder.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.CertificationSubpackets + +/** + * [AbstractSignatureBuilder] subclass devoted to generating direct-key signatures over primary keys + * of third-party certificates. Such signatures are also sometimes referred to as "delegations", + * i.e. in the context of the Web-of-Trust. + */ +class ThirdPartyDirectKeySignatureBuilder : + AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { it == SignatureType.DIRECT_KEY } + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(SignatureType.DIRECT_KEY, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: CertificationSubpackets = _hashedSubpackets + val unhashedSubpackets: CertificationSubpackets = _unhashedSubpackets + + fun applyCallback(callback: CertificationSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + @Throws(PGPException::class) + fun build(certificate: PGPPublicKeyRing): PGPSignature = build(certificate.publicKey) + + @Throws(PGPException::class) + fun build(certifiedKey: PGPPublicKey): PGPSignature = + buildAndInitSignatureGenerator().generateCertification(certifiedKey) +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt new file mode 100644 index 00000000..a1aa57b2 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/UniversalSignatureBuilder.kt @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder + +import java.util.function.Predicate +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.subpackets.SignatureSubpackets + +/** + * Signature builder without restrictions on subpacket contents. Instead of providing a "build" + * method, this builder offers the user to decide on their own, how to generate the signature by + * exposing the [PGPSignatureGenerator] via [signatureGenerator]. + */ +class UniversalSignatureBuilder : AbstractSignatureBuilder { + + override val signatureTypePredicate: Predicate + get() = Predicate { true } + + @Throws(PGPException::class) + constructor( + signatureType: SignatureType, + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector + ) : super(signatureType, signingKey, protector) + + @Throws(PGPException::class) + constructor( + signingKey: PGPSecretKey, + protector: SecretKeyRingProtector, + archetypeSignature: PGPSignature + ) : super(signingKey, protector, archetypeSignature) + + val hashedSubpackets: SignatureSubpackets = _hashedSubpackets + val unhashedSubpackets: SignatureSubpackets = _unhashedSubpackets + + fun applyCallback(callback: SignatureSubpackets.Callback?) = apply { + callback?.let { + it.modifyHashedSubpackets(hashedSubpackets) + it.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + + val signatureGenerator: PGPSignatureGenerator + @Throws(PGPException::class) get() = buildAndInitSignatureGenerator() +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java index 72acc125..56605f83 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ThirdPartyDirectKeySignatureBuilderTest.java @@ -8,8 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.Date; @@ -33,7 +31,7 @@ import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; public class ThirdPartyDirectKeySignatureBuilderTest { @Test - public void testDirectKeySignatureBuilding() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + public void testDirectKeySignatureBuilding() throws PGPException { PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() .modernKeyRing("Alice"); @@ -55,7 +53,7 @@ public class ThirdPartyDirectKeySignatureBuilderTest { } }); - PGPSignature directKeySig = dsb.build(secretKeys.getPublicKey()); + PGPSignature directKeySig = dsb.build(); assertNotNull(directKeySig); secretKeys = KeyRingUtils.injectCertification(secretKeys, secretKeys.getPublicKey(), directKeySig); From 608ec0b7b0f92c36c0c076bcd992644c5ad651c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:39:26 +0100 Subject: [PATCH 198/351] Annotate methods with @Nonnull --- .../java/org/pgpainless/sop/ArmorImpl.java | 11 +++-- .../pgpainless/sop/ChangeKeyPasswordImpl.java | 14 ++++-- .../java/org/pgpainless/sop/DearmorImpl.java | 7 ++- .../java/org/pgpainless/sop/DecryptImpl.java | 32 ++++++++----- .../org/pgpainless/sop/DetachedSignImpl.java | 24 ++++++---- .../pgpainless/sop/DetachedVerifyImpl.java | 17 ++++--- .../java/org/pgpainless/sop/EncryptImpl.java | 45 ++++++++++++------- .../org/pgpainless/sop/ExtractCertImpl.java | 8 +++- .../org/pgpainless/sop/GenerateKeyImpl.java | 16 +++++-- .../org/pgpainless/sop/InlineDetachImpl.java | 10 +++-- .../org/pgpainless/sop/InlineSignImpl.java | 21 ++++++--- .../org/pgpainless/sop/InlineVerifyImpl.java | 16 ++++--- .../org/pgpainless/sop/ListProfilesImpl.java | 8 ++-- .../org/pgpainless/sop/RevokeKeyImpl.java | 14 ++++-- .../main/java/org/pgpainless/sop/SOPImpl.java | 19 ++++++++ .../java/org/pgpainless/sop/VersionImpl.java | 6 +++ 16 files changed, 190 insertions(+), 78 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index daee3a9b..421dc7a7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -18,21 +18,26 @@ import sop.enums.ArmorLabel; import sop.exception.SOPGPException; import sop.operation.Armor; +import javax.annotation.Nonnull; + /** * Implementation of the
armor
operation using PGPainless. */ public class ArmorImpl implements Armor { + @Nonnull @Override - public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { + @Deprecated + public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); } + @Nonnull @Override - public Ready data(InputStream data) throws SOPGPException.BadData { + public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { // By buffering the output stream, we can improve performance drastically BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java index 95377b12..56613720 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java @@ -24,32 +24,38 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.ChangeKeyPassword; +import javax.annotation.Nonnull; + public class ChangeKeyPasswordImpl implements ChangeKeyPassword { private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector(); private Passphrase newPassphrase = Passphrase.emptyPassphrase(); private boolean armor = true; + @Nonnull @Override public ChangeKeyPassword noArmor() { armor = false; return this; } + @Nonnull @Override - public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { + public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)); return this; } + @Nonnull @Override - public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { + public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { this.newPassphrase = Passphrase.fromPassword(newPassphrase); return this; } + @Nonnull @Override - public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected { + public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected { SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase); PGPSecretKeyRingCollection secretKeyRingCollection; try { @@ -76,7 +82,7 @@ public class ChangeKeyPasswordImpl implements ChangeKeyPassword { final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys); return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { if (armor) { ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); changedSecretKeyCollection.encode(armorOut); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index 29483437..5fd0a03b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -15,13 +15,16 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.Dearmor; +import javax.annotation.Nonnull; + /** * Implementation of the
dearmor
operation using PGPainless. */ public class DearmorImpl implements Dearmor { + @Nonnull @Override - public Ready data(InputStream data) { + public Ready data(@Nonnull InputStream data) { InputStream decoder; try { decoder = PGPUtil.getDecoderStream(data); @@ -31,7 +34,7 @@ public class DearmorImpl implements Dearmor { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); Streams.pipeAll(decoder, bufferedOutputStream); bufferedOutputStream.flush(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index d15713ca..bc5081d7 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -7,7 +7,6 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -33,6 +32,9 @@ import sop.SessionKey; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.Decrypt; +import sop.util.UTF8Util; + +import javax.annotation.Nonnull; /** * Implementation of the
decrypt
operation using PGPainless. @@ -42,20 +44,23 @@ public class DecryptImpl implements Decrypt { private final ConsumerOptions consumerOptions = ConsumerOptions.get(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); + @Nonnull @Override - public DecryptImpl verifyNotBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + public DecryptImpl verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { consumerOptions.verifyNotBefore(timestamp); return this; } + @Nonnull @Override - public DecryptImpl verifyNotAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + public DecryptImpl verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { consumerOptions.verifyNotAfter(timestamp); return this; } + @Nonnull @Override - public DecryptImpl verifyWithCert(InputStream certIn) throws SOPGPException.BadData, IOException { + public DecryptImpl verifyWithCert(@Nonnull InputStream certIn) throws SOPGPException.BadData, IOException { PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true); if (certs != null) { consumerOptions.addVerificationCerts(certs); @@ -63,8 +68,9 @@ public class DecryptImpl implements Decrypt { return this; } + @Nonnull @Override - public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { + public DecryptImpl withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption { consumerOptions.setSessionKey( new org.pgpainless.util.SessionKey( SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), @@ -72,8 +78,9 @@ public class DecryptImpl implements Decrypt { return this; } + @Nonnull @Override - public DecryptImpl withPassword(String password) { + public DecryptImpl withPassword(@Nonnull String password) { consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)); String withoutTrailingWhitespace = removeTrailingWhitespace(password); if (!password.equals(withoutTrailingWhitespace)) { @@ -91,8 +98,9 @@ public class DecryptImpl implements Decrypt { return passphrase.substring(0, i); } + @Nonnull @Override - public DecryptImpl withKey(InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { + public DecryptImpl withKey(@Nonnull InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true); for (PGPSecretKeyRing key : secretKeyCollection) { @@ -102,15 +110,17 @@ public class DecryptImpl implements Decrypt { return this; } + @Nonnull @Override - public Decrypt withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); + public Decrypt withKeyPassword(@Nonnull byte[] password) { + String string = new String(password, UTF8Util.UTF8); protector.addPassphrase(Passphrase.fromPassword(string)); return this; } + @Nonnull @Override - public ReadyWithResult ciphertext(InputStream ciphertext) + public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg { @@ -136,7 +146,7 @@ public class DecryptImpl implements Decrypt { return new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { Streams.pipeAll(decryptionStream, outputStream); decryptionStream.close(); MessageMetadata metadata = decryptionStream.getMetadata(); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index c32cb219..034e13bc 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -8,7 +8,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; @@ -35,6 +34,9 @@ import sop.SigningResult; import sop.enums.SignAs; import sop.exception.SOPGPException; import sop.operation.DetachedSign; +import sop.util.UTF8Util; + +import javax.annotation.Nonnull; /** * Implementation of the
sign
operation using PGPainless. @@ -42,7 +44,7 @@ import sop.operation.DetachedSign; public class DetachedSignImpl implements DetachedSign { private boolean armor = true; - private SignAs mode = SignAs.Binary; + private SignAs mode = SignAs.binary; private final SigningOptions signingOptions = SigningOptions.get(); private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private final List signingKeys = new ArrayList<>(); @@ -54,13 +56,15 @@ public class DetachedSignImpl implements DetachedSign { } @Override - public DetachedSign mode(SignAs mode) { + @Nonnull + public DetachedSign mode(@Nonnull SignAs mode) { this.mode = mode; return this; } @Override - public DetachedSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + @Nonnull + public DetachedSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); for (PGPSecretKeyRing key : keys) { KeyRingInfo info = PGPainless.inspectKeyRing(key); @@ -74,14 +78,16 @@ public class DetachedSignImpl implements DetachedSign { } @Override - public DetachedSign withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); + @Nonnull + public DetachedSign withKeyPassword(@Nonnull byte[] password) { + String string = new String(password, UTF8Util.UTF8); protector.addPassphrase(Passphrase.fromPassword(string)); return this; } @Override - public ReadyWithResult data(InputStream data) throws IOException { + @Nonnull + public ReadyWithResult data(@Nonnull InputStream data) throws IOException { for (PGPSecretKeyRing key : signingKeys) { try { signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); @@ -101,7 +107,7 @@ public class DetachedSignImpl implements DetachedSign { return new ReadyWithResult() { @Override - public SigningResult writeTo(OutputStream outputStream) throws IOException { + public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { if (signingStream.isClosed()) { throw new IllegalStateException("EncryptionStream is already closed."); @@ -157,7 +163,7 @@ public class DetachedSignImpl implements DetachedSign { } private static DocumentSignatureType modeToSigType(SignAs mode) { - return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT + return mode == SignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index cdae0215..4c475bd0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -23,6 +23,8 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; +import javax.annotation.Nonnull; + /** * Implementation of the
verify
operation using PGPainless. */ @@ -31,26 +33,30 @@ public class DetachedVerifyImpl implements DetachedVerify { private final ConsumerOptions options = ConsumerOptions.get(); @Override - public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotBefore(timestamp); return this; } @Override - public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotAfter(timestamp); return this; } @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + @Nonnull + public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); options.addVerificationCerts(certificates); return this; } @Override - public DetachedVerifyImpl signatures(InputStream signatures) throws SOPGPException.BadData { + @Nonnull + public DetachedVerifyImpl signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { try { options.addVerificationOfDetachedSignatures(signatures); } catch (IOException | PGPException e) { @@ -60,7 +66,8 @@ public class DetachedVerifyImpl implements DetachedVerify { } @Override - public List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { options.forceNonOpenPgpData(); DecryptionStream decryptionStream; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java index 08c58395..cbd5a108 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java @@ -7,7 +7,6 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -30,12 +29,16 @@ import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.OpenPgpFingerprint; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.util.Passphrase; +import sop.EncryptionResult; import sop.Profile; -import sop.Ready; +import sop.ReadyWithResult; import sop.enums.EncryptAs; import sop.exception.SOPGPException; import sop.operation.Encrypt; import sop.util.ProxyOutputStream; +import sop.util.UTF8Util; + +import javax.annotation.Nonnull; /** * Implementation of the
encrypt
operation using PGPainless. @@ -52,23 +55,26 @@ public class EncryptImpl implements Encrypt { private final Set signingKeys = new HashSet<>(); private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases - private EncryptAs encryptAs = EncryptAs.Binary; + private EncryptAs encryptAs = EncryptAs.binary; boolean armor = true; + @Nonnull @Override public Encrypt noArmor() { armor = false; return this; } + @Nonnull @Override - public Encrypt mode(EncryptAs mode) throws SOPGPException.UnsupportedOption { + public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption { this.encryptAs = mode; return this; } + @Nonnull @Override - public Encrypt signWith(InputStream keyIn) + public Encrypt signWith(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { if (signingOptions == null) { signingOptions = SigningOptions.get(); @@ -89,21 +95,24 @@ public class EncryptImpl implements Encrypt { return this; } + @Nonnull @Override - public Encrypt withKeyPassword(byte[] password) { - String passphrase = new String(password, Charset.forName("UTF8")); + public Encrypt withKeyPassword(@Nonnull byte[] password) { + String passphrase = new String(password, UTF8Util.UTF8); protector.addPassphrase(Passphrase.fromPassword(passphrase)); return this; } + @Nonnull @Override - public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); return this; } + @Nonnull @Override - public Encrypt withCert(InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { try { PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); encryptionOptions.addRecipients(certificates); @@ -115,8 +124,9 @@ public class EncryptImpl implements Encrypt { return this; } + @Nonnull @Override - public Encrypt profile(String profileName) { + public Encrypt profile(@Nonnull String profileName) { // sanitize profile name to make sure we only accept supported profiles for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { @@ -130,8 +140,9 @@ public class EncryptImpl implements Encrypt { throw new SOPGPException.UnsupportedProfile("encrypt", profileName); } + @Nonnull @Override - public Ready plaintext(InputStream plaintext) throws IOException { + public ReadyWithResult plaintext(@Nonnull InputStream plaintext) throws IOException { if (!encryptionOptions.hasEncryptionMethod()) { throw new SOPGPException.MissingArg("Missing encryption method."); } @@ -146,7 +157,7 @@ public class EncryptImpl implements Encrypt { signingOptions.addInlineSignature( protector, signingKey, - (encryptAs == EncryptAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) + (encryptAs == EncryptAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) ); } catch (KeyException.UnacceptableSigningKeyException e) { throw new SOPGPException.KeyCannotSign(); @@ -163,12 +174,14 @@ public class EncryptImpl implements Encrypt { .onOutputStream(proxy) .withOptions(producerOptions); - return new Ready() { + return new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { proxy.replaceOutputStream(outputStream); Streams.pipeAll(plaintext, encryptionStream); encryptionStream.close(); + // TODO: Extract and emit SessionKey + return new EncryptionResult(null); } }; } catch (PGPException e) { @@ -178,9 +191,9 @@ public class EncryptImpl implements Encrypt { private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { switch (encryptAs) { - case Binary: + case binary: return StreamEncoding.BINARY; - case Text: + case text: return StreamEncoding.UTF8; } throw new IllegalArgumentException("Invalid value encountered: " + encryptAs); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java index be7fc9c3..6dce2578 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java @@ -19,6 +19,8 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.ExtractCert; +import javax.annotation.Nonnull; + /** * Implementation of the
extract-cert
operation using PGPainless. */ @@ -27,13 +29,15 @@ public class ExtractCertImpl implements ExtractCert { private boolean armor = true; @Override + @Nonnull public ExtractCert noArmor() { armor = false; return this; } @Override - public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { + @Nonnull + public Ready key(@Nonnull InputStream keyInputStream) throws IOException, SOPGPException.BadData { PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true); List certs = new ArrayList<>(); @@ -44,7 +48,7 @@ public class ExtractCertImpl implements ExtractCert { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { for (PGPPublicKeyRing cert : certs) { OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index b6d5fe73..03583891 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -31,6 +31,8 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.operation.GenerateKey; +import javax.annotation.Nonnull; + /** * Implementation of the
generate-key
operation using PGPainless. */ @@ -48,25 +50,29 @@ public class GenerateKeyImpl implements GenerateKey { private String profile = CURVE25519_PROFILE.getName(); @Override + @Nonnull public GenerateKey noArmor() { this.armor = false; return this; } @Override - public GenerateKey userId(String userId) { + @Nonnull + public GenerateKey userId(@Nonnull String userId) { this.userIds.add(userId); return this; } @Override - public GenerateKey withKeyPassword(String password) { + @Nonnull + public GenerateKey withKeyPassword(@Nonnull String password) { this.passphrase = Passphrase.fromPassword(password); return this; } @Override - public GenerateKey profile(String profileName) { + @Nonnull + public GenerateKey profile(@Nonnull String profileName) { // Sanitize the profile name to make sure we support the given profile for (Profile profile : SUPPORTED_PROFILES) { if (profile.getName().equals(profileName)) { @@ -81,18 +87,20 @@ public class GenerateKeyImpl implements GenerateKey { } @Override + @Nonnull public GenerateKey signingOnly() { signingOnly = true; return this; } @Override + @Nonnull public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { try { final PGPSecretKeyRing key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly); return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { if (armor) { ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(key, outputStream); key.encode(armoredOutputStream); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java index da6e0917..85c06d25 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java @@ -30,6 +30,8 @@ import sop.Signatures; import sop.exception.SOPGPException; import sop.operation.InlineDetach; +import javax.annotation.Nonnull; + /** * Implementation of the
inline-detach
operation using PGPainless. */ @@ -38,20 +40,22 @@ public class InlineDetachImpl implements InlineDetach { private boolean armor = true; @Override + @Nonnull public InlineDetach noArmor() { this.armor = false; return this; } @Override - public ReadyWithResult message(InputStream messageInputStream) { + @Nonnull + public ReadyWithResult message(@Nonnull InputStream messageInputStream) { return new ReadyWithResult() { private final ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); @Override - public Signatures writeTo(OutputStream messageOutputStream) + public Signatures writeTo(@Nonnull OutputStream messageOutputStream) throws SOPGPException.NoSignature, IOException { PGPSignatureList signatures = null; @@ -142,7 +146,7 @@ public class InlineDetachImpl implements InlineDetach { return new Signatures() { @Override - public void writeTo(OutputStream signatureOutputStream) throws IOException { + public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { Streams.pipeAll(new ByteArrayInputStream(sigOut.toByteArray()), signatureOutputStream); } }; diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index dd4ab0cf..6ed1d471 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -7,7 +7,6 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; @@ -28,6 +27,9 @@ import sop.Ready; import sop.enums.InlineSignAs; import sop.exception.SOPGPException; import sop.operation.InlineSign; +import sop.util.UTF8Util; + +import javax.annotation.Nonnull; /** * Implementation of the
inline-sign
operation using PGPainless. @@ -41,19 +43,22 @@ public class InlineSignImpl implements InlineSign { private final List signingKeys = new ArrayList<>(); @Override - public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { this.mode = mode; return this; } @Override + @Nonnull public InlineSign noArmor() { this.armor = false; return this; } @Override - public InlineSign key(InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + @Nonnull + public InlineSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); for (PGPSecretKeyRing key : keys) { KeyRingInfo info = PGPainless.inspectKeyRing(key); @@ -67,14 +72,16 @@ public class InlineSignImpl implements InlineSign { } @Override - public InlineSign withKeyPassword(byte[] password) { - String string = new String(password, Charset.forName("UTF8")); + @Nonnull + public InlineSign withKeyPassword(@Nonnull byte[] password) { + String string = new String(password, UTF8Util.UTF8); protector.addPassphrase(Passphrase.fromPassword(string)); return this; } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.clearsigned) { @@ -99,7 +106,7 @@ public class InlineSignImpl implements InlineSign { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() .onOutputStream(outputStream) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index aecb891b..352bd7c8 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -26,6 +26,8 @@ import sop.Verification; import sop.exception.SOPGPException; import sop.operation.InlineVerify; +import javax.annotation.Nonnull; + /** * Implementation of the
inline-verify
operation using PGPainless. */ @@ -34,29 +36,33 @@ public class InlineVerifyImpl implements InlineVerify { private final ConsumerOptions options = ConsumerOptions.get(); @Override - public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotBefore(timestamp); return this; } @Override - public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { options.verifyNotAfter(timestamp); return this; } @Override - public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + @Nonnull + public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); options.addVerificationCerts(certificates); return this; } @Override - public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public ReadyWithResult> data(@Nonnull InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { return new ReadyWithResult>() { @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { DecryptionStream decryptionStream; try { decryptionStream = PGPainless.decryptAndOrVerify() diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java index 36ef4861..e39c080d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java @@ -10,6 +10,8 @@ import sop.Profile; import sop.exception.SOPGPException; import sop.operation.ListProfiles; +import javax.annotation.Nonnull; + /** * Implementation of the
list-profiles
operation using PGPainless. * @@ -17,10 +19,8 @@ import sop.operation.ListProfiles; public class ListProfilesImpl implements ListProfiles { @Override - public List subcommand(String command) { - if (command == null) { - throw new SOPGPException.UnsupportedProfile("null"); - } + @Nonnull + public List subcommand(@Nonnull String command) { switch (command) { case "generate-key": diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java index 6d72bf8f..6b11b73a 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java @@ -32,11 +32,15 @@ import sop.exception.SOPGPException; import sop.operation.RevokeKey; import sop.util.UTF8Util; +import javax.annotation.Nonnull; + public class RevokeKeyImpl implements RevokeKey { private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); private boolean armor = true; + @Override + @Nonnull public RevokeKey noArmor() { this.armor = false; return this; @@ -50,7 +54,9 @@ public class RevokeKeyImpl implements RevokeKey { * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable */ - public RevokeKey withKeyPassword(byte[] password) + @Override + @Nonnull + public RevokeKey withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String string; @@ -63,7 +69,9 @@ public class RevokeKeyImpl implements RevokeKey { return this; } - public Ready keys(InputStream keys) throws SOPGPException.BadData { + @Override + @Nonnull + public Ready keys(@Nonnull InputStream keys) throws SOPGPException.BadData { PGPSecretKeyRingCollection secretKeyRings; try { secretKeyRings = KeyReader.readSecretKeys(keys, true); @@ -100,7 +108,7 @@ public class RevokeKeyImpl implements RevokeKey { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates); if (armor) { ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 4009d2a8..d13ebc02 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -22,6 +22,8 @@ import sop.operation.ListProfiles; import sop.operation.RevokeKey; import sop.operation.Version; +import javax.annotation.Nonnull; + /** * Implementation of the
sop
API using PGPainless. *
 {@code
@@ -35,86 +37,103 @@ public class SOPImpl implements SOP {
     }
 
     @Override
+    @Nonnull
     public Version version() {
         return new VersionImpl();
     }
 
     @Override
+    @Nonnull
     public GenerateKey generateKey() {
         return new GenerateKeyImpl();
     }
 
     @Override
+    @Nonnull
     public ExtractCert extractCert() {
         return new ExtractCertImpl();
     }
 
     @Override
+    @Nonnull
     public DetachedSign sign() {
         return detachedSign();
     }
 
     @Override
+    @Nonnull
     public DetachedSign detachedSign() {
         return new DetachedSignImpl();
     }
 
     @Override
+    @Nonnull
     public InlineSign inlineSign() {
         return new InlineSignImpl();
     }
 
     @Override
+    @Nonnull
     public DetachedVerify verify() {
         return detachedVerify();
     }
 
     @Override
+    @Nonnull
     public DetachedVerify detachedVerify() {
         return new DetachedVerifyImpl();
     }
 
     @Override
+    @Nonnull
     public InlineVerify inlineVerify() {
         return new InlineVerifyImpl();
     }
 
     @Override
+    @Nonnull
     public Encrypt encrypt() {
         return new EncryptImpl();
     }
 
     @Override
+    @Nonnull
     public Decrypt decrypt() {
         return new DecryptImpl();
     }
 
     @Override
+    @Nonnull
     public Armor armor() {
         return new ArmorImpl();
     }
 
     @Override
+    @Nonnull
     public Dearmor dearmor() {
         return new DearmorImpl();
     }
 
     @Override
+    @Nonnull
     public ListProfiles listProfiles() {
         return new ListProfilesImpl();
     }
 
     @Override
+    @Nonnull
     public RevokeKey revokeKey() {
         return new RevokeKeyImpl();
     }
 
     @Override
+    @Nonnull
     public ChangeKeyPassword changeKeyPassword() {
         return new ChangeKeyPasswordImpl();
     }
 
     @Override
+    @Nonnull
     public InlineDetach inlineDetach() {
         return new InlineDetachImpl();
     }
diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
index 488d95b3..424f8d19 100644
--- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
+++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java
@@ -12,6 +12,8 @@ import java.util.Properties;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import sop.operation.Version;
 
+import javax.annotation.Nonnull;
+
 /**
  * Implementation of the 
version
operation using PGPainless. */ @@ -21,11 +23,13 @@ public class VersionImpl implements Version { private static final int SOP_VERSION = 7; @Override + @Nonnull public String getName() { return "PGPainless-SOP"; } @Override + @Nonnull public String getVersion() { // See https://stackoverflow.com/a/50119235 String version; @@ -44,11 +48,13 @@ public class VersionImpl implements Version { } @Override + @Nonnull public String getBackendVersion() { return "PGPainless " + getVersion(); } @Override + @Nonnull public String getExtendedVersion() { double bcVersion = new BouncyCastleProvider().getVersion(); String FORMAT_VERSION = String.format("%02d", SOP_VERSION); From 71431b7b0a6f0552e6f6c166bad385855707533e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:40:00 +0100 Subject: [PATCH 199/351] Bump sop-java to 8.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 73c5547c..8a27f5ed 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '7.0.0' + sopJavaVersion = '8.0.0-SNAPSHOT' } } From 1c10dd5bcef1aa2039c791ade3ec7965d2dd6f15 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:40:22 +0100 Subject: [PATCH 200/351] Adapt changes from sop-java 8.0.0 --- .../java/org/pgpainless/sop/ArmorTest.java | 19 ++++++++----------- .../sop/CarolKeySignEncryptRoundtripTest.java | 7 ++++--- .../org/pgpainless/sop/DetachedSignTest.java | 6 +++--- .../sop/EncryptDecryptRoundTripTest.java | 11 +++++++++++ .../org/pgpainless/sop/GenerateKeyTest.java | 6 ------ .../org/pgpainless/sop/ListProfilesTest.java | 5 ----- .../PGPainlessChangeKeyPasswordTest.java | 1 + 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index 3830912a..a97b968d 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -4,30 +4,27 @@ package org.pgpainless.sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; - -import org.bouncycastle.openpgp.PGPException; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.ArmorUtils; import sop.enums.ArmorLabel; import sop.exception.SOPGPException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class ArmorTest { @Test public void labelIsNotSupported() { - assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.Sig)); + assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.sig)); } @Test - public void armor() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void armor() throws IOException { byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); byte[] knownGoodArmor = ArmorUtils.toAsciiArmoredString(data) .replace("Version: PGPainless\n", "") // armor command does not add version anymore diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java index 58dfaa62..83778106 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/CarolKeySignEncryptRoundtripTest.java @@ -11,7 +11,8 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.DecryptionResult; -import sop.Ready; +import sop.EncryptionResult; +import sop.ReadyWithResult; import sop.testsuite.assertions.VerificationListAssert; public class CarolKeySignEncryptRoundtripTest { @@ -276,11 +277,11 @@ public class CarolKeySignEncryptRoundtripTest { public void regressionTest() throws IOException { SOPImpl sop = new SOPImpl(); byte[] msg = "Hello, World!\n".getBytes(); - Ready encryption = sop.encrypt() + ReadyWithResult encryption = sop.encrypt() .signWith(CAROL_KEY.getBytes()) .withCert(BOB_CERT.getBytes()) .plaintext(msg); - byte[] ciphertext = encryption.getBytes(); + byte[] ciphertext = encryption.toByteArrayAndResult().getBytes(); ByteArrayAndResult decryption = sop.decrypt() .withKey(BOB_KEY.getBytes()) diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java index 9238697a..7b2c9131 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/DetachedSignTest.java @@ -51,7 +51,7 @@ public class DetachedSignTest { public void signArmored() throws IOException { byte[] signature = sop.sign() .key(key) - .mode(SignAs.Binary) + .mode(SignAs.binary) .data(data) .toByteArrayAndResult().getBytes(); @@ -95,7 +95,7 @@ public class DetachedSignTest { byte[] signature = sop.sign() .key(key) .noArmor() - .mode(SignAs.Text) + .mode(SignAs.text) .data(data) .toByteArrayAndResult().getBytes(); @@ -142,7 +142,7 @@ public class DetachedSignTest { @Test public void mode() throws IOException, PGPException { byte[] signature = sop.sign() - .mode(SignAs.Text) + .mode(SignAs.text) .key(key) .data(data) .toByteArrayAndResult().getBytes(); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java index ccfeaf41..aa366ef1 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/EncryptDecryptRoundTripTest.java @@ -64,6 +64,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCert) .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -97,6 +98,7 @@ public class EncryptDecryptRoundTripTest { .withCert(aliceCertNoArmor) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -118,6 +120,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -141,6 +144,7 @@ public class EncryptDecryptRoundTripTest { sop.encrypt() .withPassword("passphr4s3") .plaintext(message) + .toByteArrayAndResult() .getInputStream() ) .toByteArrayAndResult(); @@ -158,6 +162,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withPassword("passphr4s3 ") .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -178,6 +183,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); DecryptionResult result = sop.decrypt() @@ -196,6 +202,7 @@ public class EncryptDecryptRoundTripTest { byte[] encrypted = sop.encrypt() .withCert(bobCert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.MissingArg.class, () -> sop @@ -266,6 +273,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -308,6 +316,7 @@ public class EncryptDecryptRoundTripTest { .withCert(cert1) .withCert(cert2) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); byte[] decrypted = sop.decrypt() @@ -339,6 +348,7 @@ public class EncryptDecryptRoundTripTest { byte[] ciphertext = sop.encrypt() .withCert(cert) .plaintext(plaintext) + .toByteArrayAndResult() .getBytes(); assertThrows(SOPGPException.KeyIsProtected.class, @@ -571,6 +581,7 @@ public class EncryptDecryptRoundTripTest { .profile("rfc4880") .withCert(cert) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index 5894bfa7..ca6df790 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -100,10 +100,4 @@ public class GenerateKeyTest { assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop.generateKey().profile("invalid")); } - - @Test - public void nullProfile() { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> - sop.generateKey().profile((String) null)); - } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java index 9158f11e..e15a04cc 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ListProfilesTest.java @@ -37,9 +37,4 @@ public class ListProfilesTest { sop.listProfiles().subcommand("help")); } - @Test - public void listProfilesOfNullThrows() { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> - sop.listProfiles().subcommand(null)); - } } diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java index 78ba6737..c1fc2cd9 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -75,6 +75,7 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { .withKeyPassword(p4) .withCert(cert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() .verifyWithCert(cert) From 8203bd58c7e7965d22eb03d76cb179cbb4cc9ae0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:05:39 +0100 Subject: [PATCH 201/351] Bump sop-java to 8.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 8a27f5ed..5a3c904b 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '8.0.0-SNAPSHOT' + sopJavaVersion = '8.0.0' } } From cf638da13045c6bb2b51eefeafdccc4edcdef762 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:09:15 +0100 Subject: [PATCH 202/351] Bump sop spec to 8 --- .../src/main/java/org/pgpainless/sop/VersionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index 424f8d19..d7514861 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -20,7 +20,7 @@ import javax.annotation.Nonnull; public class VersionImpl implements Version { // draft version - private static final int SOP_VERSION = 7; + private static final int SOP_VERSION = 8; @Override @Nonnull From 481dfac455d2c71faf7ffda50ee07835a91da114 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:23:52 +0100 Subject: [PATCH 203/351] Revert PassphraseProvider API change --- .../key/protection/CachingSecretKeyRingProtector.kt | 6 +++--- .../PasswordBasedSecretKeyRingProtector.kt | 10 +++++----- .../MapBasedPassphraseProvider.kt | 6 +++--- .../SecretKeyPassphraseProvider.kt | 4 ++-- .../SolitaryPassphraseProvider.kt | 4 ++-- .../MissingPassphraseForDecryptionTest.java | 8 ++++---- ...eDecryptionUsingKeyWithMissingPassphraseTest.java | 12 ++++++------ .../CachingSecretKeyRingProtectorTest.java | 4 ++-- .../key/protection/PassphraseProtectedKeyTest.java | 4 ++-- .../key/protection/SecretKeyRingProtectorTest.java | 4 ++-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt index 5a3ff47f..20704685 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/CachingSecretKeyRingProtector.kt @@ -21,7 +21,7 @@ import org.pgpainless.util.Passphrase */ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphraseProvider { - private val cache: MutableMap + private val cache: MutableMap private val protector: SecretKeyRingProtector private val provider: SecretKeyPassphraseProvider? @@ -152,12 +152,12 @@ class CachingSecretKeyRingProtector : SecretKeyRingProtector, SecretKeyPassphras */ fun forgetPassphrase(key: PGPPublicKey) = apply { forgetPassphrase(key.keyID) } - override fun getPassphraseFor(keyId: Long): Passphrase? { + override fun getPassphraseFor(keyId: Long?): Passphrase? { return if (hasPassphrase(keyId)) cache[keyId] else provider?.getPassphraseFor(keyId)?.also { cache[keyId] = it } } - override fun hasPassphrase(keyId: Long) = cache[keyId]?.isValid ?: false + override fun hasPassphrase(keyId: Long?) = cache[keyId]?.isValid ?: false override fun hasPassphraseFor(keyId: Long) = hasPassphrase(keyId) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt index 74ef881f..9eb47e88 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.kt @@ -38,12 +38,12 @@ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? { + override fun getPassphraseFor(keyId: Long?): Passphrase? { return if (hasPassphrase(keyId)) passphrase else null } - override fun hasPassphrase(keyId: Long): Boolean { - return keyRing.getPublicKey(keyId) != null + override fun hasPassphrase(keyId: Long?): Boolean { + return keyId != null && keyRing.getPublicKey(keyId) != null } } .let { PasswordBasedSecretKeyRingProtector(it) } @@ -59,11 +59,11 @@ class PasswordBasedSecretKeyRingProtector : BaseSecretKeyRingProtector { passphrase: Passphrase ): PasswordBasedSecretKeyRingProtector { return object : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? { + override fun getPassphraseFor(keyId: Long?): Passphrase? { return if (hasPassphrase(keyId)) passphrase else null } - override fun hasPassphrase(keyId: Long): Boolean { + override fun hasPassphrase(keyId: Long?): Boolean { return keyId == singleKeyId } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt index ffa3619a..3457cff7 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.kt @@ -14,9 +14,9 @@ import org.pgpainless.util.Passphrase * * TODO: Make this null-safe and throw an exception instead? */ -class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { +class MapBasedPassphraseProvider(val map: Map) : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? = map[keyId] + override fun getPassphraseFor(keyId: Long?): Passphrase? = map[keyId] - override fun hasPassphrase(keyId: Long): Boolean = map.containsKey(keyId) + override fun hasPassphrase(keyId: Long?): Boolean = map.containsKey(keyId) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt index d872da51..a80b8bb0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.kt @@ -30,7 +30,7 @@ interface SecretKeyPassphraseProvider { * @param keyId if of the secret key * @return passphrase or null, if no passphrase record has been found. */ - fun getPassphraseFor(keyId: Long): Passphrase? + fun getPassphraseFor(keyId: Long?): Passphrase? - fun hasPassphrase(keyId: Long): Boolean + fun hasPassphrase(keyId: Long?): Boolean } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt index a8a1735d..a9f6801d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.kt @@ -9,7 +9,7 @@ import org.pgpainless.util.Passphrase /** Implementation of the [SecretKeyPassphraseProvider] that holds a single [Passphrase]. */ class SolitaryPassphraseProvider(val passphrase: Passphrase?) : SecretKeyPassphraseProvider { - override fun getPassphraseFor(keyId: Long): Passphrase? = passphrase + override fun getPassphraseFor(keyId: Long?): Passphrase? = passphrase - override fun hasPassphrase(keyId: Long): Boolean = true + override fun hasPassphrase(keyId: Long?): Boolean = true } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index 1cf55a4d..42562713 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -63,13 +63,13 @@ public class MissingPassphraseForDecryptionTest { // interactive callback SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { // is called in interactive mode return Passphrase.fromPassword(passphrase); } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return true; } }; @@ -95,13 +95,13 @@ public class MissingPassphraseForDecryptionTest { SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { fail("MUST NOT get called in non-interactive mode."); return null; } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return true; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index 914f477e..c86a823c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -120,13 +120,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void missingPassphraseFirst() throws PGPException, IOException { SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available."); return null; } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return false; } }); @@ -150,13 +150,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockEachKeyWith(p1, k1); SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available."); return null; } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return false; } }); @@ -178,13 +178,13 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { public void messagePassphraseFirst() throws PGPException, IOException { SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { fail("Since we provide a decryption passphrase, we should not try to decrypt any key."); return null; } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return false; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java index 9869f7b5..3f7a9e6e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java @@ -32,13 +32,13 @@ public class CachingSecretKeyRingProtectorTest { // Dummy passphrase callback that returns the doubled key-id as passphrase private final SecretKeyPassphraseProvider dummyCallback = new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { long doubled = keyId * 2; return Passphrase.fromPassword(Long.toString(doubled)); } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return true; } }; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index b5703eef..370cfd85 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -31,7 +31,7 @@ public class PassphraseProtectedKeyTest { new SecretKeyPassphraseProvider() { @Nullable @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { if (keyId == TestKeys.CRYPTIE_KEY_ID) { return new Passphrase(TestKeys.CRYPTIE_PASSWORD.toCharArray()); } else { @@ -40,7 +40,7 @@ public class PassphraseProtectedKeyTest { } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return keyId == TestKeys.CRYPTIE_KEY_ID; } }); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index b6781355..a5030f74 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -108,12 +108,12 @@ public class SecretKeyRingProtectorTest { CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() { @Override - public Passphrase getPassphraseFor(long keyId) { + public Passphrase getPassphraseFor(Long keyId) { return Passphrase.fromPassword("missingP455w0rd"); } @Override - public boolean hasPassphrase(long keyId) { + public boolean hasPassphrase(Long keyId) { return true; } }); From b58826f90767c37f197baf0d5031f8e331f71832 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:26:27 +0100 Subject: [PATCH 204/351] Update changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f86b67f..99b6ead6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -# 1.6.2 +## 2.0.0-SNAPSHOT +- `pgpainless-core` + - Rewrote most of the codebase in Kotlin + - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) +- `pgpainless-sop`, `pgpainless-cli` + - Bump `sop-java` to `8.0.0`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) + - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key + +## 1.6.2 - Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on` - Bump `bcpg-jdk8on` to `1.76` - Bump `bcprov-jdk18on` to `1.76` @@ -13,7 +21,7 @@ SPDX-License-Identifier: CC0-1.0 encrypting to legacy keys which do not carry any key flags. - Allow overriding of reference time in `EncryptionOptions` and `SigningOptions`. -# 1.6.1 +## 1.6.1 - `KeyRingBuilder`: Require UTF8 when adding user-ID via `addUserId(byte[])` - `pgpainless-sop`: Remove dependency on jetbrains annotations - Add `CertificateAuthority` interface to allow integration with [`pgpainless-wot`](https://github.com/pgpainless/pgpainless-wot) From f2448890e18a6437d3eb8d5a559553bc40504353 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Nov 2023 13:32:52 +0100 Subject: [PATCH 205/351] Bump sop-java to 8.0.1 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 5a3c904b..12941b2f 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.11' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '8.0.0' + sopJavaVersion = '8.0.1' } } From 417684f55d2c2eb6f5501e977969c1b4930e36f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Nov 2023 14:04:11 +0100 Subject: [PATCH 206/351] Update changelog --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b6ead6..14cbb9f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,13 @@ SPDX-License-Identifier: CC0-1.0 - Rewrote most of the codebase in Kotlin - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) - `pgpainless-sop`, `pgpainless-cli` - - Bump `sop-java` to `8.0.0`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) + - Bump `sop-java` to `8.0.1`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key + - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + +## 1.6.3 +- Bump `sop-java` to `7.0.1` +- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty ## 1.6.2 - Switch `bcpg` and `bcprov` artifacts from `-jdk15to18`variant to `-jdk18on` @@ -111,6 +116,10 @@ SPDX-License-Identifier: CC0-1.0 - Add profile `rfc4880` to reflect status quo - `version`: Add support for `--sop-spec` option +## 1.4.6 +- Bump `sop-java` to `4.1.2` +- Fix `decrypt --verify-with` to not throw `NoSignature` exception (exit code 3) if `VERIFICAIONS` is empty. + ## 1.4.5 - Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key - Security: Fix faulty bit-strength policy check for signing subkeys @@ -184,6 +193,10 @@ SPDX-License-Identifier: CC0-1.0 - Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` - Remove `BCUtil` class +## 1.3.18 +- Bump `sop-java` to `4.1.2` +- Fix `decrypt --verify-with XYZ` not throwing `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty (#415) + ## 1.3.17 - Bugfix: Direct-Key signatures are calculated over the signee key only, not the signer key + signee key - Security: Fix faulty bit-strength policy check for signing subkeys From 6999f4d241a6b491a88e2c350d370f1935117c1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 23 Nov 2023 14:45:23 +0100 Subject: [PATCH 207/351] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cbb9f6..6cebdaeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ SPDX-License-Identifier: CC0-1.0 - `generate-key`: Add support for new `--signing-only` option - Move some methods related to password changing from `SecretKeyRingEditor` to `KeyRingUtils` +## 1.5.7 +- Bump `sop-java` to `6.1.1` +- `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + ## 1.5.6 - Bump `jacoco` to `0.8.8` (thanks @hkos) - Ignore malformed, non-UTF8 user-IDs on certificates From 9ab0c35b78d632d15c73058857cd0c8676d573eb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 26 Nov 2023 10:55:47 +0100 Subject: [PATCH 208/351] add unit test to read decryption stream beyond end --- .../DecryptAndVerifyMessageTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index ad61e132..753d1246 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -82,6 +82,24 @@ public class DecryptAndVerifyMessageTest { assertEquals(new SubkeyIdentifier(TestKeys.JULIET_FINGERPRINT), metadata.getDecryptionKey()); } + @TestTemplate + @ExtendWith(TestAllImplementations.class) + public void decryptMessageAndReadBeyondEndTest() throws Exception { + final String encryptedMessage = TestKeys.MSG_SIGN_CRYPT_JULIET_JULIET; + + final ConsumerOptions options = new ConsumerOptions() + .addDecryptionKey(juliet) + .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); + + try (final DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) + .withOptions(options); + final ByteArrayOutputStream toPlain = new ByteArrayOutputStream()) { + Streams.pipeAll(decryptor, toPlain); + assertEquals(-1, decryptor.read()); + } + } + @TestTemplate @ExtendWith(TestAllImplementations.class) public void decryptMessageAndVerifySignatureByteByByteTest() throws Exception { From 1e3340809869941563e78eedf819270c83b961c5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 27 Nov 2023 13:26:41 +0100 Subject: [PATCH 209/351] Please the checkstyle checker --- .../decryption_verification/DecryptAndVerifyMessageTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 753d1246..4b6d5a86 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -91,10 +91,10 @@ public class DecryptAndVerifyMessageTest { .addDecryptionKey(juliet) .addVerificationCert(KeyRingUtils.publicKeyRingFrom(juliet)); - try (final DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + try (DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes())) .withOptions(options); - final ByteArrayOutputStream toPlain = new ByteArrayOutputStream()) { + ByteArrayOutputStream toPlain = new ByteArrayOutputStream()) { Streams.pipeAll(decryptor, toPlain); assertEquals(-1, decryptor.read()); } From b0caa95378d4ce3b75f18df784b6c48bf5350c71 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 27 Nov 2023 13:27:23 +0100 Subject: [PATCH 210/351] Properly feed an EOS token to the push down automata in OpenPgpMessageInputStream.read() --- .../decryption_verification/OpenPgpMessageInputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 5e8b68f2..df67ca0f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -486,6 +486,7 @@ class OpenPgpMessageInputStream( override fun read(): Int { if (nestedInputStream == null) { if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.END_OF_SEQUENCE) syntaxVerifier.assertValid() } return -1 From 49de608785f72b58077245552e1c1c963b330778 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 27 Nov 2023 13:31:31 +0100 Subject: [PATCH 211/351] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cebdaeb..7c8f92be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ SPDX-License-Identifier: CC0-1.0 - Bump `sop-java` to `8.0.1`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty +- Properly update the push down automaton with an EOS token when reaching the end of a packet stream using `OpenPgpMessageInputStream.read()`. Thanks @iNPUTmice for the report. ## 1.6.3 - Bump `sop-java` to `7.0.1` From f39d2c5566a30515a3879eae465ddfb113e37738 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 17:58:10 +0100 Subject: [PATCH 212/351] Prevent subkey binding signature from predating subkey Fixes #419 --- .../org/pgpainless/key/generation/KeyRingBuilder.kt | 9 ++++++--- .../main/kotlin/org/pgpainless/key/generation/KeySpec.kt | 2 +- .../org/pgpainless/key/generation/KeySpecBuilder.kt | 2 +- .../modification/secretkeyring/SecretKeyRingEditor.kt | 3 ++- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 8404b652..67ad9669 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -246,7 +246,11 @@ class KeyRingBuilder : KeyRingBuilderInterface { const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365 @JvmStatic - fun generateKeyPair(spec: KeySpec): PGPKeyPair { + @JvmOverloads + fun generateKeyPair( + spec: KeySpec, + creationTime: Date = spec.keyCreationDate ?: Date() + ): PGPKeyPair { spec.keyType.let { type -> // Create raw Key Pair val keyPair = @@ -254,10 +258,9 @@ class KeyRingBuilder : KeyRingBuilderInterface { .also { it.initialize(type.algorithmSpec) } .generateKeyPair() - val keyCreationDate = spec.keyCreationDate ?: Date() // Form PGP Key Pair return ImplementationFactory.getInstance() - .getPGPKeyPair(type.algorithm, keyPair, keyCreationDate) + .getPGPKeyPair(type.algorithm, keyPair, creationTime) } } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt index bc8d5599..f616a7f2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpec.kt @@ -15,7 +15,7 @@ data class KeySpec( val keyType: KeyType, val subpacketGenerator: SignatureSubpackets, val isInheritedSubPackets: Boolean, - val keyCreationDate: Date + val keyCreationDate: Date? ) { val subpackets: PGPSignatureSubpacketVector diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt index 03291f2d..0e7f9aae 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeySpecBuilder.kt @@ -25,7 +25,7 @@ constructor( private var preferredHashAlgorithms: Set = algorithmSuite.hashAlgorithms private var preferredSymmetricAlgorithms: Set = algorithmSuite.symmetricKeyAlgorithms - private var keyCreationDate = Date() + private var keyCreationDate: Date? = null constructor(type: KeyType, vararg keyFlags: KeyFlag) : this(type, listOf(*keyFlags)) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 7dff25f7..5fdd2da5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -231,6 +231,7 @@ class SecretKeyRingEditor( override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { SignatureSubpacketsHelper.applyFrom( keySpec.subpackets, hashedSubpackets as SignatureSubpackets) + hashedSubpackets.setSignatureCreationTime(referenceTime) } } return addSubKey(keySpec, subkeyPassphrase, callback, protector) @@ -242,7 +243,7 @@ class SecretKeyRingEditor( callback: SelfSignatureSubpackets.Callback?, protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface { - val keyPair = KeyRingBuilder.generateKeyPair(keySpec) + val keyPair = KeyRingBuilder.generateKeyPair(keySpec, referenceTime) val subkeyProtector = PasswordBasedSecretKeyRingProtector.forKeyId(keyPair.keyID, subkeyPassphrase) val keyFlags = KeyFlag.fromBitmask(keySpec.subpackets.keyFlags).toMutableList() From 74c7b025a0aa49f947fedb1753a72bcee4334858 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 19:36:01 +0100 Subject: [PATCH 213/351] Do not choke on unknown signature subpackets Fixes #418 --- .../signature/subpackets/SignatureSubpacketsHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt index 6c39432e..8a6c16bf 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsHelper.kt @@ -17,7 +17,7 @@ class SignatureSubpacketsHelper { fun applyFrom(vector: PGPSignatureSubpacketVector, subpackets: SignatureSubpackets) = subpackets.apply { for (subpacket in vector.toArray()) { - val type = SignatureSubpacket.requireFromCode(subpacket.type) + val type = SignatureSubpacket.fromCode(subpacket.type) when (type) { SignatureSubpacket.signatureCreationTime, SignatureSubpacket.issuerKeyId, @@ -134,6 +134,7 @@ class SignatureSubpacketsHelper { SignatureSubpacket.preferredAEADAlgorithms, SignatureSubpacket.attestedCertification -> subpackets.addResidualSubpacket(subpacket) + else -> subpackets.addResidualSubpacket(subpacket) } } } From 97455aa2563b3813376f13b621c0aea1a7efec47 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 19:36:44 +0100 Subject: [PATCH 214/351] Add test for handling key with unknown signature subpacket --- ...eyWithUnsupportedSignatureSubpacketTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt new file mode 100644 index 00000000..e557adc3 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnsupportedSignatureSubpacketTest.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import java.util.* +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.junit.jupiter.api.Test +import org.pgpainless.PGPainless +import org.pgpainless.key.protection.SecretKeyRingProtector + +class KeyWithUnsupportedSignatureSubpacketTest { + + @Test + fun `can set new expiration date on key containing unknown subpacket 34`() { + val armoredKey = + """-----BEGIN PGP PRIVATE KEY BLOCK----- + +lFgEZWiyNhYJKwYBBAHaRw8BAQdA71QipJ0CAqOEqQWjuoQE4E7LarKSrNDwE/6K +bQNrCLwAAQCtJ8kVG2AmbDfdVtr/7Ag+yBh0oCvjRvyUCOyIbruOeg+6tClTdWJw +YWNrZXQzNCBUZXN0S2V5IDx0ZXN0QHBncGFpbmxlc3Mub3JnPoiTBBMWCgA7FiEE +zhy5yrnZYU/iBza4G03SQVuWqx0FAmVosjYCGwMFCwkIBwICIgIGFQoJCAsCBBYC +AwECHgcCF4AACgkQG03SQVuWqx1UGgD+IYLeh9t5eJCEnzueuOTYnTnrzyhnLgm9 +dw5qwMXU8VQA/28GCOb7610hyjiBbrrcshkWAKuMwp8bUSz5FOeS5cQEnF0EZWiy +NhIKKwYBBAGXVQEFAQEHQK99ClLDYtn0I2b6Y26NhaL0RWcrNoI/ci0xgXEK2L0Y +AwEIBwAA/06qciQHI0v7MP2LMWm/ZuTJwzlPqV8VsBhrDMyUPUD4D52IeAQYFgoA +IBYhBM4cucq52WFP4gc2uBtN0kFblqsdBQJlaLI2AhsMAAoJEBtN0kFblqsdRQ0A +/iUJ/Fp+D2RjZL+aiwByIxPCVvMJ7a28+GQGjg3hsU2BAP474dfOOVZiTDLWWxsB +wxfzOAQxXDhgR9xd/Lk3MNJxDg== +=YAt0 +-----END PGP PRIVATE KEY BLOCK-----""" + val key: PGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(armoredKey)!! + PGPainless.modifyKeyRing(secretKey = key) + .setExpirationDate(Date(), SecretKeyRingProtector.unprotectedKeys()) + .done() + } +} From b8b46a3ab279538b1cbd9ed64800a8ea70da696f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 20:26:35 +0100 Subject: [PATCH 215/351] Bump BC to 1.77 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 12941b2f..023e8b2b 100644 --- a/version.gradle +++ b/version.gradle @@ -8,7 +8,7 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.76' + bouncyCastleVersion = '1.77' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.2.11' From a5a9153692757bccbb98d2b37ca46d45c288d6e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 20:42:20 +0100 Subject: [PATCH 216/351] Bump logback to 1.4.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 023e8b2b..3d19d12f 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.77' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.2.11' + logbackVersion = '1.4.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '8.0.1' From 7b37f206d6f49d103c73fc4db8075c047cca14bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 30 Nov 2023 20:46:02 +0100 Subject: [PATCH 217/351] Update CHANGELOG --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8f92be..abf4b0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog ## 2.0.0-SNAPSHOT +- Bump `bcpg-jdk8on` to `1.77` +- Bump `bcprov-jdk18on` to `1.77` +- Bump `logback-core` and `logback-classic` to `1.4.13` - `pgpainless-core` - Rewrote most of the codebase in Kotlin - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) @@ -13,7 +16,17 @@ SPDX-License-Identifier: CC0-1.0 - Bump `sop-java` to `8.0.1`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty -- Properly update the push down automaton with an EOS token when reaching the end of a packet stream using `OpenPgpMessageInputStream.read()`. Thanks @iNPUTmice for the report. +- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) +- Do not choke on unknown signature subpackets (thanks @Jerbell) +- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) + +## 1.6.4 +- Bump `bcpg-jdk8on` to `1.77` +- Bump `bcprov-jdk18on` to `1.77` +- Bump `logback-core` and `logback-classic` to `1.4.13` +- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) +- Do not choke on unknown signature subpackets (thanks @Jerbell) +- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) ## 1.6.3 - Bump `sop-java` to `7.0.1` From d7b6dfc8d45a8d8b924768f51d719e51b3e7057d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Dec 2023 17:50:27 +0100 Subject: [PATCH 218/351] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf4b0f1..f5296b4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ SPDX-License-Identifier: CC0-1.0 - Do not choke on unknown signature subpackets (thanks @Jerbell) - Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) +## 1.6.5 +- Add `SecretKeyRingEditor.setExpirationDateOfSubkey()` + ## 1.6.4 - Bump `bcpg-jdk8on` to `1.77` - Bump `bcprov-jdk18on` to `1.77` From 69f1028fd9b0b39e7575b9749bf6a9b444be1154 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Dec 2023 18:20:51 +0100 Subject: [PATCH 219/351] Add method to change expiration time of subkeys Port of e06f60f62c13605806413b24e37d912c62008da0 to kotlin --- .../secretkeyring/SecretKeyRingEditor.kt | 62 +++++++++++++ .../SecretKeyRingEditorInterface.kt | 18 ++++ .../ChangeSubkeyExpirationTimeTest.java | 90 +++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 5fdd2da5..ad24ec04 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -8,6 +8,7 @@ import java.util.* import java.util.function.Predicate import javax.annotation.Nonnull import kotlin.NoSuchElementException +import openpgp.openPgpKeyId import org.bouncycastle.bcpg.sig.KeyExpirationTime import org.bouncycastle.extensions.getKeyExpirationDate import org.bouncycastle.extensions.publicKeyAlgorithm @@ -460,6 +461,28 @@ class SecretKeyRingEditor( return this } + override fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface = apply { + // is primary key + if (keyId == secretKeyRing.publicKey.keyID) { + return setExpirationDate(expiration, protector) + } + + // is subkey + val subkey = + secretKeyRing.getPublicKey(keyId) + ?: throw NoSuchElementException("No subkey with ID ${keyId.openPgpKeyId()} found.") + val prevBinding = + inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) + ?: throw NoSuchElementException( + "Previous subkey binding signaure for ${keyId.openPgpKeyId()} MUST NOT be null.") + val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) + secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) + } + override fun createMinimalRevocationCertificate( protector: SecretKeyRingProtector, revocationAttributes: RevocationAttributes? @@ -676,6 +699,45 @@ class SecretKeyRingEditor( .build() } + private fun reissueSubkeyBindingSignature( + subkey: PGPPublicKey, + expiration: Date?, + protector: SecretKeyRingProtector, + prevSubkeyBindingSignature: PGPSignature + ): PGPSignature { + val primaryKey = secretKeyRing.publicKey + val secretPrimaryKey = secretKeyRing.secretKey + val secretSubkey: PGPSecretKey? = secretKeyRing.getSecretKey(subkey.keyID) + + val builder = + SubkeyBindingSignatureBuilder(secretPrimaryKey, protector, prevSubkeyBindingSignature) + builder.hashedSubpackets.apply { + // set expiration + setSignatureCreationTime(referenceTime) + setKeyExpirationTime(subkey, expiration) + setSignatureExpirationTime(null) // avoid copying sig exp time + + // signing-capable subkeys need embedded primary key binding sig + SignatureSubpacketsUtil.parseKeyFlags(prevSubkeyBindingSignature)?.let { flags -> + if (flags.contains(KeyFlag.SIGN_DATA)) { + if (secretSubkey == null) { + throw NoSuchElementException( + "Secret key does not contain secret-key" + + " component for subkey ${subkey.keyID.openPgpKeyId()}") + } + + // create new embedded back-sig + clearEmbeddedSignatures() + addEmbeddedSignature( + PrimaryKeyBindingSignatureBuilder(secretSubkey, protector) + .build(primaryKey)) + } + } + } + + return builder.build(subkey) + } + private fun selectUserIds(predicate: Predicate): List = inspectKeyRing(secretKeyRing).validUserIds.filter { predicate.test(it) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt index b8fb993c..140ff905 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.kt @@ -469,6 +469,24 @@ interface SecretKeyRingEditorInterface { protector: SecretKeyRingProtector ): SecretKeyRingEditorInterface + /** + * Set the expiration date for the subkey identified by the given keyId to the given expiration + * date. If the key is supposed to never expire, then an expiration date of null is expected. + * + * @param expiration new expiration date of null + * @param keyId id of the subkey + * @param protector to unlock the secret key + * @return the builder + * @throws PGPException in case we cannot generate a new subkey-binding or self-signature with + * the changed expiration date + */ + @Throws(PGPException::class) + fun setExpirationDateOfSubkey( + expiration: Date?, + keyId: Long, + protector: SecretKeyRingProtector + ): SecretKeyRingEditorInterface + /** * Create a minimal, self-authorizing revocation certificate, containing only the primary key * and a revocation signature. This type of revocation certificates was introduced in OpenPGP diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java new file mode 100644 index 00000000..e1926b67 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeSubkeyExpirationTimeTest.java @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.modification; + +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.key.OpenPgpFingerprint; +import org.pgpainless.key.OpenPgpV4Fingerprint; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.DateUtil; + +import java.io.IOException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ChangeSubkeyExpirationTimeTest { + + @Test + public void changeExpirationTimeOfSubkey() { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing().modernKeyRing("Alice"); + Date now = secretKeys.getPublicKey().getCreationTime(); + Date inAnHour = new Date(now.getTime() + 1000 * 60 * 60); + PGPPublicKey encryptionKey = PGPainless.inspectKeyRing(secretKeys) + .getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey( + inAnHour, + encryptionKey.getKeyID(), + SecretKeyRingProtector.unprotectedKeys()) + .done(); + + JUtils.assertDateEquals(inAnHour, PGPainless.inspectKeyRing(secretKeys) + .getSubkeyExpirationDate(OpenPgpFingerprint.of(encryptionKey))); + } + + @Test + public void changeExpirationTimeOfExpiredSubkey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing( + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: CA52 4D5D E3D8 9CD9 105B BA45 3761 076B C6B5 3000\n" + + "Comment: Alice \n" + + "\n" + + "lFgEZXHykRYJKwYBBAHaRw8BAQdATArrVxPEpuA/wcayAxRl/v1tIYJSe4MCA/fO\n" + + "84CFgpcAAP9uZkLjoBIQAjUTEiS8Wk3sui3u4mJ4WVQEpNhQSpq37g8gtBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iJUEExYKAEcFAmVx8pIJEDdhB2vGtTAA\n" + + "FiEEylJNXePYnNkQW7pFN2EHa8a1MAACngECmwEFFgIDAQAECwkIBwUVCgkICwWJ\n" + + "CWYBgAKZAQAAG3oA/0iJbwyjGOTa2RlgBKdmFjlBG5uwMGheKge/aZBbdUd8AQCB\n" + + "8NFWmyLlne4hDMM2g8RFf/W156wnyTH7jTQLx2sZDJxYBGVx8pIWCSsGAQQB2kcP\n" + + "AQEHQLQt6ns7yTxLvIWXqFCekh6QEvUumhHvCTjZPXa/UxCNAAEA+FHhZ1uik6PN\n" + + "Pwli9Tp9QGddf3pwQw+OL/K7gpZO3sgQHYjVBBgWCgB9BQJlcfKSAp4BApsCBRYC\n" + + "AwEABAsJCAcFFQoJCAtfIAQZFgoABgUCZXHykgAKCRCRKlHdDPaYKjyZAQD10Km4\n" + + "Qs37yF9bntS+z9Va7AMUuBlzYF5H/nXCRuqQTAEA60q++7Xwj94yLfoAfxH0V6Wd\n" + + "L2rDJCDZ3FFMlycToQMACgkQN2EHa8a1MADmDgD9EGzH6pPYRW5vWQGXNsr7PMWK\n" + + "LlBnevc0DaVWEHTu9tcA/iezQ9R+A90qcE1+HeNIJbSB89yIoJje2vePRV/JakAI\n" + + "nF0EZXHykhIKKwYBBAGXVQEFAQEHQOiLc02OQJD9qdpsyR6bJ52Cu8rUMlEJOELz\n" + + "1858OoQyAwEIBwAA/3YkHGmnVaQvUpSwlCInOvHvjLNLH9b9Lh/OxiuSoMgIEASI\n" + + "dQQYFgoAHQUCZXHykgKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEDdhB2vGtTAA\n" + + "1nkBAPAUcHxI1O+fE/QzuLANLHDeWc3Mc09KKnWoTkt/kk5VAQCIPlKQAcmmKdYE\n" + + "Tiz8woSKLQKswKr/jVMqnUiGPsU/DoiSBBgWCgBECRA3YQdrxrUwABYhBMpSTV3j\n" + + "2JzZEFu6RTdhB2vGtTAABYJlcfL6Ap4BApsMBRYCAwEABAsJCAcFFQoJCAsFiQAA\n" + + "AGgAAMNmAQDN/TML2zdgBNkfh7TIqbI4Flx54Yi7qEjSXg0Z+tszHgD/e1Bf+xEs\n" + + "BC9ewVsyQsnj3B0FliGYaPiQeoY/FGBmYQs=\n" + + "=5Ur6\n" + + "-----END PGP PRIVATE KEY BLOCK-----" + ); + assertNotNull(secretKeys); + + // subkey is expired at 2023-12-07 16:29:46 UTC + OpenPgpFingerprint encryptionSubkey = new OpenPgpV4Fingerprint("2E541354A23C9943375EC27A3EF133ED8720D636"); + JUtils.assertDateEquals( + DateUtil.parseUTCDate("2023-12-07 16:29:46 UTC"), + PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + + // re-validate the subkey by setting its expiry to null (no expiry) + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDateOfSubkey(null, encryptionSubkey.getKeyId(), SecretKeyRingProtector.unprotectedKeys()) + .done(); + + assertNull(PGPainless.inspectKeyRing(secretKeys).getSubkeyExpirationDate(encryptionSubkey)); + } +} From fc45c9450a5b36168221c9d01f79d0c5550cf59a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Dec 2023 18:25:18 +0100 Subject: [PATCH 220/351] Update SECURITY.md --- SECURITY.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index bffceb73..4a7785ba 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -12,13 +12,13 @@ SPDX-License-Identifier: Apache-2.0 Use this section to tell people about which versions of your project are currently being supported with security updates. -| Version | Supported | -|---------|--------------------| -| 1.6.X | :white_check_mark: | -| 1.5.X | :white_check_mark: | -| 1.4.X | :white_check_mark: | -| 1.3.X | :white_check_mark: | -| < 1.3.X | :x: | +| Version | Supported | Note | +|---------|--------------------|------------| +| 1.6.X | :white_check_mark: | LTS branch | +| 1.5.X | :white_check_mark: | | +| 1.4.X | :white_check_mark: | | +| 1.3.X | :white_check_mark: | LTS branch | +| < 1.3.X | :x: | | ## Reporting a Vulnerability From 5053221e93e191cb61e9a55f4e4625dba0a2a992 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 15 Dec 2023 18:30:04 +0100 Subject: [PATCH 221/351] Bump logback-core to 1.4.14 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 3d19d12f..707ca162 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.77' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.4.13' + logbackVersion = '1.4.14' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '8.0.1' From de9a16125218d8404aaa8e44563026b7ad407499 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 4 Jan 2024 18:20:09 +0100 Subject: [PATCH 222/351] Accept certification signatures using SHA-1 before 2023-02-01 This commit introduces a dedicated SignatureHashAlgorithmPolicy for certification signatures. The default configuration will accept SHA-1 on sigs created before 2023-02-01. --- .../negotiation/HashAlgorithmNegotiator.kt | 2 +- .../key/generation/KeyRingBuilder.kt | 3 +- .../kotlin/org/pgpainless/policy/Policy.kt | 32 +++++++++++++++++-- .../signature/consumer/SignatureValidator.kt | 12 +++++-- .../encryption_signing/SigningTest.java | 4 +-- .../org/pgpainless/example/ManagePolicy.java | 11 ++++--- .../pgpainless/policy/PolicySetterTest.java | 10 ++++-- .../org/pgpainless/policy/PolicyTest.java | 32 +++++++++---------- 8 files changed, 74 insertions(+), 32 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt index 31d6b118..b9474247 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/HashAlgorithmNegotiator.kt @@ -34,7 +34,7 @@ interface HashAlgorithmNegotiator { */ @JvmStatic fun negotiateSignatureHashAlgorithm(policy: Policy): HashAlgorithmNegotiator { - return negotiateByPolicy(policy.signatureHashAlgorithmPolicy) + return negotiateByPolicy(policy.dataSignatureHashAlgorithmPolicy) } /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 67ad9669..2eeab371 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -216,7 +216,8 @@ class KeyRingBuilder : KeyRingBuilderInterface { } private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder { - val hashAlgorithm = PGPainless.getPolicy().signatureHashAlgorithmPolicy.defaultHashAlgorithm + val hashAlgorithm = + PGPainless.getPolicy().certificationSignatureHashAlgorithmPolicy.defaultHashAlgorithm return ImplementationFactory.getInstance() .getPGPContentSignerBuilder(certKey.publicKey.algorithm, hashAlgorithm.algorithmId) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt index 22f42cd9..ba022838 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -10,8 +10,9 @@ import org.pgpainless.util.DateUtil import org.pgpainless.util.NotationRegistry class Policy( - var signatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var certificationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, var revocationSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, + var dataSignatureHashAlgorithmPolicy: HashAlgorithmPolicy, var symmetricKeyEncryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, var symmetricKeyDecryptionAlgorithmPolicy: SymmetricKeyAlgorithmPolicy, var compressionAlgorithmPolicy: CompressionAlgorithmPolicy, @@ -21,8 +22,9 @@ class Policy( constructor() : this( - HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), - HashAlgorithmPolicy.smartSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartCertificationSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartCertificationSignatureHashAlgorithmPolicy(), + HashAlgorithmPolicy.smartDataSignatureHashAlgorithmPolicy(), SymmetricKeyAlgorithmPolicy.symmetricKeyEncryptionPolicy2022(), SymmetricKeyAlgorithmPolicy.symmetricKeyDecryptionPolicy2022(), CompressionAlgorithmPolicy.anyCompressionAlgorithmPolicy(), @@ -89,6 +91,30 @@ class Policy( fun defaultHashAlgorithm() = defaultHashAlgorithm companion object { + // https://sequoia-pgp.org/blog/2023/02/01/202302-happy-sha1-day/ + // signature data which is not attacker-controlled is acceptable before 2023-02-01 + @JvmStatic + fun smartCertificationSignatureHashAlgorithmPolicy() = + HashAlgorithmPolicy( + HashAlgorithm.SHA512, + buildMap { + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_512, null) + put(HashAlgorithm.SHA3_256, null) + put(HashAlgorithm.SHA512, null) + put(HashAlgorithm.SHA384, null) + put(HashAlgorithm.SHA256, null) + put(HashAlgorithm.SHA224, null) + put( + HashAlgorithm.RIPEMD160, + DateUtil.parseUTCDate("2023-02-01 00:00:00 UTC")) + put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2023-02-01 00:00:00 UTC")) + put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")) + }) + + @JvmStatic + fun smartDataSignatureHashAlgorithmPolicy() = smartSignatureHashAlgorithmPolicy() + @JvmStatic fun smartSignatureHashAlgorithmPolicy() = HashAlgorithmPolicy( diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt index c7cbd6fd..0aa8b3b4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -235,12 +235,18 @@ abstract class SignatureValidator { signature: PGPSignature, policy: Policy ): Policy.HashAlgorithmPolicy { - val type = SignatureType.requireFromCode(signature.signatureType) - return when (type) { + return when (SignatureType.requireFromCode(signature.signatureType)) { SignatureType.CERTIFICATION_REVOCATION, SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy - else -> policy.signatureHashAlgorithmPolicy + SignatureType.GENERIC_CERTIFICATION, + SignatureType.NO_CERTIFICATION, + SignatureType.CASUAL_CERTIFICATION, + SignatureType.POSITIVE_CERTIFICATION, + SignatureType.DIRECT_KEY, + SignatureType.SUBKEY_BINDING, + SignatureType.PRIMARYKEY_BINDING -> policy.certificationSignatureHashAlgorithmPolicy + else -> policy.dataSignatureHashAlgorithmPolicy } } 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 bfd330aa..1dd974f1 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 @@ -207,7 +207,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } @@ -237,7 +237,7 @@ public class SigningTest { SubkeyIdentifier signingKey = sigs.keySet().iterator().next(); PGPSignature signature = sigs.get(signingKey).iterator().next(); - assertEquals(PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), + assertEquals(PGPainless.getPolicy().getDataSignatureHashAlgorithmPolicy().defaultHashAlgorithm().getAlgorithmId(), signature.getHashAlgorithm()); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java index 858c99a9..3b29e35d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/ManagePolicy.java @@ -43,7 +43,10 @@ public class ManagePolicy { @AfterEach public void resetPolicy() { // Policy for hash algorithms in non-revocation signatures - PGPainless.getPolicy().setSignatureHashAlgorithmPolicy( + PGPainless.getPolicy().setCertificationSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); + // Policy for hash algorithms in data signatures + PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( Policy.HashAlgorithmPolicy.static2022SignatureHashAlgorithmPolicy()); // Policy for hash algorithms in revocation signatures PGPainless.getPolicy().setRevocationSignatureHashAlgorithmPolicy( @@ -83,7 +86,7 @@ public class ManagePolicy { // Get PGPainless' policy singleton Policy policy = PGPainless.getPolicy(); - Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); + Policy.HashAlgorithmPolicy sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // Per default, non-revocation signatures using SHA-1 are rejected assertFalse(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); @@ -95,9 +98,9 @@ public class ManagePolicy { // List of acceptable hash algorithms Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1)); // Set the hash algo policy as policy for non-revocation signatures - policy.setSignatureHashAlgorithmPolicy(customPolicy); + policy.setDataSignatureHashAlgorithmPolicy(customPolicy); - sigHashAlgoPolicy = policy.getSignatureHashAlgorithmPolicy(); + sigHashAlgoPolicy = policy.getDataSignatureHashAlgorithmPolicy(); assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA512)); // SHA-1 is now acceptable as well assertTrue(sigHashAlgoPolicy.isAcceptable(HashAlgorithm.SHA1)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java index 31092c28..6e90847d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicySetterTest.java @@ -16,9 +16,15 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm; public class PolicySetterTest { @Test - public void testSetSignatureHashAlgorithmPolicy_NullFails() { + public void testSetCertificationSignatureHashAlgorithmPolicy_NullFails() { Policy policy = Policy.getInstance(); - assertThrows(NullPointerException.class, () -> policy.setSignatureHashAlgorithmPolicy(null)); + assertThrows(NullPointerException.class, () -> policy.setCertificationSignatureHashAlgorithmPolicy(null)); + } + + @Test + public void testSetDataSignatureHashAlgorithmPolicy_NullFails() { + Policy policy = Policy.getInstance(); + assertThrows(NullPointerException.class, () -> policy.setDataSignatureHashAlgorithmPolicy(null)); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index aa7078e4..9ff4df85 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -44,7 +44,7 @@ public class PolicyTest { sigHashAlgoMap.put(HashAlgorithm.SHA256, null); sigHashAlgoMap.put(HashAlgorithm.SHA224, null); sigHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + policy.setCertificationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); Map revHashAlgoMap = new HashMap<>(); revHashAlgoMap.put(HashAlgorithm.SHA512, null); @@ -107,40 +107,40 @@ public class PolicyTest { @Test public void testAcceptableSignatureHashAlgorithm() { - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); // Usage date before termination date -> acceptable - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableSignatureHashAlgorithm() { - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test public void testDefaultSignatureHashAlgorithm() { - assertEquals(HashAlgorithm.SHA512, policy.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); + assertEquals(HashAlgorithm.SHA512, policy.getCertificationSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); } @Test public void testAcceptableRevocationSignatureHashAlgorithm() { assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384)); assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384.getAlgorithmId())); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); - assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableRevocationSignatureHashAlgorithm() { assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160)); assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160.getAlgorithmId())); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test @@ -181,8 +181,8 @@ public class PolicyTest { @Test public void testUnknownSignatureHashAlgorithmIsNotAcceptable() { - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1)); - assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1)); + assertFalse(policy.getCertificationSignatureHashAlgorithmPolicy().isAcceptable(-1, new Date())); } @Test From ce51f4b8ccf87d219f2ffa4f7ae0e181d2cff7ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 24 Jan 2024 11:22:29 +0100 Subject: [PATCH 223/351] Add documentation to AEAD Algorithms --- .../org/pgpainless/algorithm/AEADAlgorithm.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt index 253b37dd..6a3a6214 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt @@ -5,8 +5,25 @@ package org.pgpainless.algorithm enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) { + + /** + * Encrypt-then-Authenticate-then-Translate mode. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-eax-mode + */ EAX(1, 16, 16), + + /** + * Offset-Codebook mode. OCB is mandatory to implement in crypto-refresh. Favored by GnuPG. Is + * not yet FIPS compliant, but supported by most implementations and therefore favorable. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-ocb-mode + */ OCB(2, 15, 16), + + /** + * Galois/Counter-Mode. GCM is controversial. Some say it is hard to get right. Some + * implementations like GnuPG therefore avoid it. May be necessary to achieve FIPS compliance. + * https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-gcm-mode + */ GCM(3, 12, 16), ; From acd7f15744517ff2c5c7b34905da307723fead54 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 24 Jan 2024 11:25:24 +0100 Subject: [PATCH 224/351] Rename LibrePGP features --- .../org/pgpainless/algorithm/Feature.kt | 34 ++++++++++++------- .../SignatureSubpacketsUtilTest.java | 6 ++-- .../subpackets/SignatureSubpacketsTest.java | 4 +-- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt index 3f9be1f5..ec1cfe21 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt @@ -7,7 +7,10 @@ package org.pgpainless.algorithm /** * An enumeration of features that may be set in the feature subpacket. * - * See [RFC4880: Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) + * See [RFC4880: Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) See + * [crypto-refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) + * See + * [LibrePGP: Features](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) */ enum class Feature(val featureId: Byte) { @@ -17,39 +20,44 @@ enum class Feature(val featureId: Byte) { * * See * [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14) + * See + * [Crypto-Refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) */ MODIFICATION_DETECTION(0x01), /** - * Support for Authenticated Encryption with Additional Data (AEAD). If a key announces this - * feature, it signals support for consuming AEAD Encrypted Data Packets. + * Support for OCB Encrypted Data (AEAD) as defined in LibrePGP (NON-STANDARD!) If a key + * announces this feature, it signals support for consuming OCB Encrypted Data Packets. * * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. * * See - * [AEAD Encrypted Data Packet](https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-) + * [LibrePGP: OCB Encrypted Data Packet](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) */ - GNUPG_AEAD_ENCRYPTED_DATA(0x02), + LIBREPGP_OCB_ENCRYPTED_DATA(0x02), /** - * If a key announces this feature, it is a version 5 public key. The version 5 format is - * similar to the version 4 format except for the addition of a count for the key material. This - * count helps to parse secret key packets (which are an extension of the public key packet - * format) in the case of an unknown algorithm. In addition, fingerprints of version 5 keys are - * calculated differently from version 4 keys. + * If a key announces this feature, it is a version 5 public key as defined in LibrePGP + * (NON-STANDARD!). The version 5 format is similar to the version 4 format except for the + * addition of a count for the key material. This count helps to parse secret key packets (which + * are an extension of the public key packet format) in the case of an unknown algorithm. In + * addition, fingerprints of version 5 keys are calculated differently from version 4 keys. * * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. * * See - * [Public-Key Packet Formats](https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats) + * [LibrePGP: Version 5 Public-Key Format](https://www.ietf.org/archive/id/draft-koch-librepgp-00.html#name-features) */ - GNUPG_VERSION_5_PUBLIC_KEY(0x04), + LIBREPGP_VERSION_5_PUBLIC_KEY(0x04), /** - * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. + * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. This packet + * protects data using AEAD encryption as defined in crypto-refresh. * * See * [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd) + * See + * [crypto-refresh: Features](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-features) */ MODIFICATION_DETECTION_2(0x08), ; diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java index 79149dd2..1caeb9e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -140,7 +140,7 @@ public class SignatureSubpacketsUtilTest { PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); - hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_ENCRYPTED_DATA)); + hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); generator.setHashedSubpackets(hashed.generate()); PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); @@ -148,8 +148,8 @@ public class SignatureSubpacketsUtilTest { assertNotNull(featureSet); assertEquals(2, featureSet.size()); assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); - assertTrue(featureSet.contains(Feature.GNUPG_AEAD_ENCRYPTED_DATA)); - assertFalse(featureSet.contains(Feature.GNUPG_VERSION_5_PUBLIC_KEY)); + assertTrue(featureSet.contains(Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); + assertFalse(featureSet.contains(Feature.LIBREPGP_VERSION_5_PUBLIC_KEY)); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java index beb87ec5..14eed4de 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketsTest.java @@ -388,7 +388,7 @@ public class SignatureSubpacketsTest { @Test public void testSetFeatures() { - wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_ENCRYPTED_DATA); + wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA); PGPSignatureSubpacketVector vector = SignatureSubpacketsHelper.toVector(wrapper); Features features = vector.getFeatures(); @@ -476,7 +476,7 @@ public class SignatureSubpacketsTest { subpackets.setKeyFlags(true, KeyFlag.toBitmask(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)); subpackets.addSignerUserID(false, "alice@test.test"); subpackets.setRevocationReason(true, RevocationAttributes.Reason.KEY_RETIRED.code(), "Key was retired."); - subpackets.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.GNUPG_AEAD_ENCRYPTED_DATA)); + subpackets.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.LIBREPGP_OCB_ENCRYPTED_DATA)); byte[] hash = new byte[128]; new Random().nextBytes(hash); subpackets.setSignatureTarget(false, publicKeys.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId(), hash); From b103f3ecc2a23a64693ddaf73fbcae794c60178e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 24 Jan 2024 11:33:29 +0100 Subject: [PATCH 225/351] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5296b4e..64e879a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ SPDX-License-Identifier: CC0-1.0 - Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) - Do not choke on unknown signature subpackets (thanks @Jerbell) - Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) +- Rename LibrePGP-related `Feature` enums: + - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` + - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` ## 1.6.5 - Add `SecretKeyRingEditor.setExpirationDateOfSubkey()` From bd26268533e40edff99bc83f3e98b808c03169cd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 24 Jan 2024 18:59:35 +0100 Subject: [PATCH 226/351] Add syntactic sugar for SignatureSubpacketCallback factory methods --- .../subpackets/BaseSignatureSubpackets.kt | 48 ++++++++++++++ .../subpackets/CertificationSubpackets.kt | 48 ++++++++++++++ .../RevocationSignatureSubpackets.kt | 52 +++++++++++++++ .../subpackets/SelfSignatureSubpackets.kt | 65 ++++++++++++++++++- .../subpackets/SignatureSubpackets.kt | 45 +++++++++++++ 5 files changed, 257 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt index 3586eecf..b9d7fb3f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.kt @@ -156,4 +156,52 @@ interface BaseSignatureSubpackets { fun addEmbeddedSignature(embeddedSignature: EmbeddedSignature): BaseSignatureSubpackets fun clearEmbeddedSignatures(): BaseSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [BaseSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = BaseSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: BaseSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: BaseSignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [BaseSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = BaseSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: BaseSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: BaseSignatureSubpackets) { + function(unhashedSubpackets) + } + } + } + } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt index c3edf2c4..bb1d6550 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/CertificationSubpackets.kt @@ -7,4 +7,52 @@ package org.pgpainless.signature.subpackets interface CertificationSubpackets : BaseSignatureSubpackets { interface Callback : SignatureSubpacketCallback + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [CertificationSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = CertificationSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: CertificationSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: CertificationSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [CertificationSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = CertificationSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: CertificationSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: CertificationSubpackets) { + function(unhashedSubpackets) + } + } + } + } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt index 2e152f9e..79807322 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.kt @@ -27,4 +27,56 @@ interface RevocationSignatureSubpackets : BaseSignatureSubpackets { ): RevocationSignatureSubpackets fun setRevocationReason(reason: RevocationReason?): RevocationSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [RevocationSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = RevocationSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: RevocationSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets( + hashedSubpackets: RevocationSignatureSubpackets + ) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [RevocationSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = RevocationSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: RevocationSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets( + unhashedSubpackets: RevocationSignatureSubpackets + ) { + function(unhashedSubpackets) + } + } + } + } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt index d1b2b428..318b7adf 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.kt @@ -16,7 +16,22 @@ import org.pgpainless.algorithm.* interface SelfSignatureSubpackets : BaseSignatureSubpackets { - interface Callback : SignatureSubpacketCallback + interface Callback : SignatureSubpacketCallback { + fun then(nextCallback: SignatureSubpacketCallback): Callback { + val currCallback = this + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + currCallback.modifyHashedSubpackets(hashedSubpackets) + nextCallback.modifyHashedSubpackets(hashedSubpackets) + } + + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + currCallback.modifyUnhashedSubpackets(unhashedSubpackets) + nextCallback.modifyUnhashedSubpackets(unhashedSubpackets) + } + } + } + } fun setKeyFlags(vararg keyflags: KeyFlag): SelfSignatureSubpackets @@ -116,4 +131,52 @@ interface SelfSignatureSubpackets : BaseSignatureSubpackets { fun setFeatures(isCritical: Boolean, vararg features: Feature): SelfSignatureSubpackets fun setFeatures(features: Features?): SelfSignatureSubpackets + + companion object { + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [SelfSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SelfSignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: SelfSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SelfSignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [SelfSignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SelfSignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: SelfSignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: SelfSignatureSubpackets) { + function(unhashedSubpackets) + } + } + } + } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt index e8fe2d94..886cedb6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpackets.kt @@ -89,6 +89,51 @@ class SignatureSubpackets : fun createEmptySubpackets(): SignatureSubpackets { return SignatureSubpackets() } + + /** Factory method for a [Callback] that does nothing. */ + @JvmStatic fun nop() = object : Callback {} + + /** + * Factory function with receiver, which returns a [Callback] that modifies the hashed + * subpacket area of a [SignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SignatureSubpackets.applyHashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyHashed(function: SignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyHashedSubpackets(hashedSubpackets: SignatureSubpackets) { + function(hashedSubpackets) + } + } + } + + /** + * Factory function with receiver, which returns a [Callback] that modifies the unhashed + * subpacket area of a [SignatureSubpackets] object. + * + * Can be called like this: + * ``` + * val callback = SignatureSubpackets.applyUnhashed { + * setCreationTime(date) + * ... + * } + * ``` + */ + @JvmStatic + fun applyUnhashed(function: SignatureSubpackets.() -> Unit): Callback { + return object : Callback { + override fun modifyUnhashedSubpackets(unhashedSubpackets: SignatureSubpackets) { + function(unhashedSubpackets) + } + } + } } override fun setRevocationReason( From 842c8980b95c31998899dbad33ec22332b097a64 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 2 Feb 2024 22:03:27 +0100 Subject: [PATCH 227/351] Downgrade logback to 1.2.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 707ca162..2f15c7ab 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.77' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.4.14' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '8.0.1' From 412f804eeef4f243f55a2db5ab66c4f0da9ad4d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 2 Feb 2024 22:06:11 +0100 Subject: [PATCH 228/351] PGPainless 1.6.6 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e879a0..d4aaca37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog + ## 2.0.0-SNAPSHOT - Bump `bcpg-jdk8on` to `1.77` - Bump `bcprov-jdk18on` to `1.77` @@ -23,6 +24,9 @@ SPDX-License-Identifier: CC0-1.0 - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` +## 1.6.6 +- Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426 + ## 1.6.5 - Add `SecretKeyRingEditor.setExpirationDateOfSubkey()` diff --git a/README.md b/README.md index 659ca896..484234d8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.6.2' + implementation 'org.pgpainless:pgpainless-core:1.6.6' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 55cf0cb2..e31f993b 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.6.2" + implementation "org.pgpainless:pgpainless-sop:1.6.6" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.6.2 + 1.6.6 ... diff --git a/version.gradle b/version.gradle index 2f15c7ab..4ec395ef 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '1.6.3' + shortVersion = '1.6.7' isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 From 252c520ca23ea64cb2831c4893971210c6b1933c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Feb 2024 14:43:38 +0100 Subject: [PATCH 229/351] Move org.bouncycastle classes to org.pgpainless.bouncycastle in order to avoid split package See https://github.com/pgpainless/pgpainless/issues/428 for more background information --- .../CachingBcPublicKeyDataDecryptorFactory.kt | 2 +- .../bouncycastle/extensions/PGPKeyRingExtensions.kt | 2 +- .../extensions/PGPPublicKeyExtensions.kt | 2 +- .../extensions/PGPSecretKeyExtensions.kt | 2 +- .../extensions/PGPSecretKeyRingExtensions.kt | 2 +- .../extensions/PGPSignatureExtensions.kt | 2 +- .../decryption_verification/ConsumerOptions.kt | 2 +- .../decryption_verification/MessageMetadata.kt | 2 +- .../OpenPgpMessageInputStream.kt | 8 ++++---- .../encryption_signing/BcHashContextSigner.kt | 2 +- .../encryption_signing/EncryptionResult.kt | 2 +- .../pgpainless/encryption_signing/SigningOptions.kt | 2 +- .../org/pgpainless/key/generation/KeyRingBuilder.kt | 2 +- .../main/kotlin/org/pgpainless/key/info/KeyInfo.kt | 8 ++++---- .../kotlin/org/pgpainless/key/info/KeyRingInfo.kt | 2 +- .../secretkeyring/SecretKeyRingEditor.kt | 6 +++--- .../org/pgpainless/key/protection/UnlockSecretKey.kt | 2 +- .../pgpainless/key/protection/fixes/S2KUsageFix.kt | 2 +- .../kotlin/org/pgpainless/key/util/KeyRingUtils.kt | 4 ++-- .../key/util/PublicKeyParameterValidationUtil.kt | 2 +- .../org/pgpainless/signature/SignatureUtils.kt | 2 +- .../signature/consumer/CertificateValidator.kt | 2 +- .../pgpainless/signature/consumer/SignaturePicker.kt | 6 +++--- .../signature/consumer/SignatureValidator.kt | 12 ++++++------ .../consumer/SignatureValidityComparator.kt | 2 +- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 1 + 26 files changed, 42 insertions(+), 41 deletions(-) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt (98%) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/extensions/PGPKeyRingExtensions.kt (98%) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/extensions/PGPPublicKeyExtensions.kt (97%) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/extensions/PGPSecretKeyExtensions.kt (98%) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt (98%) rename pgpainless-core/src/main/kotlin/org/{ => pgpainless}/bouncycastle/extensions/PGPSignatureExtensions.kt (98%) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt similarity index 98% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt index 3a6f5351..2f8ddd7b 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle +package org.pgpainless.bouncycastle import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt similarity index 98% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt index 611fa591..7126db66 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensions.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle.extensions +package org.pgpainless.bouncycastle.extensions import openpgp.openPgpKeyId import org.bouncycastle.openpgp.PGPKeyRing diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt similarity index 97% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt index 847f1cf1..832c5d6d 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle.extensions +package org.pgpainless.bouncycastle.extensions import org.bouncycastle.asn1.gnu.GNUObjectIdentifiers import org.bouncycastle.bcpg.ECDHPublicBCPGKey diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt similarity index 98% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt index 6049742c..84f81e64 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensions.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle.extensions +package org.pgpainless.bouncycastle.extensions import org.bouncycastle.bcpg.S2K import org.bouncycastle.openpgp.PGPException diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt similarity index 98% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt index 2116c748..99c562e6 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle.extensions +package org.pgpainless.bouncycastle.extensions import openpgp.openPgpKeyId import org.bouncycastle.openpgp.* diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt similarity index 98% rename from pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt index 5547e48b..df40c461 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.bouncycastle.extensions +package org.pgpainless.bouncycastle.extensions import java.util.* import openpgp.plusSeconds diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index dff33b2d..5007c8a5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -7,9 +7,9 @@ package org.pgpainless.decryption_verification import java.io.IOException import java.io.InputStream import java.util.* -import org.bouncycastle.extensions.getPublicKeyFor import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy import org.pgpainless.key.SubkeyIdentifier diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt index ecdeadf0..f7238391 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/MessageMetadata.kt @@ -6,13 +6,13 @@ package org.pgpainless.decryption_verification import java.util.* import javax.annotation.Nonnull -import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPLiteralData import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.bouncycastle.extensions.matches import org.pgpainless.exception.MalformedOpenPgpMessageException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index df67ca0f..ca2159fc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -10,16 +10,16 @@ import java.io.OutputStream import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream import org.bouncycastle.bcpg.UnsupportedPacketVersionException -import org.bouncycastle.extensions.getPublicKeyFor -import org.bouncycastle.extensions.getSecretKeyFor -import org.bouncycastle.extensions.issuerKeyId -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.util.io.TeeInputStream import org.pgpainless.PGPainless import org.pgpainless.algorithm.* +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.bouncycastle.extensions.getSecretKeyFor +import org.pgpainless.bouncycastle.extensions.issuerKeyId +import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.decryption_verification.MessageMetadata.* import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil import org.pgpainless.decryption_verification.syntax_check.InputSymbol diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt index 68e41091..47aed2be 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/BcHashContextSigner.kt @@ -5,7 +5,6 @@ package org.pgpainless.encryption_signing import java.security.MessageDigest -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKeyRing @@ -13,6 +12,7 @@ import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPSignatureGenerator import org.pgpainless.PGPainless import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.key.protection.SecretKeyRingProtector class BcHashContextSigner { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index 24d89191..4f6e6978 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -5,13 +5,13 @@ package org.pgpainless.encryption_signing import java.util.* -import org.bouncycastle.extensions.matches import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.matches import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.MultiMap diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt index 089b2d28..e0fe2972 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -5,7 +5,6 @@ package org.pgpainless.encryption_signing import java.util.* -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless.Companion.getPolicy import org.pgpainless.PGPainless.Companion.inspectKeyRing @@ -13,6 +12,7 @@ import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator.Companion.negotiateSignatureHashAlgorithm +import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.exception.KeyException import org.pgpainless.exception.KeyException.* import org.pgpainless.implementation.ImplementationFactory diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 2eeab371..05adf7d9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -7,7 +7,6 @@ package org.pgpainless.key.generation import java.io.IOException import java.security.KeyPairGenerator import java.util.* -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.* import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor @@ -17,6 +16,7 @@ import org.bouncycastle.util.Strings import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.policy.Policy import org.pgpainless.provider.ProviderFactory diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt index f510af3e..75a35140 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyInfo.kt @@ -4,12 +4,12 @@ package org.pgpainless.key.info -import org.bouncycastle.extensions.getCurveName -import org.bouncycastle.extensions.hasDummyS2K -import org.bouncycastle.extensions.isDecrypted -import org.bouncycastle.extensions.isEncrypted import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSecretKey +import org.pgpainless.bouncycastle.extensions.getCurveName +import org.pgpainless.bouncycastle.extensions.hasDummyS2K +import org.pgpainless.bouncycastle.extensions.isDecrypted +import org.pgpainless.bouncycastle.extensions.isEncrypted @Deprecated("Deprecated in favor of extension functions to PGPSecretKey and PGPPublicKey.") class KeyInfo private constructor(val secretKey: PGPSecretKey?, val publicKey: PGPPublicKey) { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index ea5de18d..56dafe10 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -6,10 +6,10 @@ package org.pgpainless.key.info import java.util.* import openpgp.openPgpKeyId -import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless import org.pgpainless.algorithm.* +import org.pgpainless.bouncycastle.extensions.* import org.pgpainless.exception.KeyException.UnboundUserIdException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index ad24ec04..ec93c6d6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -10,9 +10,6 @@ import javax.annotation.Nonnull import kotlin.NoSuchElementException import openpgp.openPgpKeyId import org.bouncycastle.bcpg.sig.KeyExpirationTime -import org.bouncycastle.extensions.getKeyExpirationDate -import org.bouncycastle.extensions.publicKeyAlgorithm -import org.bouncycastle.extensions.requirePublicKey import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless import org.pgpainless.PGPainless.Companion.inspectKeyRing @@ -21,6 +18,9 @@ import org.pgpainless.algorithm.Feature import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator +import org.pgpainless.bouncycastle.extensions.getKeyExpirationDate +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.requirePublicKey import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.generation.KeyRingBuilder diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt index f9fe3e1d..b3b0308f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/UnlockSecretKey.kt @@ -7,12 +7,12 @@ package org.pgpainless.key.protection import kotlin.jvm.Throws import openpgp.openPgpKeyId -import org.bouncycastle.extensions.isEncrypted import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.isEncrypted import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.util.PublicKeyParameterValidationUtil diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt index 43cca885..a1a9f6c2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/protection/fixes/S2KUsageFix.kt @@ -5,10 +5,10 @@ package org.pgpainless.key.protection.fixes import org.bouncycastle.bcpg.SecretKeyPacket -import org.bouncycastle.extensions.unlock import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.bouncycastle.extensions.unlock import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 29f343c1..2736f625 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -9,10 +9,10 @@ import kotlin.jvm.Throws import openpgp.openPgpKeyId import org.bouncycastle.bcpg.S2K import org.bouncycastle.bcpg.SecretKeyPacket -import org.bouncycastle.extensions.certificate -import org.bouncycastle.extensions.requireSecretKey import org.bouncycastle.openpgp.* import org.bouncycastle.util.Strings +import org.pgpainless.bouncycastle.extensions.certificate +import org.pgpainless.bouncycastle.extensions.requireSecretKey import org.pgpainless.exception.MissingPassphraseException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.protection.SecretKeyRingProtector diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt index 57f0d423..a1e79bf3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/PublicKeyParameterValidationUtil.kt @@ -9,7 +9,6 @@ import java.io.IOException import java.math.BigInteger import java.security.SecureRandom import org.bouncycastle.bcpg.* -import org.bouncycastle.extensions.publicKeyAlgorithm import org.bouncycastle.openpgp.* import org.bouncycastle.util.Arrays import org.bouncycastle.util.io.Streams @@ -17,6 +16,7 @@ import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.algorithm.PublicKeyAlgorithm.Companion.requireFromId import org.pgpainless.algorithm.SignatureType import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm import org.pgpainless.exception.KeyIntegrityException import org.pgpainless.implementation.ImplementationFactory.Companion.getInstance diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index e84ed0d3..770dfc56 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -9,10 +9,10 @@ import java.io.InputStream import java.util.* import openpgp.plusSeconds import org.bouncycastle.bcpg.sig.KeyExpirationTime -import org.bouncycastle.extensions.* import org.bouncycastle.openpgp.* import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.io.Streams +import org.pgpainless.bouncycastle.extensions.* import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt index 43c92959..e28f99b6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -7,13 +7,13 @@ package org.pgpainless.signature.consumer import java.io.InputStream import java.util.* import openpgp.openPgpKeyId -import org.bouncycastle.extensions.issuerKeyId import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.issuerKeyId import org.pgpainless.exception.SignatureValidationException import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt index 56db7ee0..5952003e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignaturePicker.kt @@ -5,13 +5,13 @@ package org.pgpainless.signature.consumer import java.util.Date -import org.bouncycastle.extensions.getPublicKeyFor -import org.bouncycastle.extensions.isExpired -import org.bouncycastle.extensions.wasIssuedBy import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.getPublicKeyFor +import org.pgpainless.bouncycastle.extensions.isExpired +import org.pgpainless.bouncycastle.extensions.wasIssuedBy import org.pgpainless.exception.SignatureValidationException import org.pgpainless.policy.Policy diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt index 0aa8b3b4..e16ef158 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -8,12 +8,6 @@ import java.lang.Exception import java.util.Date import openpgp.formatUTC import openpgp.openPgpKeyId -import org.bouncycastle.extensions.fingerprint -import org.bouncycastle.extensions.isHardRevocation -import org.bouncycastle.extensions.isOfType -import org.bouncycastle.extensions.publicKeyAlgorithm -import org.bouncycastle.extensions.signatureExpirationDate -import org.bouncycastle.extensions.signatureHashAlgorithm import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.PGPSignature @@ -21,6 +15,12 @@ import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector import org.pgpainless.algorithm.KeyFlag import org.pgpainless.algorithm.SignatureSubpacket import org.pgpainless.algorithm.SignatureType +import org.pgpainless.bouncycastle.extensions.fingerprint +import org.pgpainless.bouncycastle.extensions.isHardRevocation +import org.pgpainless.bouncycastle.extensions.isOfType +import org.pgpainless.bouncycastle.extensions.publicKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.signatureExpirationDate +import org.pgpainless.bouncycastle.extensions.signatureHashAlgorithm import org.pgpainless.exception.SignatureValidationException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.OpenPgpFingerprint diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt index 38c409b7..1153b875 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidityComparator.kt @@ -4,8 +4,8 @@ package org.pgpainless.signature.consumer -import org.bouncycastle.extensions.isHardRevocation import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.bouncycastle.extensions.isHardRevocation /** * Comparator which sorts signatures based on an ordering and on revocation hardness. diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index b8eab5e4..362af7f2 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -18,6 +18,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.key.SubkeyIdentifier; From e933af94c7ef316d4bcbeccaafd10396abcc2023 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Feb 2024 14:52:48 +0100 Subject: [PATCH 230/351] Rename PublicKeyAlgorithm.EDDSA to EDDSA_LEGACY --- .../org/bouncycastle/extensions/PGPPublicKeyExtensions.kt | 4 ++-- .../kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt | 2 +- .../kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt | 2 +- .../src/main/kotlin/org/pgpainless/policy/Policy.kt | 2 +- .../src/test/java/org/pgpainless/example/GenerateKeys.java | 4 ++-- .../key/generation/GenerateEllipticCurveKeyTest.java | 2 +- .../key/modification/RefuseToAddWeakSubkeyTest.java | 2 +- .../pgpainless/signature/OnePassSignatureBracketingTest.java | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt index 847f1cf1..76f2899b 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -16,7 +16,7 @@ import org.pgpainless.key.generation.type.eddsa.EdDSACurve /** * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and - * [PublicKeyAlgorithm.EDDSA], this method returns the name of the underlying elliptic curve. + * [PublicKeyAlgorithm.EDDSA_LEGACY], this method returns the name of the underlying elliptic curve. * * For other key types or unknown curves, this method throws an [IllegalArgumentException]. * @@ -28,7 +28,7 @@ fun PGPPublicKey.getCurveName(): String { when (it) { PublicKeyAlgorithm.ECDSA -> publicKeyPacket.key as ECDSAPublicBCPGKey PublicKeyAlgorithm.ECDH -> publicKeyPacket.key as ECDHPublicBCPGKey - PublicKeyAlgorithm.EDDSA -> publicKeyPacket.key as EdDSAPublicBCPGKey + PublicKeyAlgorithm.EDDSA_LEGACY -> publicKeyPacket.key as EdDSAPublicBCPGKey else -> throw IllegalArgumentException("No an elliptic curve public key ($it).") } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt index 4a218673..8cf03420 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -60,7 +60,7 @@ enum class PublicKeyAlgorithm( DIFFIE_HELLMAN(21, false, true), /** Digital Signature Algorithm based on twisted Edwards Curves. */ - EDDSA(22, true, false), + EDDSA_LEGACY(22, true, false), ; fun isSigningCapable(): Boolean = signingCapable diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt index 6130328a..1c539129 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt @@ -10,7 +10,7 @@ import org.pgpainless.key.generation.type.KeyType class EdDSA private constructor(val curve: EdDSACurve) : KeyType { override val name = "EdDSA" - override val algorithm = PublicKeyAlgorithm.EDDSA + override val algorithm = PublicKeyAlgorithm.EDDSA_LEGACY override val bitStrength = curve.bitStrength override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt index ba022838..cb209025 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -374,7 +374,7 @@ class Policy( put(PublicKeyAlgorithm.ECDSA, 250) // Note: EdDSA is not mentioned in the BSI document. // We assume that the requirements are similar to other EC algorithms. - put(PublicKeyAlgorithm.EDDSA, 250) + put(PublicKeyAlgorithm.EDDSA_LEGACY, 250) // §7.2.1 put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000) // §7.2.2 diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java index dc9c51f2..7c7ec09a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java @@ -74,9 +74,9 @@ public class GenerateKeys { KeyRingInfo keyInfo = new KeyRingInfo(secretKey); assertEquals(3, keyInfo.getSecretKeys().size()); assertEquals(userId, keyInfo.getPrimaryUserId()); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyInfo.getPublicKey().getAlgorithm()); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyInfo.getSigningSubkeys().get(0).getAlgorithm()); assertEquals(PublicKeyAlgorithm.ECDH.getAlgorithmId(), keyInfo.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getAlgorithm()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index 8ea4877d..f8dd6b7c 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -38,7 +38,7 @@ public class GenerateEllipticCurveKeyTest { .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) .build(); - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); UnlockSecretKey.unlockSecretKey(keyRing.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java index f49a6271..04197d6f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/RefuseToAddWeakSubkeyTest.java @@ -67,7 +67,7 @@ public class RefuseToAddWeakSubkeyTest { minimalBitStrengths.put(PublicKeyAlgorithm.ECDSA, 250); // Note: EdDSA is not mentioned in the BSI document. // We assume that the requirements are similar to other EC algorithms. - minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA, 250); + minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA_LEGACY, 250); // §7.2.1 minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); // §7.2.2 diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java index 0042d14b..fd7c53e9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/OnePassSignatureBracketingTest.java @@ -144,7 +144,7 @@ public class OnePassSignatureBracketingTest { // 4 is hash algo assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), encoded[4]); // 5 is public key algo - assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), encoded[5]); + assertEquals(PublicKeyAlgorithm.EDDSA_LEGACY.getAlgorithmId(), encoded[5]); // [6,7,8,9,10,11,12,13] are key-id boolean last = i == signatures.size() - 1; From a8983232098b9e932449ce39507fdba4b3a7fcc0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Feb 2024 14:57:02 +0100 Subject: [PATCH 231/351] Rename KeyType.EDDSA to KeyType.EDDSA_LEGACY --- .../cli/commands/RoundTripEncryptDecryptCmdTest.java | 6 +++--- .../cli/commands/RoundTripSignVerifyCmdTest.java | 4 ++-- .../bouncycastle/extensions/PGPPublicKeyExtensions.kt | 5 +++-- .../org/pgpainless/key/generation/KeyRingTemplates.kt | 11 +++++++---- .../org/pgpainless/key/generation/type/KeyType.kt | 7 ++++--- .../key/generation/type/ecc/EllipticCurve.kt | 2 +- .../{eddsa/EdDSA.kt => eddsa_legacy/EdDSALegacy.kt} | 6 +++--- .../EdDSALegacyCurve.kt} | 4 ++-- .../encryption_signing/EncryptionOptionsTest.java | 6 +++--- .../encryption_signing/MultiSigningSubkeyTest.java | 6 +++--- .../pgpainless/encryption_signing/SigningTest.java | 10 +++++----- .../java/org/pgpainless/example/GenerateKeys.java | 4 ++-- .../key/generation/BrainpoolKeyGenerationTest.java | 6 +++--- .../key/generation/GenerateEllipticCurveKeyTest.java | 4 ++-- .../GenerateKeyWithCustomCreationDateTest.java | 4 ++-- .../GenerateKeyWithoutPrimaryKeyFlagsTest.java | 6 +++--- .../key/generation/GenerateKeyWithoutUserIdTest.java | 6 +++--- .../key/generation/IllegalKeyFlagsTest.java | 6 +++--- .../key/generation/KeyGenerationSubpacketsTest.java | 4 ++-- .../java/org/pgpainless/key/info/KeyRingInfoTest.java | 6 +++--- .../org/pgpainless/key/info/UserIdRevocationTest.java | 6 +++--- ...dSubkeyWithModifiedBindingSignatureSubpackets.java | 4 ++-- .../util/GuessPreferredHashAlgorithmTest.java | 4 ++-- .../org/pgpainless/key/KeyWithoutSelfSigsTest.kt | 7 ++++--- .../main/java/org/pgpainless/sop/GenerateKeyImpl.java | 6 +++--- .../java/org/pgpainless/sop/IncapableKeysTest.java | 8 ++++---- .../operation/PGPainlessChangeKeyPasswordTest.java | 6 +++--- 27 files changed, 80 insertions(+), 74 deletions(-) rename pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/{eddsa/EdDSA.kt => eddsa_legacy/EdDSALegacy.kt} (71%) rename pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/{eddsa/EdDSACurve.kt => eddsa_legacy/EdDSALegacyCurve.kt} (58%) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 1d89aeeb..ec03deef 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -24,7 +24,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -304,7 +304,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("No Crypt ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); @@ -323,7 +323,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 0ff83144..5eca5bc0 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -25,7 +25,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.slf4j.LoggerFactory; @@ -202,7 +202,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt index 76f2899b..5e40dd5d 100644 --- a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPPublicKeyExtensions.kt @@ -12,7 +12,7 @@ import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil import org.bouncycastle.openpgp.PGPPublicKey import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.OpenPgpFingerprint -import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve /** * For secret keys of types [PublicKeyAlgorithm.ECDSA], [PublicKeyAlgorithm.ECDH] and @@ -33,7 +33,8 @@ fun PGPPublicKey.getCurveName(): String { } } .let { - if (it.curveOID == GNUObjectIdentifiers.Ed25519) return EdDSACurve._Ed25519.curveName + if (it.curveOID == GNUObjectIdentifiers.Ed25519) + return EdDSALegacyCurve._Ed25519.curveName else it.curveOID } .let { it to ECUtil.getCurveName(it) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt index 5e9fa7fd..19df783b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -9,7 +9,7 @@ import org.pgpainless.PGPainless.Companion.buildKeyRing import org.pgpainless.algorithm.KeyFlag import org.pgpainless.key.generation.KeySpec.Companion.getBuilder import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.rsa.RsaLength import org.pgpainless.key.generation.type.xdh.XDHSpec import org.pgpainless.util.Passphrase @@ -131,7 +131,7 @@ class KeyRingTemplates { .apply { setPrimaryKey( getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) addSubkey( @@ -179,13 +179,16 @@ class KeyRingTemplates { ): PGPSecretKeyRing = buildKeyRing() .apply { - setPrimaryKey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + setPrimaryKey( + getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) addSubkey( getBuilder( KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - addSubkey(getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + addSubkey( + getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) setPassphrase(passphrase) if (userId != null) { addUserId(userId) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt index bc1497f9..0994d98c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -9,8 +9,8 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.ecc.EllipticCurve import org.pgpainless.key.generation.type.ecc.ecdh.ECDH import org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA -import org.pgpainless.key.generation.type.eddsa.EdDSA -import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacy +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.rsa.RSA import org.pgpainless.key.generation.type.rsa.RsaLength import org.pgpainless.key.generation.type.xdh.XDH @@ -100,7 +100,8 @@ interface KeyType { @JvmStatic fun ECDSA(curve: EllipticCurve): ECDSA = ECDSA.fromCurve(curve) - @JvmStatic fun EDDSA(curve: EdDSACurve): EdDSA = EdDSA.fromCurve(curve) + @JvmStatic + fun EDDSA_LEGACY(curve: EdDSALegacyCurve): EdDSALegacy = EdDSALegacy.fromCurve(curve) @JvmStatic fun XDH(curve: XDHSpec): XDH = XDH.fromSpec(curve) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt index d9b51cb3..d7267583 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt @@ -7,7 +7,7 @@ package org.pgpainless.key.generation.type.ecc /** * Elliptic curves for use with [org.pgpainless.key.generation.type.ecc.ecdh.ECDH] and * [org.pgpainless.key.generation.type.ecc.ecdsa.ECDSA]. For curve25519 related curve definitions - * see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa.EdDSACurve]. + * see [XDHSpec] and [org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve]. */ enum class EllipticCurve(val curveName: String, val bitStrength: Int) { _P256("prime256v1", 256), // prime256v1 is equivalent to P-256, see diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt similarity index 71% rename from pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt index 1c539129..e177de68 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSA.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacy.kt @@ -2,19 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type.eddsa +package org.pgpainless.key.generation.type.eddsa_legacy import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType -class EdDSA private constructor(val curve: EdDSACurve) : KeyType { +class EdDSALegacy private constructor(val curve: EdDSALegacyCurve) : KeyType { override val name = "EdDSA" override val algorithm = PublicKeyAlgorithm.EDDSA_LEGACY override val bitStrength = curve.bitStrength override val algorithmSpec = ECNamedCurveGenParameterSpec(curve.curveName) companion object { - @JvmStatic fun fromCurve(curve: EdDSACurve) = EdDSA(curve) + @JvmStatic fun fromCurve(curve: EdDSALegacyCurve) = EdDSALegacy(curve) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt similarity index 58% rename from pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt index 943c8237..2a32b8aa 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa/EdDSACurve.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/eddsa_legacy/EdDSALegacyCurve.kt @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type.eddsa +package org.pgpainless.key.generation.type.eddsa_legacy -enum class EdDSACurve(val curveName: String, val bitStrength: Int) { +enum class EdDSALegacyCurve(val curveName: String, val bitStrength: Int) { _Ed25519("ed25519", 256), ; diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 87dab34a..0d884f56 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -33,7 +33,7 @@ import org.pgpainless.exception.KeyException; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; @@ -51,7 +51,7 @@ public class EncryptionOptionsTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER) .build()) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS) .build()) @@ -138,7 +138,7 @@ public class EncryptionOptionsTest { public void testAddRecipient_KeyWithoutEncryptionKeyFails() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { EncryptionOptions options = new EncryptionOptions(); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("test@pgpainless.org") .build(); PGPPublicKeyRing publicKeys = KeyRingUtils.publicKeyRingFrom(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java index 5304c32c..625480e7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -21,7 +21,7 @@ import org.pgpainless.decryption_verification.SignatureVerification; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -52,8 +52,8 @@ public class MultiSigningSubkeyTest { @BeforeAll public static void generateKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { signingKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addUserId("Alice ") 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 1dd974f1..c62116b3 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 @@ -40,7 +40,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.util.KeyRingUtils; @@ -187,7 +187,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms()) .addUserId("Alice") .build(); @@ -217,7 +217,7 @@ public class SigningTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey( - KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(HashAlgorithm.MD5)) .addUserId("Alice") .build(); @@ -246,7 +246,7 @@ public class SigningTest { public void signingWithNonCapableKeyThrowsKeyCannotSignException() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Alice") .build(); @@ -262,7 +262,7 @@ public class SigningTest { public void signWithInvalidUserIdThrowsKeyValidationError() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addUserId("Alice") .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java index 7c7ec09a..d6bcb0b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/GenerateKeys.java @@ -27,7 +27,7 @@ import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.KeySpecBuilder; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.util.UserId; @@ -180,7 +180,7 @@ public class GenerateKeys { Passphrase passphrase = Passphrase.fromPassword("1nters3x"); PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), // The primary key MUST carry the CERTIFY_OTHER flag, but CAN carry additional flags KeyFlag.CERTIFY_OTHER)) // Add the first subkey (in this case encryption) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java index 969a0587..1e2d9ad8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java @@ -24,7 +24,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyInfo; @@ -71,7 +71,7 @@ public class BrainpoolKeyGenerationTest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( KeyType.ECDSA(EllipticCurve._BRAINPOOLP384R1), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder( KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addSubkey(KeySpec.getBuilder( @@ -99,7 +99,7 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey eddsaSub = iterator.next(); KeyInfo eddsaInfo = new KeyInfo(eddsaSub); - assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); + assertEquals(EdDSALegacyCurve._Ed25519.getName(), eddsaInfo.getCurveName()); assertEquals(256, eddsaSub.getPublicKey().getBitStrength()); PGPSecretKey xdhSub = iterator.next(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index f8dd6b7c..9d5f5d0d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -17,7 +17,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; @@ -32,7 +32,7 @@ public class GenerateEllipticCurveKeyTest { throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing keyRing = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java index d2697b82..8c150287 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -22,7 +22,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.util.DateUtil; @@ -34,7 +34,7 @@ public class GenerateKeyWithCustomCreationDateTest { Date creationDate = DateUtil.parseUTCDate("2018-06-11 14:12:09 UTC"); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .setKeyCreationDate(creationDate)) // primary key with custom creation time .addUserId("Alice") .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java index 63f04a8f..9368ce01 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -34,7 +34,7 @@ import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.exception.KeyException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -43,8 +43,8 @@ public class GenerateKeyWithoutPrimaryKeyFlagsTest { @Test public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { - PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addUserId("Alice") .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java index e7884f3f..920dbb80 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -22,7 +22,7 @@ import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -47,8 +47,8 @@ public class GenerateKeyWithoutUserIdTest { Date now = new Date(); Date expirationDate = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) .setExpirationDate(expirationDate) .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java index 24ea4aa4..b921f0f9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.util.TestAllImplementations; @@ -29,9 +29,9 @@ public class IllegalKeyFlagsTest { KeyType.XDH(XDHSpec._X25519), KeyFlag.AUTHENTICATION)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.ENCRYPT_STORAGE)); + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_STORAGE)); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java index 231b0485..1772b8b1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java @@ -30,7 +30,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -113,7 +113,7 @@ public class KeyGenerationSubpacketsTest { List keysBefore = info.getPublicKeys(); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .addSubKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).build(), + .addSubKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build(), Passphrase.emptyPassphrase(), SecretKeyRingProtector.unprotectedKeys()) .done(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java index e5e452f2..34465bba 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/KeyRingInfoTest.java @@ -45,7 +45,7 @@ import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; import org.pgpainless.key.util.KeyRingUtils; @@ -224,7 +224,7 @@ public class KeyRingInfoTest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.ECDH(EllipticCurve._BRAINPOOLP384R1), KeyFlag.ENCRYPT_STORAGE)) @@ -560,7 +560,7 @@ public class KeyRingInfoTest { public void testGetExpirationDateForUse_NoSuchKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Alice") - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .build(); KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java index 0caf8b75..7c2073f4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java @@ -28,7 +28,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -41,7 +41,7 @@ public class UserIdRevocationTest { public void testRevocationWithoutRevocationAttributes() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) @@ -79,7 +79,7 @@ public class UserIdRevocationTest { public void testRevocationWithRevocationReason() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( - KeyType.EDDSA(EdDSACurve._Ed25519), + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId("primary@key.id") diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java index 1eb086fd..85cddfd6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/AddSubkeyWithModifiedBindingSignatureSubpackets.java @@ -29,7 +29,7 @@ import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; @@ -47,7 +47,7 @@ public class AddSubkeyWithModifiedBindingSignatureSubpackets { KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys); PGPKeyPair secretSubkey = KeyRingBuilder.generateKeyPair( - KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA).build()); + KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).build()); long secondsUntilExpiration = 1000; secretKeys = PGPainless.modifyKeyRing(secretKeys) diff --git a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java index ca7a06d1..e276ba8f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/util/GuessPreferredHashAlgorithmTest.java @@ -21,7 +21,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; public class GuessPreferredHashAlgorithmTest { @@ -30,7 +30,7 @@ public class GuessPreferredHashAlgorithmTest { public void guessPreferredHashAlgorithmsAssumesHashAlgoUsedBySelfSig() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .overridePreferredHashAlgorithms(new HashAlgorithm[] {}) .overridePreferredSymmetricKeyAlgorithms(new SymmetricKeyAlgorithm[] {}) diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt index bb3e2bd4..7cdb4c6e 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt @@ -18,7 +18,7 @@ import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.key.generation.KeySpec import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.xdh.XDHSpec import org.pgpainless.key.protection.SecretKeyRingProtector @@ -56,9 +56,10 @@ class KeyWithoutSelfSigsTest { fun generateKey() { val key = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519))) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) .addSubkey( - KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + KeySpec.getBuilder( + KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey( KeySpec.getBuilder( KeyType.XDH(XDHSpec._X25519), diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 03583891..27803c89 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -21,7 +21,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.util.ArmorUtils; @@ -123,8 +123,8 @@ public class GenerateKeyImpl implements GenerateKey { // XDH + EdDSA if (profile.equals(CURVE25519_PROFILE.getName())) { keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)); + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)); if (!signingOnly) { keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java index 5139bbf6..efcd51c4 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/IncapableKeysTest.java @@ -13,7 +13,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.util.ArmorUtils; import sop.SOP; import sop.exception.SOPGPException; @@ -38,15 +38,15 @@ public class IncapableKeysTest { public static void generateKeys() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing key = PGPainless.buildKeyRing() .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._P256), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Non Signing ") .build(); nonSigningKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); nonSigningCert = sop.extractCert().key(nonSigningKey).getBytes(); key = PGPainless.buildKeyRing() - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addUserId("Non Encryption ") .build(); nonEncryptionKey = ArmorUtils.toAsciiArmoredString(key).getBytes(StandardCharsets.UTF_8); diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java index c1fc2cd9..8a99fa0b 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.util.Passphrase; import sop.SOP; @@ -34,8 +34,8 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { @MethodSource("provideInstances") public void changePasswordOfKeyWithSeparateSubkeyPasswords(SOP sop) throws IOException, PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); Iterator keys = secretKeys.getPublicKeys(); From 020d411417f2ef3d4a0a6c80f13bb98ce06c334b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Feb 2024 15:12:29 +0100 Subject: [PATCH 232/351] Move CachingBcPublicKeyDataDecryptorFactory to org.pgpainless.decryption_verification package --- .../CachingBcPublicKeyDataDecryptorFactory.kt | 7 +++---- .../CachingBcPublicKeyDataDecryptorFactoryTest.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) rename pgpainless-core/src/main/kotlin/org/pgpainless/{bouncycastle => decryption_verification}/CachingBcPublicKeyDataDecryptorFactory.kt (89%) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt similarity index 89% rename from pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt index 2f8ddd7b..3d07065e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt @@ -1,13 +1,12 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub +// SPDX-FileCopyrightText: 2024 Paul Schaub // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.bouncycastle +package org.pgpainless.decryption_verification import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory import org.bouncycastle.util.encoders.Base64 -import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory import org.pgpainless.key.SubkeyIdentifier /** diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java index 362af7f2..10cf4b1f 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -18,7 +18,7 @@ import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; +import org.pgpainless.decryption_verification.CachingBcPublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.key.SubkeyIdentifier; From b756de3082406dc1946e2c0af17e4e526b5aec4a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 21 Feb 2024 15:19:33 +0100 Subject: [PATCH 233/351] Rename XDH to XDH_LEGACY --- .../cli/commands/RoundTripEncryptDecryptCmdTest.java | 4 ++-- .../cli/commands/RoundTripSignVerifyCmdTest.java | 4 ++-- .../org/pgpainless/key/generation/KeyRingTemplates.kt | 6 +++--- .../kotlin/org/pgpainless/key/generation/type/KeyType.kt | 6 +++--- .../type/{xdh/XDH.kt => xdh_legacy/XDHLegacy.kt} | 6 +++--- .../type/{xdh/XDHSpec.kt => xdh_legacy/XDHLegacySpec.kt} | 4 ++-- .../encryption_signing/EncryptionOptionsTest.java | 6 +++--- .../encryption_signing/MultiSigningSubkeyTest.java | 4 ++-- .../key/generation/BrainpoolKeyGenerationTest.java | 6 +++--- .../CertificationKeyMustBeAbleToCertifyTest.java | 4 ++-- .../key/generation/GenerateEllipticCurveKeyTest.java | 4 ++-- .../generation/GenerateKeyWithCustomCreationDateTest.java | 4 ++-- .../generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java | 4 ++-- .../key/generation/GenerateKeyWithoutUserIdTest.java | 4 ++-- .../pgpainless/key/generation/IllegalKeyFlagsTest.java | 8 ++++---- .../key/generation/KeyGenerationSubpacketsTest.java | 4 ++-- .../org/pgpainless/key/info/UserIdRevocationTest.java | 6 +++--- .../kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt | 4 ++-- .../src/main/java/org/pgpainless/sop/GenerateKeyImpl.java | 4 ++-- .../operation/PGPainlessChangeKeyPasswordTest.java | 4 ++-- 20 files changed, 48 insertions(+), 48 deletions(-) rename pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/{xdh/XDH.kt => xdh_legacy/XDHLegacy.kt} (72%) rename pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/{xdh/XDHSpec.kt => xdh_legacy/XDHLegacySpec.kt} (56%) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index ec03deef..9969298a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -25,7 +25,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -325,7 +325,7 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); File certFile = writeFile("cert.pgp", PGPainless.extractCertificate(secretKeys).getEncoded()); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java index 5eca5bc0..9dcb3aca 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripSignVerifyCmdTest.java @@ -26,7 +26,7 @@ import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyRingInfo; import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; @@ -203,7 +203,7 @@ public class RoundTripSignVerifyCmdTest extends CLITest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .addUserId("Cannot Sign ") .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); File keyFile = writeFile("key.pgp", secretKeys.getEncoded()); diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt index 19df783b..e790ab17 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -11,7 +11,7 @@ import org.pgpainless.key.generation.KeySpec.Companion.getBuilder import org.pgpainless.key.generation.type.KeyType import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.rsa.RsaLength -import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec import org.pgpainless.util.Passphrase class KeyRingTemplates { @@ -136,7 +136,7 @@ class KeyRingTemplates { KeyFlag.SIGN_DATA)) addSubkey( getBuilder( - KeyType.XDH(XDHSpec._X25519), + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) setPassphrase(passphrase) @@ -184,7 +184,7 @@ class KeyRingTemplates { KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) addSubkey( getBuilder( - KeyType.XDH(XDHSpec._X25519), + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) addSubkey( diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt index 0994d98c..ea145b7f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -13,8 +13,8 @@ import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacy import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve import org.pgpainless.key.generation.type.rsa.RSA import org.pgpainless.key.generation.type.rsa.RsaLength -import org.pgpainless.key.generation.type.xdh.XDH -import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacy +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec @Suppress("INAPPLICABLE_JVM_NAME") // https://youtrack.jetbrains.com/issue/KT-31420 interface KeyType { @@ -103,6 +103,6 @@ interface KeyType { @JvmStatic fun EDDSA_LEGACY(curve: EdDSALegacyCurve): EdDSALegacy = EdDSALegacy.fromCurve(curve) - @JvmStatic fun XDH(curve: XDHSpec): XDH = XDH.fromSpec(curve) + @JvmStatic fun XDH_LEGACY(curve: XDHLegacySpec): XDHLegacy = XDHLegacy.fromSpec(curve) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt similarity index 72% rename from pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt index 8a95fc3b..4f0408bc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDH.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacy.kt @@ -2,19 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type.xdh +package org.pgpainless.key.generation.type.xdh_legacy import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.generation.type.KeyType -class XDH private constructor(spec: XDHSpec) : KeyType { +class XDHLegacy private constructor(spec: XDHLegacySpec) : KeyType { override val name = "XDH" override val algorithm = PublicKeyAlgorithm.ECDH override val bitStrength = spec.bitStrength override val algorithmSpec = ECNamedCurveGenParameterSpec(spec.algorithmName) companion object { - @JvmStatic fun fromSpec(spec: XDHSpec) = XDH(spec) + @JvmStatic fun fromSpec(spec: XDHLegacySpec) = XDHLegacy(spec) } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt similarity index 56% rename from pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt index 36fcfcaa..cb634850 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh/XDHSpec.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/xdh_legacy/XDHLegacySpec.kt @@ -2,9 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.key.generation.type.xdh +package org.pgpainless.key.generation.type.xdh_legacy -enum class XDHSpec(val algorithmName: String, val curveName: String, val bitStrength: Int) { +enum class XDHLegacySpec(val algorithmName: String, val curveName: String, val bitStrength: Int) { _X25519("X25519", "curve25519", 256), ; diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 0d884f56..2c1cd9f6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -34,7 +34,7 @@ import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.util.Passphrase; @@ -53,9 +53,9 @@ public class EncryptionOptionsTest { secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS) .build()) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE) .build()) .addUserId("test@pgpainless.org") .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java index 625480e7..28097db6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MultiSigningSubkeyTest.java @@ -23,7 +23,7 @@ import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.util.MultiMap; @@ -55,7 +55,7 @@ public class MultiSigningSubkeyTest { .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addUserId("Alice ") .build(); signingCert = PGPainless.extractCertificate(signingKey); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java index 1e2d9ad8..77023908 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java @@ -26,7 +26,7 @@ import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.util.UserId; import org.pgpainless.util.Passphrase; @@ -73,7 +73,7 @@ public class BrainpoolKeyGenerationTest { KeyType.ECDSA(EllipticCurve._BRAINPOOLP384R1), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .addSubkey(KeySpec.getBuilder( KeyType.RSA(RsaLength._3072), KeyFlag.SIGN_DATA)) .addUserId(UserId.nameAndEmail("Alice", "alice@pgpainless.org")) @@ -104,7 +104,7 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey xdhSub = iterator.next(); KeyInfo xdhInfo = new KeyInfo(xdhSub); - assertEquals(XDHSpec._X25519.getCurveName(), xdhInfo.getCurveName()); + assertEquals(XDHLegacySpec._X25519.getCurveName(), xdhInfo.getCurveName()); assertEquals(256, xdhSub.getPublicKey().getBitStrength()); PGPSecretKey rsaSub = iterator.next(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java index 7b6710c5..02d5aed0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/CertificationKeyMustBeAbleToCertifyTest.java @@ -12,7 +12,7 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.TestAllImplementations; public class CertificationKeyMustBeAbleToCertifyTest { @@ -29,7 +29,7 @@ public class CertificationKeyMustBeAbleToCertifyTest { KeyType.ECDH(EllipticCurve._P256), KeyType.ECDH(EllipticCurve._P384), KeyType.ECDH(EllipticCurve._P521), - KeyType.XDH(XDHSpec._X25519) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519) }; for (KeyType type : typesIncapableOfCreatingVerifications) { assertThrows(IllegalArgumentException.class, () -> PGPainless diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index 9d5f5d0d..4cb992db 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -18,7 +18,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.UserId; @@ -34,7 +34,7 @@ public class GenerateEllipticCurveKeyTest { .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId(UserId.onlyEmail("alice@wonderland.lit").toString()) .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java index 8c150287..0ad564db 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithCustomCreationDateTest.java @@ -23,7 +23,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.ecc.EllipticCurve; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.DateUtil; public class GenerateKeyWithCustomCreationDateTest { @@ -33,7 +33,7 @@ public class GenerateKeyWithCustomCreationDateTest { throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { Date creationDate = DateUtil.parseUTCDate("2018-06-11 14:12:09 UTC"); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA) .setKeyCreationDate(creationDate)) // primary key with custom creation time .addUserId("Alice") diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java index 9368ce01..e477aeef 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutPrimaryKeyFlagsTest.java @@ -35,7 +35,7 @@ import org.pgpainless.exception.KeyException; import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; @@ -45,7 +45,7 @@ public class GenerateKeyWithoutPrimaryKeyFlagsTest { public void generateKeyWithoutCertifyKeyFlag_cannotCertifyThirdParties() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing().setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .addUserId("Alice") .build(); PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java index 920dbb80..e6a5c96a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -23,7 +23,7 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.timeframe.TestTimeFrameProvider; @@ -49,7 +49,7 @@ public class GenerateKeyWithoutUserIdTest { PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER).setKeyCreationDate(now)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA).setKeyCreationDate(now)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE).setKeyCreationDate(now)) .setExpirationDate(expirationDate) .build(); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java index b921f0f9..a00e58c7 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/IllegalKeyFlagsTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.TestAllImplementations; public class IllegalKeyFlagsTest { @@ -20,13 +20,13 @@ public class IllegalKeyFlagsTest { @ExtendWith(TestAllImplementations.class) public void testKeyCannotCarryFlagsTest() { assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.SIGN_DATA)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.SIGN_DATA)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.CERTIFY_OTHER)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.CERTIFY_OTHER)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.AUTHENTICATION)); + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.AUTHENTICATION)); assertThrows(IllegalArgumentException.class, () -> KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.ENCRYPT_COMMS)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java index 1772b8b1..238d1a40 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/KeyGenerationSubpacketsTest.java @@ -31,7 +31,7 @@ import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; @@ -130,7 +130,7 @@ public class KeyGenerationSubpacketsTest { assertNotNull(bindingSig.getHashedSubPackets().getEmbeddedSignatures().get(0)); secretKeys = PGPainless.modifyKeyRing(secretKeys) - .addSubKey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), + .addSubKey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS).build(), Passphrase.emptyPassphrase(), new SelfSignatureSubpackets.Callback() { @Override diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java index 7c2073f4..abb067d1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/info/UserIdRevocationTest.java @@ -29,7 +29,7 @@ import org.pgpainless.key.TestKeys; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnprotectedKeysProtector; @@ -44,7 +44,7 @@ public class UserIdRevocationTest { KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId("primary@key.id") .addUserId("secondary@key.id") .build(); @@ -81,7 +81,7 @@ public class UserIdRevocationTest { .setPrimaryKey(KeySpec.getBuilder( KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS)) .addUserId("primary@key.id") .addUserId("secondary@key.id") .build(); diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt index 7cdb4c6e..c7879483 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt @@ -19,7 +19,7 @@ import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.key.generation.KeySpec import org.pgpainless.key.generation.type.KeyType import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve -import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec import org.pgpainless.key.protection.SecretKeyRingProtector class KeyWithoutSelfSigsTest { @@ -62,7 +62,7 @@ class KeyWithoutSelfSigsTest { KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) .addSubkey( KeySpec.getBuilder( - KeyType.XDH(XDHSpec._X25519), + KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_STORAGE, KeyFlag.ENCRYPT_COMMS)) .build() diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 27803c89..f366d644 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -23,7 +23,7 @@ import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.Passphrase; import sop.Profile; @@ -126,7 +126,7 @@ public class GenerateKeyImpl implements GenerateKey { .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)); if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); + keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); } } // RSA 4096 diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java index 8a99fa0b..baf595d3 100644 --- a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/operation/PGPainlessChangeKeyPasswordTest.java @@ -14,7 +14,7 @@ import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve; -import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec; import org.pgpainless.util.Passphrase; import sop.SOP; import sop.testsuite.TestData; @@ -36,7 +36,7 @@ public class PGPainlessChangeKeyPasswordTest extends ChangeKeyPasswordTest { PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.CERTIFY_OTHER)) .addSubkey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) - .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH_LEGACY(XDHLegacySpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) .build(); Iterator keys = secretKeys.getPublicKeys(); long primaryKeyId = keys.next().getKeyID(); From 11cb7e21072e76af8d402cc43c31334ed959495d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Feb 2024 11:15:18 +0100 Subject: [PATCH 234/351] Update issue templates --- .github/ISSUE_TEMPLATE/cli-application.md | 35 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/library.md | 28 ++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/cli-application.md create mode 100644 .github/ISSUE_TEMPLATE/library.md diff --git a/.github/ISSUE_TEMPLATE/cli-application.md b/.github/ISSUE_TEMPLATE/cli-application.md new file mode 100644 index 00000000..68f35b74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cli-application.md @@ -0,0 +1,35 @@ +--- +name: CLI Application +about: Report an issue with the pgpainless-cli utility +title: '' +labels: 'module: cli' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `pgpainless-cli`: + +**Installation Source** + +- Debian Repository +- Built locally (`gradle build...`) + +**To Reproduce** + +1. `pgpainless-cli foo bar [...]`' +2. ... + +**Expected behavior** + + +**Additional context** + +``` +-----BEGIN PGP FOO BAR----- +... +``` diff --git a/.github/ISSUE_TEMPLATE/library.md b/.github/ISSUE_TEMPLATE/library.md new file mode 100644 index 00000000..74f5f666 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/library.md @@ -0,0 +1,28 @@ +--- +name: Library +about: Report an issue with the libraries pgpainless-core or pgpainless-sop +title: '' +labels: 'module: core' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `pgpainless-core`: +- `pgpainless-sop`: + +**To Reproduce** + +``` +Example Code Block with your Code +``` + +**Expected behavior** + + +**Additional context** + From 60ea98df006802c098f169feb1f16b423ce95682 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 20:38:12 +0100 Subject: [PATCH 235/351] Add Github Issue Templates to dep5 file --- .reuse/dep5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index c03bbcae..7703aa47 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -79,3 +79,8 @@ License: Apache-2.0 Files: pgpainless-cli/packaging/man/* Copyright: 2022 Paul Schaub License: Apache-2.0 + +# Github Issue Templates +Files: .github/ISSUE_TEMPLATE/* +Copyright: 2024 Paul Schaub +License: CC0-1.0 From dfbc56fe2487a9de5063ce88b8020114fda39f1c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 20:54:15 +0100 Subject: [PATCH 236/351] Add tests for PGPSecretKeyRingExtensions --- .../PGPSecretKeyRingExtensionsTest.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt new file mode 100644 index 00000000..e629a5a1 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.PGPainless +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.key.TestKeys +import org.pgpainless.key.protection.SecretKeyRingProtector +import java.io.ByteArrayOutputStream + +class PGPSecretKeyRingExtensionsTest { + + @Test + fun testHasPgpSecretKeyRing() { + val key = TestKeys.getEmilSecretKeyRing() + assertTrue(key.hasSecretKey(TestKeys.EMIL_KEY_ID)) + assertTrue(key.hasSecretKey(TestKeys.EMIL_FINGERPRINT)) + + assertFalse(key.hasSecretKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasSecretKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun testRequireSecretKey() { + val key = TestKeys.getEmilSecretKeyRing() + assertNotNull(key.requireSecretKey(TestKeys.EMIL_KEY_ID)) + assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT)) + + assertThrows { + key.requireSecretKey(TestKeys.ROMEO_KEY_ID) + } + assertThrows { + key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT) + } + } + + @Test + fun testGetSecretKeyForSignature() { + val key = TestKeys.getEmilSecretKeyRing() + val signer = PGPainless.encryptAndOrSign() + .onOutputStream(ByteArrayOutputStream()) + .withOptions( + ProducerOptions.sign(SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) + ) + ) + signer.write("Hello, World!\n".toByteArray()) + signer.close() + val sig = signer.result.detachedSignatures.first().value.first() + + assertNotNull(key.getSecretKeyFor(sig)) + assertNull(TestKeys.getRomeoSecretKeyRing().getSecretKeyFor(sig)) + } +} From e561d58562be08b942a5fb9b1eaccb2245a36208 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 21:05:34 +0100 Subject: [PATCH 237/351] Add tests for PGPSecretKeyExtensions --- .../extensions/PGPSecretKeyExtensionsTest.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt new file mode 100644 index 00000000..6edf3fa4 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.TestKeys +import org.pgpainless.util.Passphrase + +class PGPSecretKeyExtensionsTest { + + @Test + fun `can successfully unlock protected secret key`() { + val key = TestKeys.getCryptieSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { + secKey.unlock(TestKeys.CRYPTIE_PASSPHRASE) + } + assertNotNull(privKey) + } + + @Test + fun `cannot unlock protected secret key using empty passphrase`() { + val key = TestKeys.getCryptieSecretKeyRing() + val secKey = key.secretKey + + assertThrows { + secKey.unlock(Passphrase.emptyPassphrase()) + } + } + + @Test + fun `can successfully unlock unprotected secret key with unprotectedKeys protector`() { + val key = TestKeys.getEmilSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { + secKey.unlock() + } + assertNotNull(privKey) + } + + @Test + fun `can successfully unlock unprotected secret key with empty passphrase`() { + val key = TestKeys.getEmilSecretKeyRing() + val secKey = key.secretKey + + val privKey = assertDoesNotThrow { + secKey.unlock(Passphrase.emptyPassphrase()) + } + assertNotNull(privKey) + } + + @Test + fun `openPgpFingerprint returns fitting fingerprint`() { + val key = TestKeys.getEmilSecretKeyRing() + + assertEquals(TestKeys.EMIL_FINGERPRINT, key.openPgpFingerprint) + assertEquals(TestKeys.EMIL_FINGERPRINT, key.secretKey.openPgpFingerprint) + } + + @Test + fun `publicKeyAlgorithm returns fitting algorithm`() { + val key = TestKeys.getEmilSecretKeyRing() + assertEquals(PublicKeyAlgorithm.ECDSA, key.secretKey.publicKeyAlgorithm) + } +} From c89c47c49141e069a011627814d236b1e1f870d4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 21:17:03 +0100 Subject: [PATCH 238/351] Add tests for PGPPublicKeyExtensions --- .../extensions/PGPPublicKeyExtensionsTest.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt new file mode 100644 index 00000000..aab2bf91 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.key.TestKeys +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.ecc.EllipticCurve +import org.pgpainless.key.generation.type.eddsa.EdDSACurve + +class PGPPublicKeyExtensionsTest { + + @Test + fun `test getCurveName for all ECDSA curves`() { + for (curve in EllipticCurve.values()) { + val key = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(curve))) + .build() + .publicKey + + assertEquals(curve.curveName, key.getCurveName()) + } + } + + @Test + fun `test getCurveName for legacy EdDSA curves`() { + for (curve in EdDSACurve.values()) { + val key = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(curve))) + .build() + .publicKey + + assertEquals(curve.curveName, key.getCurveName()) + } + } + + @Test + fun `test getCurveName fails for non-curve keys`() { + // RSA + val key = TestKeys.getJulietPublicKeyRing() + assertEquals(PublicKeyAlgorithm.RSA_GENERAL, key.publicKey.publicKeyAlgorithm) + + assertThrows { + key.publicKey.getCurveName() + } + } + + @Test + fun `openPgpFingerprint returns fitting fingerprint`() { + val key = TestKeys.getEmilSecretKeyRing() + assertEquals(TestKeys.EMIL_FINGERPRINT, key.publicKey.openPgpFingerprint) + } +} From c2abc89d5e439139784275e4fb55e4b1798ca084 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 21:29:47 +0100 Subject: [PATCH 239/351] Add tests for PGPKeyRingExtensions --- .../extensions/PGPKeyRingExtensionsTest.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt new file mode 100644 index 00000000..8ff941ba --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.bouncycastle.extensions + +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.pgpainless.key.TestKeys + +class PGPKeyRingExtensionsTest { + + @Test + fun `public key ring has public key`() { + val key = TestKeys.getJulietPublicKeyRing() + assertTrue(key.hasPublicKey(TestKeys.JULIET_KEY_ID)) + assertTrue(key.hasPublicKey(TestKeys.JULIET_FINGERPRINT)) + + assertFalse(key.hasPublicKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasPublicKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun `secret key ring has public key`() { + val key = TestKeys.getJulietSecretKeyRing() + assertTrue(key.hasPublicKey(TestKeys.JULIET_KEY_ID)) + assertTrue(key.hasPublicKey(TestKeys.JULIET_FINGERPRINT)) + + assertFalse(key.hasPublicKey(TestKeys.ROMEO_KEY_ID)) + assertFalse(key.hasPublicKey(TestKeys.ROMEO_FINGERPRINT)) + } + + @Test + fun `test requirePublicKey on secret key ring`() { + val key = TestKeys.getJulietSecretKeyRing() + assertNotNull(assertDoesNotThrow { + key.requirePublicKey(TestKeys.JULIET_KEY_ID) + }) + assertNotNull(assertDoesNotThrow { + key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) + }) + + assertThrows { + key.requirePublicKey(TestKeys.ROMEO_KEY_ID) + } + assertThrows { + key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) + } + } + + @Test + fun `test requirePublicKey on public key ring`() { + val key = TestKeys.getJulietPublicKeyRing() + assertNotNull(assertDoesNotThrow { + key.requirePublicKey(TestKeys.JULIET_KEY_ID) + }) + assertNotNull(assertDoesNotThrow { + key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) + }) + + assertThrows { + key.requirePublicKey(TestKeys.ROMEO_KEY_ID) + } + assertThrows { + key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) + } + } +} From cbbd980554d2b445bed429f289dd4a76e039ee16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 5 Mar 2024 21:30:28 +0100 Subject: [PATCH 240/351] Spotless apply --- .../extensions/PGPKeyRingExtensionsTest.kt | 32 +++++-------------- .../extensions/PGPPublicKeyExtensionsTest.kt | 22 ++++++------- .../extensions/PGPSecretKeyExtensionsTest.kt | 16 +++------- .../PGPSecretKeyRingExtensionsTest.kt | 24 ++++++-------- 4 files changed, 33 insertions(+), 61 deletions(-) diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt index 8ff941ba..49470c75 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPKeyRingExtensionsTest.kt @@ -37,36 +37,20 @@ class PGPKeyRingExtensionsTest { @Test fun `test requirePublicKey on secret key ring`() { val key = TestKeys.getJulietSecretKeyRing() - assertNotNull(assertDoesNotThrow { - key.requirePublicKey(TestKeys.JULIET_KEY_ID) - }) - assertNotNull(assertDoesNotThrow { - key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) - }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_KEY_ID) }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) }) - assertThrows { - key.requirePublicKey(TestKeys.ROMEO_KEY_ID) - } - assertThrows { - key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) - } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) } } @Test fun `test requirePublicKey on public key ring`() { val key = TestKeys.getJulietPublicKeyRing() - assertNotNull(assertDoesNotThrow { - key.requirePublicKey(TestKeys.JULIET_KEY_ID) - }) - assertNotNull(assertDoesNotThrow { - key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) - }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_KEY_ID) }) + assertNotNull(assertDoesNotThrow { key.requirePublicKey(TestKeys.JULIET_FINGERPRINT) }) - assertThrows { - key.requirePublicKey(TestKeys.ROMEO_KEY_ID) - } - assertThrows { - key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) - } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requirePublicKey(TestKeys.ROMEO_FINGERPRINT) } } } diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt index aab2bf91..e5e355c6 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPPublicKeyExtensionsTest.kt @@ -20,10 +20,11 @@ class PGPPublicKeyExtensionsTest { @Test fun `test getCurveName for all ECDSA curves`() { for (curve in EllipticCurve.values()) { - val key = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(curve))) - .build() - .publicKey + val key = + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(curve))) + .build() + .publicKey assertEquals(curve.curveName, key.getCurveName()) } @@ -32,10 +33,11 @@ class PGPPublicKeyExtensionsTest { @Test fun `test getCurveName for legacy EdDSA curves`() { for (curve in EdDSACurve.values()) { - val key = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(curve))) - .build() - .publicKey + val key = + PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(curve))) + .build() + .publicKey assertEquals(curve.curveName, key.getCurveName()) } @@ -47,9 +49,7 @@ class PGPPublicKeyExtensionsTest { val key = TestKeys.getJulietPublicKeyRing() assertEquals(PublicKeyAlgorithm.RSA_GENERAL, key.publicKey.publicKeyAlgorithm) - assertThrows { - key.publicKey.getCurveName() - } + assertThrows { key.publicKey.getCurveName() } } @Test diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt index 6edf3fa4..6dc1e4d4 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyExtensionsTest.kt @@ -21,9 +21,7 @@ class PGPSecretKeyExtensionsTest { val key = TestKeys.getCryptieSecretKeyRing() val secKey = key.secretKey - val privKey = assertDoesNotThrow { - secKey.unlock(TestKeys.CRYPTIE_PASSPHRASE) - } + val privKey = assertDoesNotThrow { secKey.unlock(TestKeys.CRYPTIE_PASSPHRASE) } assertNotNull(privKey) } @@ -32,9 +30,7 @@ class PGPSecretKeyExtensionsTest { val key = TestKeys.getCryptieSecretKeyRing() val secKey = key.secretKey - assertThrows { - secKey.unlock(Passphrase.emptyPassphrase()) - } + assertThrows { secKey.unlock(Passphrase.emptyPassphrase()) } } @Test @@ -42,9 +38,7 @@ class PGPSecretKeyExtensionsTest { val key = TestKeys.getEmilSecretKeyRing() val secKey = key.secretKey - val privKey = assertDoesNotThrow { - secKey.unlock() - } + val privKey = assertDoesNotThrow { secKey.unlock() } assertNotNull(privKey) } @@ -53,9 +47,7 @@ class PGPSecretKeyExtensionsTest { val key = TestKeys.getEmilSecretKeyRing() val secKey = key.secretKey - val privKey = assertDoesNotThrow { - secKey.unlock(Passphrase.emptyPassphrase()) - } + val privKey = assertDoesNotThrow { secKey.unlock(Passphrase.emptyPassphrase()) } assertNotNull(privKey) } diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt index e629a5a1..90641aff 100644 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/bouncycastle/extensions/PGPSecretKeyRingExtensionsTest.kt @@ -4,6 +4,7 @@ package org.pgpainless.bouncycastle.extensions +import java.io.ByteArrayOutputStream import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -15,7 +16,6 @@ import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.key.TestKeys import org.pgpainless.key.protection.SecretKeyRingProtector -import java.io.ByteArrayOutputStream class PGPSecretKeyRingExtensionsTest { @@ -35,24 +35,20 @@ class PGPSecretKeyRingExtensionsTest { assertNotNull(key.requireSecretKey(TestKeys.EMIL_KEY_ID)) assertNotNull(key.requireSecretKey(TestKeys.EMIL_FINGERPRINT)) - assertThrows { - key.requireSecretKey(TestKeys.ROMEO_KEY_ID) - } - assertThrows { - key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT) - } + assertThrows { key.requireSecretKey(TestKeys.ROMEO_KEY_ID) } + assertThrows { key.requireSecretKey(TestKeys.ROMEO_FINGERPRINT) } } @Test fun testGetSecretKeyForSignature() { val key = TestKeys.getEmilSecretKeyRing() - val signer = PGPainless.encryptAndOrSign() - .onOutputStream(ByteArrayOutputStream()) - .withOptions( - ProducerOptions.sign(SigningOptions.get() - .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key) - ) - ) + val signer = + PGPainless.encryptAndOrSign() + .onOutputStream(ByteArrayOutputStream()) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key))) signer.write("Hello, World!\n".toByteArray()) signer.close() val sig = signer.result.detachedSignatures.first().value.first() From a9cec16dc6034f3707a4867df7c82e47354e1726 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:50:40 +0100 Subject: [PATCH 241/351] Fix badge showing SOP Spec revision to show 8 instead of 7 --- pgpainless-sop/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index e31f993b..e7c93de5 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) From 265f72d99f92b5d47404eeb95b4abd20ca1e3ea3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 17:11:28 +0100 Subject: [PATCH 242/351] Fix OOM when detached signing large files Fixes #432 --- .../org/pgpainless/sop/DetachedSignImpl.java | 5 ++--- .../org/pgpainless/sop/NullOutputStream.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java index 034e13bc..400d2324 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java @@ -4,7 +4,6 @@ package org.pgpainless.sop; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -98,10 +97,10 @@ public class DetachedSignImpl implements DetachedSign { } } - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + OutputStream sink = new NullOutputStream(); try { EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(buffer) + .onOutputStream(sink) .withOptions(ProducerOptions.sign(signingOptions) .setAsciiArmor(armor)); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java new file mode 100644 index 00000000..9977ba28 --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import java.io.OutputStream; + +/** + * {@link OutputStream} that simply discards bytes written to it. + */ +public class NullOutputStream extends OutputStream { + @Override + public void write(int b) { + // NOP + } +} From 337b5d68b6f1b4fdb47675deacd0254393e93edf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 19 Mar 2024 15:51:09 +0100 Subject: [PATCH 243/351] Add Automatic-Module-Name to pgpainless-core and pgpainless-sop --- pgpainless-core/build.gradle | 7 +++++++ pgpainless-sop/build.gradle | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index 830e573d..bab6ecf1 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -27,3 +27,10 @@ dependencies { // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" } + +// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto +tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': 'org.pgpainless.core') + } +} diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index bb4ddaca..26beec67 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -34,3 +34,10 @@ test { useJUnitPlatform() environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory") } + +// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto +tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': 'org.pgpainless.sop') + } +} From 89038ebedf4f23ed95f90e6724b319688dd304b4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 14:13:58 +0100 Subject: [PATCH 244/351] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4aaca37..25d2d23a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ SPDX-License-Identifier: CC0-1.0 - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` +## 1.6.7 +- SOP: Fix OOM error when detached-signing large amounts of data (fix #432) +- Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428) +- Basic support for Java Modules for `pgpainless-core` and `pgpainless-sop` + - Added `Automatic-Module-Name` directive to gradle build files + ## 1.6.6 - Downgrade `logback-core` and `logback-classic` to `1.2.13` to fix #426 From fa5bdfcd8299da1093a7527ebe1fc28a27ad2b28 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:53:07 +0100 Subject: [PATCH 245/351] Throw BadData if KEYS are passed where CERTS are expected --- .../main/java/org/pgpainless/sop/KeyReader.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java index a2876a6e..4d676b33 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java @@ -6,8 +6,10 @@ package org.pgpainless.sop; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPRuntimeOperationException; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.pgpainless.PGPainless; +import org.pgpainless.key.collection.PGPKeyRingCollection; import sop.exception.SOPGPException; import java.io.IOException; @@ -44,19 +46,24 @@ class KeyReader { static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) throws IOException { - PGPPublicKeyRingCollection certs; + PGPKeyRingCollection certs; try { - certs = PGPainless.readKeyRing().publicKeyRingCollection(certIn); + certs = PGPainless.readKeyRing().keyRingCollection(certIn, false); } catch (IOException e) { String msg = e.getMessage(); if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) { throw new SOPGPException.BadData(e); } throw e; + } catch (PGPRuntimeOperationException e) { + throw new SOPGPException.BadData(e); } - if (requireContent && certs.size() == 0) { + if (certs.getPgpSecretKeyRingCollection().size() != 0) { + throw new SOPGPException.BadData("Secret key components encountered, while certificates were expected."); + } + if (requireContent && certs.getPgpPublicKeyRingCollection().size() == 0) { throw new SOPGPException.BadData(new PGPException("No cert data found.")); } - return certs; + return certs.getPgpPublicKeyRingCollection(); } } From 3ac273757aba6c1e78ac4801855e94fbbe96fa42 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:54:30 +0100 Subject: [PATCH 246/351] Bump sop-java to 10.0.0-SNAPSHOT and implement sopv interface subset --- pgpainless-sop/README.md | 2 +- .../java/org/pgpainless/sop/SOPVImpl.java | 40 +++++++++++++++++++ .../java/org/pgpainless/sop/VersionImpl.java | 11 ++++- .../java/org/pgpainless/sop/VersionTest.java | 7 ++++ version.gradle | 2 +- 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index e7c93de5..7cdf1ef3 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # PGPainless-SOP -[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) +[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/pgpainless-sop)](https://search.maven.org/artifact/org.pgpainless/pgpainless-sop) [![javadoc](https://javadoc.io/badge2/org.pgpainless/pgpainless-sop/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/pgpainless-sop) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java new file mode 100644 index 00000000..c1167a3b --- /dev/null +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.jetbrains.annotations.NotNull; +import org.pgpainless.util.ArmoredOutputStreamFactory; +import sop.SOPV; +import sop.operation.DetachedVerify; +import sop.operation.InlineVerify; +import sop.operation.Version; + +/** + * Implementation of the
sopv
interface subset using PGPainless. + */ +public class SOPVImpl implements SOPV { + + static { + ArmoredOutputStreamFactory.setVersionInfo(null); + } + + @NotNull + @Override + public DetachedVerify detachedVerify() { + return new DetachedVerifyImpl(); + } + + @NotNull + @Override + public InlineVerify inlineVerify() { + return new InlineVerifyImpl(); + } + + @NotNull + @Override + public Version version() { + return new VersionImpl(); + } +} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java index d7514861..3d3dd597 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java @@ -10,6 +10,8 @@ import java.util.Locale; import java.util.Properties; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.jetbrains.annotations.NotNull; +import sop.exception.SOPGPException; import sop.operation.Version; import javax.annotation.Nonnull; @@ -20,7 +22,9 @@ import javax.annotation.Nonnull; public class VersionImpl implements Version { // draft version - private static final int SOP_VERSION = 8; + private static final int SOP_VERSION = 10; + + private static final String SOPV_VERSION = "1.0"; @Override @Nonnull @@ -86,4 +90,9 @@ public class VersionImpl implements Version { return null; } + @NotNull + @Override + public String getSopVVersion() throws SOPGPException.UnsupportedOption { + return SOPV_VERSION; + } } diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java index 32d2c2e0..825b5e86 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VersionTest.java @@ -72,4 +72,11 @@ public class VersionTest { assertTrue(fullSopSpecVersion.endsWith(incompletenessRemarks)); } } + + @Test + public void testGetSopVVersion() { + String sopVVersion = sop.version().getSopVVersion(); + assertNotNull(sopVVersion); + assertTrue(sopVVersion.matches("\\d+\\.\\d+(\\.\\d+)*")); // X.Y or X.Y.Z... etc. + } } diff --git a/version.gradle b/version.gradle index 4ec395ef..f04e1457 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '8.0.1' + sopJavaVersion = '10.0.0-SNAPSHOT' } } From 44be5aa98130659960bdc6e54371d3efefc9e8e5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:54:44 +0100 Subject: [PATCH 247/351] Delegate verification operations to SOPVImpl --- .../src/main/java/org/pgpainless/sop/SOPImpl.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index d13ebc02..35a21d79 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -6,6 +6,7 @@ package org.pgpainless.sop; import org.pgpainless.util.ArmoredOutputStreamFactory; import sop.SOP; +import sop.SOPV; import sop.operation.Armor; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; @@ -36,10 +37,12 @@ public class SOPImpl implements SOP { ArmoredOutputStreamFactory.setVersionInfo(null); } + private final SOPV sopv = new SOPVImpl(); + @Override @Nonnull public Version version() { - return new VersionImpl(); + return sopv.version(); } @Override @@ -81,13 +84,13 @@ public class SOPImpl implements SOP { @Override @Nonnull public DetachedVerify detachedVerify() { - return new DetachedVerifyImpl(); + return sopv.detachedVerify(); } @Override @Nonnull public InlineVerify inlineVerify() { - return new InlineVerifyImpl(); + return sopv.inlineVerify(); } @Override From 194e4e145882e03fc7e45feb40987d4a35a41b3a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 14:17:37 +0100 Subject: [PATCH 248/351] Bump sop-java to 10.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index f04e1457..ec1bf828 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.0.0-SNAPSHOT' + sopJavaVersion = '10.0.0' } } From bd1949871a89795b3379277173fd6ed80786b2be Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 14:17:46 +0100 Subject: [PATCH 249/351] Update CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25d2d23a..c7bb5876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,12 @@ SPDX-License-Identifier: CC0-1.0 - Rewrote most of the codebase in Kotlin - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) - `pgpainless-sop`, `pgpainless-cli` - - Bump `sop-java` to `8.0.1`, implementing [SOP Spec Revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html) + - Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty + - Separate signature verification operations into `SOPV` interface + - Add `version --sopv` option + - Throw `BadData` error when passing KEYS where CERTS are expected. - Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) - Do not choke on unknown signature subpackets (thanks @Jerbell) - Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) From 806665058432163168fe9e2dd5884623d1c8409f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Mar 2024 11:00:16 +0100 Subject: [PATCH 250/351] Add comments --- .../src/main/java/org/pgpainless/sop/SOPImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java index 35a21d79..932175d3 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java @@ -30,6 +30,8 @@ import javax.annotation.Nonnull; *
 {@code
  * SOP sop = new SOPImpl();
  * }
+ * + * For a slimmed down interface that merely focuses on signature verification, see {@link SOPVImpl}. */ public class SOPImpl implements SOP { @@ -37,11 +39,13 @@ public class SOPImpl implements SOP { ArmoredOutputStreamFactory.setVersionInfo(null); } + // Delegate for sig verification operations private final SOPV sopv = new SOPVImpl(); @Override @Nonnull public Version version() { + // Delegate to SOPV return sopv.version(); } @@ -84,12 +88,14 @@ public class SOPImpl implements SOP { @Override @Nonnull public DetachedVerify detachedVerify() { + // Delegate to SOPV return sopv.detachedVerify(); } @Override @Nonnull public InlineVerify inlineVerify() { + // Delegate to SOPV return sopv.inlineVerify(); } From b393a90da47ddf6eb30beddc96c4a8e7ef7ecc97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Mar 2024 16:16:29 +0100 Subject: [PATCH 251/351] Port pgpainless-sop to Kotlin --- .../java/org/pgpainless/sop/ArmorImpl.java | 63 ------ .../pgpainless/sop/ChangeKeyPasswordImpl.java | 96 --------- .../java/org/pgpainless/sop/DearmorImpl.java | 45 ---- .../java/org/pgpainless/sop/DecryptImpl.java | 176 --------------- .../org/pgpainless/sop/DetachedSignImpl.java | 168 --------------- .../pgpainless/sop/DetachedVerifyImpl.java | 100 --------- .../java/org/pgpainless/sop/EncryptImpl.java | 201 ------------------ .../org/pgpainless/sop/ExtractCertImpl.java | 64 ------ .../org/pgpainless/sop/GenerateKeyImpl.java | 154 -------------- .../org/pgpainless/sop/InlineDetachImpl.java | 156 -------------- .../org/pgpainless/sop/InlineSignImpl.java | 135 ------------ .../org/pgpainless/sop/InlineVerifyImpl.java | 101 --------- .../java/org/pgpainless/sop/KeyReader.java | 69 ------ .../org/pgpainless/sop/ListProfilesImpl.java | 36 ---- .../MatchMakingSecretKeyRingProtector.java | 119 ----------- .../org/pgpainless/sop/NullOutputStream.java | 17 -- .../org/pgpainless/sop/RevokeKeyImpl.java | 123 ----------- .../main/java/org/pgpainless/sop/SOPImpl.java | 149 ------------- .../java/org/pgpainless/sop/SOPVImpl.java | 40 ---- .../pgpainless/sop/VerificationHelper.java | 52 ----- .../java/org/pgpainless/sop/VersionImpl.java | 98 --------- .../java/org/pgpainless/sop/package-info.java | 8 - .../kotlin/org/pgpainless/sop/ArmorImpl.kt | 54 +++++ .../pgpainless/sop/ChangeKeyPasswordImpl.kt | 81 +++++++ .../kotlin/org/pgpainless/sop/DearmorImpl.kt | 38 ++++ .../kotlin/org/pgpainless/sop/DecryptImpl.kt | 125 +++++++++++ .../org/pgpainless/sop/DetachedSignImpl.kt | 130 +++++++++++ .../org/pgpainless/sop/DetachedVerifyImpl.kt | 71 +++++++ .../kotlin/org/pgpainless/sop/EncryptImpl.kt | 155 ++++++++++++++ .../org/pgpainless/sop/ExtractCertImpl.kt | 47 ++++ .../org/pgpainless/sop/GenerateKeyImpl.kt | 136 ++++++++++++ .../org/pgpainless/sop/InlineDetachImpl.kt | 134 ++++++++++++ .../org/pgpainless/sop/InlineSignImpl.kt | 114 ++++++++++ .../org/pgpainless/sop/InlineVerifyImpl.kt | 73 +++++++ .../kotlin/org/pgpainless/sop/KeyReader.kt | 77 +++++++ .../org/pgpainless/sop/ListProfilesImpl.kt | 20 ++ .../sop/MatchMakingSecretKeyRingProtector.kt | 88 ++++++++ .../org/pgpainless/sop/NullOutputStream.kt | 31 +++ .../org/pgpainless/sop/RevokeKeyImpl.kt | 95 +++++++++ .../main/kotlin/org/pgpainless/sop/SOPImpl.kt | 56 +++++ .../kotlin/org/pgpainless/sop/SOPVImpl.kt | 24 +++ .../org/pgpainless/sop/VerificationHelper.kt | 48 +++++ .../kotlin/org/pgpainless/sop/VersionImpl.kt | 63 ++++++ 43 files changed, 1660 insertions(+), 2170 deletions(-) delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java delete mode 100644 pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt create mode 100644 pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java deleted file mode 100644 index 421dc7a7..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
armor
operation using PGPainless. - */ -public class ArmorImpl implements Armor { - - @Nonnull - @Override - @Deprecated - public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); - } - - @Nonnull - @Override - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - // By buffering the output stream, we can improve performance drastically - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - - // Determine nature of the given data - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(data); - openPgpIn.reset(); - - if (openPgpIn.isAsciiArmored()) { - // armoring already-armored data is an idempotent operation - Streams.pipeAll(openPgpIn, bufferedOutputStream); - bufferedOutputStream.flush(); - openPgpIn.close(); - return; - } - - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(bufferedOutputStream); - Streams.pipeAll(openPgpIn, armor); - bufferedOutputStream.flush(); - armor.close(); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java deleted file mode 100644 index 56613720..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ChangeKeyPasswordImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.ChangeKeyPassword; - -import javax.annotation.Nonnull; - -public class ChangeKeyPasswordImpl implements ChangeKeyPassword { - - private final MatchMakingSecretKeyRingProtector oldProtector = new MatchMakingSecretKeyRingProtector(); - private Passphrase newPassphrase = Passphrase.emptyPassphrase(); - private boolean armor = true; - - @Nonnull - @Override - public ChangeKeyPassword noArmor() { - armor = false; - return this; - } - - @Nonnull - @Override - public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { - oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)); - return this; - } - - @Nonnull - @Override - public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { - this.newPassphrase = Passphrase.fromPassword(newPassphrase); - return this; - } - - @Nonnull - @Override - public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected { - SecretKeyRingProtector newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase); - PGPSecretKeyRingCollection secretKeyRingCollection; - try { - secretKeyRingCollection = KeyReader.readSecretKeys(inputStream, true); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - - List updatedSecretKeys = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRingCollection) { - oldProtector.addSecretKey(secretKeys); - try { - PGPSecretKeyRing changed = KeyRingUtils.changePassphrase(null, secretKeys, oldProtector, newProtector); - updatedSecretKeys.add(changed); - } catch (MissingPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } catch (PGPException e) { - if (e.getMessage().contains("Exception decrypting key")) { - throw new SOPGPException.KeyIsProtected("Cannot unlock key " + OpenPgpFingerprint.of(secretKeys), e); - } - throw new RuntimeException("Cannot change passphrase of key " + OpenPgpFingerprint.of(secretKeys), e); - } - } - final PGPSecretKeyRingCollection changedSecretKeyCollection = new PGPSecretKeyRingCollection(updatedSecretKeys); - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); - changedSecretKeyCollection.encode(armorOut); - armorOut.close(); - } else { - changedSecretKeyCollection.encode(outputStream); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java deleted file mode 100644 index 5fd0a03b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.util.io.Streams; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
dearmor
operation using PGPainless. - */ -public class DearmorImpl implements Dearmor { - - @Nonnull - @Override - public Ready data(@Nonnull InputStream data) { - InputStream decoder; - try { - decoder = PGPUtil.getDecoderStream(data); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return new Ready() { - - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); - Streams.pipeAll(decoder, bufferedOutputStream); - bufferedOutputStream.flush(); - decoder.close(); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java deleted file mode 100644 index bc5081d7..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.util.Passphrase; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
decrypt
operation using PGPainless. - */ -public class DecryptImpl implements Decrypt { - - private final ConsumerOptions consumerOptions = ConsumerOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - - @Nonnull - @Override - public DecryptImpl verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotBefore(timestamp); - return this; - } - - @Nonnull - @Override - public DecryptImpl verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - consumerOptions.verifyNotAfter(timestamp); - return this; - } - - @Nonnull - @Override - public DecryptImpl verifyWithCert(@Nonnull InputStream certIn) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certs = KeyReader.readPublicKeys(certIn, true); - if (certs != null) { - consumerOptions.addVerificationCerts(certs); - } - return this; - } - - @Nonnull - @Override - public DecryptImpl withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - consumerOptions.setSessionKey( - new org.pgpainless.util.SessionKey( - SymmetricKeyAlgorithm.requireFromId(sessionKey.getAlgorithm()), - sessionKey.getKey())); - return this; - } - - @Nonnull - @Override - public DecryptImpl withPassword(@Nonnull String password) { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)); - String withoutTrailingWhitespace = removeTrailingWhitespace(password); - if (!password.equals(withoutTrailingWhitespace)) { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(withoutTrailingWhitespace)); - } - return this; - } - - private static String removeTrailingWhitespace(String passphrase) { - int i = passphrase.length() - 1; - // Find index of first non-whitespace character from the back - while (i > 0 && Character.isWhitespace(passphrase.charAt(i))) { - i--; - } - return passphrase.substring(0, i); - } - - @Nonnull - @Override - public DecryptImpl withKey(@Nonnull InputStream keyIn) throws SOPGPException.BadData, IOException, SOPGPException.UnsupportedAsymmetricAlgo { - PGPSecretKeyRingCollection secretKeyCollection = KeyReader.readSecretKeys(keyIn, true); - - for (PGPSecretKeyRing key : secretKeyCollection) { - protector.addSecretKey(key); - consumerOptions.addDecryptionKey(key, protector); - } - return this; - } - - @Nonnull - @Override - public Decrypt withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Nonnull - @Override - public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg { - - if (consumerOptions.getDecryptionKeys().isEmpty() && consumerOptions.getDecryptionPassphrases().isEmpty() && consumerOptions.getSessionKey() == null) { - throw new SOPGPException.MissingArg("Missing decryption key, passphrase or session key."); - } - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(ciphertext) - .withOptions(consumerOptions); - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.CannotDecrypt("No usable decryption key or password provided.", e); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { - throw new SOPGPException.BadData(e); - } finally { - // Forget passphrases after decryption - protector.clear(); - } - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - MessageMetadata metadata = decryptionStream.getMetadata(); - - if (!metadata.isEncrypted()) { - throw new SOPGPException.BadData("Data is not encrypted."); - } - - List verificationList = new ArrayList<>(); - for (SignatureVerification signatureVerification : metadata.getVerifiedInlineSignatures()) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - SessionKey sessionKey = null; - if (metadata.getSessionKey() != null) { - org.pgpainless.util.SessionKey sk = metadata.getSessionKey(); - sessionKey = new SessionKey( - (byte) sk.getAlgorithm().getAlgorithmId(), - sk.getKey() - ); - } - - return new DecryptionResult(sessionKey, verificationList); - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java deleted file mode 100644 index 400d2324..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedSignImpl.java +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionResult; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
sign
operation using PGPainless. - */ -public class DetachedSignImpl implements DetachedSign { - - private boolean armor = true; - private SignAs mode = SignAs.binary; - private final SigningOptions signingOptions = SigningOptions.get(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - public DetachedSign noArmor() { - armor = false; - return this; - } - - @Override - @Nonnull - public DetachedSign mode(@Nonnull SignAs mode) { - this.mode = mode; - return this; - } - - @Override - @Nonnull - public DetachedSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - @Nonnull - public DetachedSign withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult data(@Nonnull InputStream data) throws IOException { - for (PGPSecretKeyRing key : signingKeys) { - try { - signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)); - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - OutputStream sink = new NullOutputStream(); - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(sink) - .withOptions(ProducerOptions.sign(signingOptions) - .setAsciiArmor(armor)); - - return new ReadyWithResult() { - @Override - public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - EncryptionResult encryptionResult = signingStream.getResult(); - - // forget passphrases - protector.clear(); - - List signatures = new ArrayList<>(); - for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) { - signatures.addAll(encryptionResult.getDetachedSignatures().get(key)); - } - - OutputStream out; - if (armor) { - out = ArmoredOutputStreamFactory.get(outputStream); - } else { - out = outputStream; - } - for (PGPSignature sig : signatures) { - sig.encode(out); - } - out.close(); - outputStream.close(); // armor out does not close underlying stream - - return SigningResult.builder() - .setMicAlg(micAlgFromSignatures(signatures)) - .build(); - } - }; - - } catch (PGPException e) { - throw new RuntimeException(e); - } - - } - - private MicAlg micAlgFromSignatures(Iterable signatures) { - int algorithmId = 0; - for (PGPSignature signature : signatures) { - int sigAlg = signature.getHashAlgorithm(); - if (algorithmId == 0 || algorithmId == sigAlg) { - algorithmId = sigAlg; - } else { - return MicAlg.empty(); - } - } - return algorithmId == 0 ? MicAlg.empty() : MicAlg.fromHashAlgorithmId(algorithmId); - } - - private static DocumentSignatureType modeToSigType(SignAs mode) { - return mode == SignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT - : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java deleted file mode 100644 index 4c475bd0..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
verify
operation using PGPainless. - */ -public class DetachedVerifyImpl implements DetachedVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - @Nonnull - public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - @Nonnull - public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - @Nonnull - public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - @Nonnull - public DetachedVerifyImpl signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { - try { - options.addVerificationOfDetachedSignatures(signatures); - } catch (IOException | PGPException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Override - @Nonnull - public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - options.forceNonOpenPgpData(); - - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.drain(decryptionStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - for (SignatureVerification signatureVerification : metadata.getVerifiedDetachedSignatures()) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java deleted file mode 100644 index cbd5a108..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/EncryptImpl.java +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.encryption_signing.EncryptionOptions; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.KeyException; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.EncryptionResult; -import sop.Profile; -import sop.ReadyWithResult; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; -import sop.util.ProxyOutputStream; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
encrypt
operation using PGPainless. - */ -public class EncryptImpl implements Encrypt { - - private static final Profile RFC4880_PROFILE = new Profile("rfc4880", "Follow the packet format of rfc4880"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(RFC4880_PROFILE); - - EncryptionOptions encryptionOptions = EncryptionOptions.get(); - SigningOptions signingOptions = null; - MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final Set signingKeys = new HashSet<>(); - private String profile = RFC4880_PROFILE.getName(); // TODO: Use in future releases - - private EncryptAs encryptAs = EncryptAs.binary; - boolean armor = true; - - @Nonnull - @Override - public Encrypt noArmor() { - armor = false; - return this; - } - - @Nonnull - @Override - public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption { - this.encryptAs = mode; - return this; - } - - @Nonnull - @Override - public Encrypt signWith(@Nonnull InputStream keyIn) - throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - if (signingOptions == null) { - signingOptions = SigningOptions.get(); - } - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - if (keys.size() != 1) { - throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size())); - } - PGPSecretKeyRing signingKey = keys.iterator().next(); - - KeyRingInfo info = PGPainless.inspectKeyRing(signingKey); - if (info.getSigningSubkeys().isEmpty()) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(signingKey) + " cannot sign."); - } - - protector.addSecretKey(signingKey); - signingKeys.add(signingKey); - return this; - } - - @Nonnull - @Override - public Encrypt withKeyPassword(@Nonnull byte[] password) { - String passphrase = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(passphrase)); - return this; - } - - @Nonnull - @Override - public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - encryptionOptions.addPassphrase(Passphrase.fromPassword(password)); - return this; - } - - @Nonnull - @Override - public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { - try { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - encryptionOptions.addRecipients(certificates); - } catch (KeyException.UnacceptableEncryptionKeyException e) { - throw new SOPGPException.CertCannotEncrypt(e.getMessage(), e); - } catch (IOException e) { - throw new SOPGPException.BadData(e); - } - return this; - } - - @Nonnull - @Override - public Encrypt profile(@Nonnull String profileName) { - // sanitize profile name to make sure we only accept supported profiles - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - // profile is supported, return - this.profile = profile.getName(); - return this; - } - } - - // Profile is not supported, throw - throw new SOPGPException.UnsupportedProfile("encrypt", profileName); - } - - @Nonnull - @Override - public ReadyWithResult plaintext(@Nonnull InputStream plaintext) throws IOException { - if (!encryptionOptions.hasEncryptionMethod()) { - throw new SOPGPException.MissingArg("Missing encryption method."); - } - ProducerOptions producerOptions = signingOptions != null ? - ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) : - ProducerOptions.encrypt(encryptionOptions); - producerOptions.setAsciiArmor(armor); - producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs)); - - for (PGPSecretKeyRing signingKey : signingKeys) { - try { - signingOptions.addInlineSignature( - protector, - signingKey, - (encryptAs == EncryptAs.binary ? DocumentSignatureType.BINARY_DOCUMENT : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT) - ); - } catch (KeyException.UnacceptableSigningKeyException e) { - throw new SOPGPException.KeyCannotSign(); - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected(); - } catch (PGPException e) { - throw new SOPGPException.BadData(e); - } - } - - try { - ProxyOutputStream proxy = new ProxyOutputStream(); - EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() - .onOutputStream(proxy) - .withOptions(producerOptions); - - return new ReadyWithResult() { - @Override - public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - proxy.replaceOutputStream(outputStream); - Streams.pipeAll(plaintext, encryptionStream); - encryptionStream.close(); - // TODO: Extract and emit SessionKey - return new EncryptionResult(null); - } - }; - } catch (PGPException e) { - throw new IOException(); - } - } - - private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) { - switch (encryptAs) { - case binary: - return StreamEncoding.BINARY; - case text: - return StreamEncoding.UTF8; - } - throw new IllegalArgumentException("Invalid value encountered: " + encryptAs); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java deleted file mode 100644 index 6dce2578..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ExtractCertImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import org.pgpainless.util.ArmorUtils; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
extract-cert
operation using PGPainless. - */ -public class ExtractCertImpl implements ExtractCert { - - private boolean armor = true; - - @Override - @Nonnull - public ExtractCert noArmor() { - armor = false; - return this; - } - - @Override - @Nonnull - public Ready key(@Nonnull InputStream keyInputStream) throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyInputStream, true); - - List certs = new ArrayList<>(); - for (PGPSecretKeyRing key : keys) { - PGPPublicKeyRing cert = PGPainless.extractCertificate(key); - certs.add(cert); - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - - for (PGPPublicKeyRing cert : certs) { - OutputStream out = armor ? ArmorUtils.toAsciiArmoredStream(cert, outputStream) : outputStream; - cert.encode(out); - - if (armor) { - out.close(); - } - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java deleted file mode 100644 index 03583891..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.KeyFlag; -import org.pgpainless.key.generation.KeyRingBuilder; -import org.pgpainless.key.generation.KeySpec; -import org.pgpainless.key.generation.type.KeyType; -import org.pgpainless.key.generation.type.eddsa.EdDSACurve; -import org.pgpainless.key.generation.type.rsa.RsaLength; -import org.pgpainless.key.generation.type.xdh.XDHSpec; -import org.pgpainless.util.ArmorUtils; -import org.pgpainless.util.Passphrase; -import sop.Profile; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
generate-key
operation using PGPainless. - */ -public class GenerateKeyImpl implements GenerateKey { - - public static final Profile CURVE25519_PROFILE = new Profile("draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519"); - public static final Profile RSA4096_PROFILE = new Profile("rfc4880", "Generate 4096-bit RSA keys"); - - public static final List SUPPORTED_PROFILES = Arrays.asList(CURVE25519_PROFILE, RSA4096_PROFILE); - - private boolean armor = true; - private boolean signingOnly = false; - private final Set userIds = new LinkedHashSet<>(); - private Passphrase passphrase = Passphrase.emptyPassphrase(); - private String profile = CURVE25519_PROFILE.getName(); - - @Override - @Nonnull - public GenerateKey noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public GenerateKey userId(@Nonnull String userId) { - this.userIds.add(userId); - return this; - } - - @Override - @Nonnull - public GenerateKey withKeyPassword(@Nonnull String password) { - this.passphrase = Passphrase.fromPassword(password); - return this; - } - - @Override - @Nonnull - public GenerateKey profile(@Nonnull String profileName) { - // Sanitize the profile name to make sure we support the given profile - for (Profile profile : SUPPORTED_PROFILES) { - if (profile.getName().equals(profileName)) { - this.profile = profileName; - // return if we found the profile - return this; - } - } - - // profile not found, throw - throw new SOPGPException.UnsupportedProfile("generate-key", profileName); - } - - @Override - @Nonnull - public GenerateKey signingOnly() { - signingOnly = true; - return this; - } - - @Override - @Nonnull - public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - try { - final PGPSecretKeyRing key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly); - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - if (armor) { - ArmoredOutputStream armoredOutputStream = ArmorUtils.toAsciiArmoredStream(key, outputStream); - key.encode(armoredOutputStream); - armoredOutputStream.close(); - } else { - key.encode(outputStream); - } - } - }; - } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - - private PGPSecretKeyRing generateKeyWithProfile(String profile, Set userIds, Passphrase passphrase, boolean signingOnly) - throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { - KeyRingBuilder keyBuilder; - // XDH + EdDSA - if (profile.equals(CURVE25519_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - // RSA 4096 - else if (profile.equals(RSA4096_PROFILE.getName())) { - keyBuilder = PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) - .addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)); - if (!signingOnly) { - keyBuilder.addSubkey(KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)); - } - } - else { - // Missing else-if branch for profile. Oops. - throw new SOPGPException.UnsupportedProfile("generate-key", profile); - } - - for (String userId : userIds) { - keyBuilder.addUserId(userId); - } - if (!passphrase.isEmpty()) { - keyBuilder.setPassphrase(passphrase); - } - return keyBuilder.build(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java deleted file mode 100644 index 85c06d25..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineDetachImpl.java +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.decryption_verification.OpenPgpInputStream; -import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.exception.WrongConsumingMethodException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-detach
operation using PGPainless. - */ -public class InlineDetachImpl implements InlineDetach { - - private boolean armor = true; - - @Override - @Nonnull - public InlineDetach noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public ReadyWithResult message(@Nonnull InputStream messageInputStream) { - - return new ReadyWithResult() { - - private final ByteArrayOutputStream sigOut = new ByteArrayOutputStream(); - - @Override - public Signatures writeTo(@Nonnull OutputStream messageOutputStream) - throws SOPGPException.NoSignature, IOException { - - PGPSignatureList signatures = null; - OpenPgpInputStream pgpIn = new OpenPgpInputStream(messageInputStream); - - if (pgpIn.isNonOpenPgp()) { - throw new SOPGPException.BadData("Data appears to be non-OpenPGP."); - } - - // handle ASCII armor - if (pgpIn.isAsciiArmored()) { - ArmoredInputStream armorIn = new ArmoredInputStream(pgpIn); - - // Handle cleartext signature framework - if (armorIn.isClearText()) { - try { - signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, messageOutputStream); - if (signatures.isEmpty()) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - } catch (WrongConsumingMethodException e) { - throw new SOPGPException.BadData(e); - } - } - // else just dearmor - pgpIn = new OpenPgpInputStream(armorIn); - } - - // if data was not using cleartext signatures framework - if (signatures == null) { - - if (!pgpIn.isBinaryOpenPgp()) { - throw new SOPGPException.BadData("Data was containing ASCII armored non-OpenPGP data."); - } - - // handle binary OpenPGP data - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn); - Object next; - while ((next = objectFactory.nextObject()) != null) { - - if (next instanceof PGPOnePassSignatureList) { - // skip over ops - continue; - } - - if (next instanceof PGPLiteralData) { - // write out contents of literal data packet - PGPLiteralData literalData = (PGPLiteralData) next; - InputStream literalIn = literalData.getDataStream(); - Streams.pipeAll(literalIn, messageOutputStream); - literalIn.close(); - continue; - } - - if (next instanceof PGPCompressedData) { - // decompress compressed data - PGPCompressedData compressedData = (PGPCompressedData) next; - try { - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(compressedData.getDataStream()); - } catch (PGPException e) { - throw new SOPGPException.BadData("Cannot decompress PGPCompressedData", e); - } - continue; - } - - if (next instanceof PGPSignatureList) { - signatures = (PGPSignatureList) next; - } - } - } - - if (signatures == null) { - throw new SOPGPException.BadData("Data did not contain OpenPGP signatures."); - } - - // write out signatures - if (armor) { - ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(sigOut); - for (PGPSignature signature : signatures) { - signature.encode(armorOut); - } - armorOut.close(); - } else { - for (PGPSignature signature : signatures) { - signature.encode(sigOut); - } - } - - return new Signatures() { - @Override - public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { - Streams.pipeAll(new ByteArrayInputStream(sigOut.toByteArray()), signatureOutputStream); - } - }; - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java deleted file mode 100644 index 6ed1d471..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.algorithm.DocumentSignatureType; -import org.pgpainless.encryption_signing.EncryptionStream; -import org.pgpainless.encryption_signing.ProducerOptions; -import org.pgpainless.encryption_signing.SigningOptions; -import org.pgpainless.exception.KeyException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-sign
operation using PGPainless. - */ -public class InlineSignImpl implements InlineSign { - - private boolean armor = true; - private InlineSignAs mode = InlineSignAs.binary; - private final SigningOptions signingOptions = new SigningOptions(); - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private final List signingKeys = new ArrayList<>(); - - @Override - @Nonnull - public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { - this.mode = mode; - return this; - } - - @Override - @Nonnull - public InlineSign noArmor() { - this.armor = false; - return this; - } - - @Override - @Nonnull - public InlineSign key(@Nonnull InputStream keyIn) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { - PGPSecretKeyRingCollection keys = KeyReader.readSecretKeys(keyIn, true); - for (PGPSecretKeyRing key : keys) { - KeyRingInfo info = PGPainless.inspectKeyRing(key); - if (!info.isUsableForSigning()) { - throw new SOPGPException.KeyCannotSign("Key " + info.getFingerprint() + " does not have valid, signing capable subkeys."); - } - protector.addSecretKey(key); - signingKeys.add(key); - } - return this; - } - - @Override - @Nonnull - public InlineSign withKeyPassword(@Nonnull byte[] password) { - String string = new String(password, UTF8Util.UTF8); - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - for (PGPSecretKeyRing key : signingKeys) { - try { - if (mode == InlineSignAs.clearsigned) { - signingOptions.addDetachedSignature(protector, key, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT); - } else { - signingOptions.addInlineSignature(protector, key, modeToSigType(mode)); - } - } catch (KeyException.UnacceptableSigningKeyException | KeyException.MissingSecretKeyException e) { - throw new SOPGPException.KeyCannotSign("Key " + OpenPgpFingerprint.of(key) + " cannot sign.", e); - } catch (PGPException e) { - throw new SOPGPException.KeyIsProtected("Key " + OpenPgpFingerprint.of(key) + " cannot be unlocked.", e); - } - } - - ProducerOptions producerOptions = ProducerOptions.sign(signingOptions); - if (mode == InlineSignAs.clearsigned) { - producerOptions.setCleartextSigned(); - producerOptions.setAsciiArmor(true); - } else { - producerOptions.setAsciiArmor(armor); - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - try { - EncryptionStream signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(outputStream) - .withOptions(producerOptions); - - if (signingStream.isClosed()) { - throw new IllegalStateException("EncryptionStream is already closed."); - } - - Streams.pipeAll(data, signingStream); - signingStream.close(); - - // forget passphrases - protector.clear(); - } catch (PGPException e) { - throw new RuntimeException(e); - } - } - }; - } - - private static DocumentSignatureType modeToSigType(InlineSignAs mode) { - return mode == InlineSignAs.binary ? DocumentSignatureType.BINARY_DOCUMENT - : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java deleted file mode 100644 index 352bd7c8..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.util.io.Streams; -import org.pgpainless.PGPainless; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.MessageMetadata; -import org.pgpainless.decryption_verification.SignatureVerification; -import org.pgpainless.exception.MalformedOpenPgpMessageException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
inline-verify
operation using PGPainless. - */ -public class InlineVerifyImpl implements InlineVerify { - - private final ConsumerOptions options = ConsumerOptions.get(); - - @Override - @Nonnull - public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotBefore(timestamp); - return this; - } - - @Override - @Nonnull - public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - options.verifyNotAfter(timestamp); - return this; - } - - @Override - @Nonnull - public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - PGPPublicKeyRingCollection certificates = KeyReader.readPublicKeys(cert, true); - options.addVerificationCerts(certificates); - return this; - } - - @Override - @Nonnull - public ReadyWithResult> data(@Nonnull InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { - return new ReadyWithResult>() { - @Override - public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - DecryptionStream decryptionStream; - try { - decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(data) - .withOptions(options); - - Streams.pipeAll(decryptionStream, outputStream); - decryptionStream.close(); - - MessageMetadata metadata = decryptionStream.getMetadata(); - List verificationList = new ArrayList<>(); - - List verifications = metadata.isUsingCleartextSignatureFramework() ? - metadata.getVerifiedDetachedSignatures() : - metadata.getVerifiedInlineSignatures(); - - for (SignatureVerification signatureVerification : verifications) { - verificationList.add(VerificationHelper.mapVerification(signatureVerification)); - } - - if (!options.getCertificateSource().getExplicitCertificates().isEmpty()) { - if (verificationList.isEmpty()) { - throw new SOPGPException.NoSignature(); - } - } - - return verificationList; - } catch (MissingDecryptionMethodException e) { - throw new SOPGPException.BadData("Cannot verify encrypted message.", e); - } catch (MalformedOpenPgpMessageException | PGPException e) { - throw new SOPGPException.BadData(e); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java deleted file mode 100644 index 4d676b33..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/KeyReader.java +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPRuntimeOperationException; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.pgpainless.PGPainless; -import org.pgpainless.key.collection.PGPKeyRingCollection; -import sop.exception.SOPGPException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Reader for OpenPGP keys and certificates with error matching according to the SOP spec. - */ -class KeyReader { - - static PGPSecretKeyRingCollection readSecretKeys(InputStream keyInputStream, boolean requireContent) - throws IOException, SOPGPException.BadData { - PGPSecretKeyRingCollection keys; - try { - keys = PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream); - } catch (IOException e) { - String message = e.getMessage(); - if (message == null) { - throw e; - } - if (message.startsWith("unknown object in stream:") || - message.startsWith("invalid header encountered")) { - throw new SOPGPException.BadData(e); - } - throw e; - } - - if (requireContent && keys.size() == 0) { - throw new SOPGPException.BadData(new PGPException("No key data found.")); - } - - return keys; - } - - static PGPPublicKeyRingCollection readPublicKeys(InputStream certIn, boolean requireContent) - throws IOException { - PGPKeyRingCollection certs; - try { - certs = PGPainless.readKeyRing().keyRingCollection(certIn, false); - } catch (IOException e) { - String msg = e.getMessage(); - if (msg != null && (msg.startsWith("unknown object in stream:") || msg.startsWith("invalid header encountered"))) { - throw new SOPGPException.BadData(e); - } - throw e; - } catch (PGPRuntimeOperationException e) { - throw new SOPGPException.BadData(e); - } - if (certs.getPgpSecretKeyRingCollection().size() != 0) { - throw new SOPGPException.BadData("Secret key components encountered, while certificates were expected."); - } - if (requireContent && certs.getPgpPublicKeyRingCollection().size() == 0) { - throw new SOPGPException.BadData(new PGPException("No cert data found.")); - } - return certs.getPgpPublicKeyRingCollection(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java deleted file mode 100644 index e39c080d..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ListProfilesImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.List; - -import sop.Profile; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
list-profiles
operation using PGPainless. - * - */ -public class ListProfilesImpl implements ListProfiles { - - @Override - @Nonnull - public List subcommand(@Nonnull String command) { - - switch (command) { - case "generate-key": - return GenerateKeyImpl.SUPPORTED_PROFILES; - - case "encrypt": - return EncryptImpl.SUPPORTED_PROFILES; - - default: - throw new SOPGPException.UnsupportedProfile(command); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java deleted file mode 100644 index 67386bde..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.java +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.util.HashSet; -import java.util.Set; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.info.KeyInfo; -import org.pgpainless.key.protection.CachingSecretKeyRingProtector; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.util.Passphrase; - -import javax.annotation.Nullable; - -/** - * Implementation of the {@link SecretKeyRingProtector} which can be handed passphrases and keys separately, - * and which then matches up passphrases and keys when needed. - */ -public class MatchMakingSecretKeyRingProtector implements SecretKeyRingProtector { - - private final Set passphrases = new HashSet<>(); - private final Set keys = new HashSet<>(); - private final CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(); - - /** - * Add a single passphrase to the protector. - * - * @param passphrase passphrase - */ - public void addPassphrase(Passphrase passphrase) { - if (passphrase.isEmpty()) { - return; - } - - if (!passphrases.add(passphrase)) { - return; - } - - for (PGPSecretKeyRing key : keys) { - for (PGPSecretKey subkey : key) { - if (protector.hasPassphrase(subkey.getKeyID())) { - continue; - } - - testPassphrase(passphrase, subkey); - } - } - } - - /** - * Add a single {@link PGPSecretKeyRing} to the protector. - * - * @param key secret keys - */ - public void addSecretKey(PGPSecretKeyRing key) { - if (!keys.add(key)) { - return; - } - - for (PGPSecretKey subkey : key) { - if (KeyInfo.isDecrypted(subkey)) { - protector.addPassphrase(subkey.getKeyID(), Passphrase.emptyPassphrase()); - } else { - for (Passphrase passphrase : passphrases) { - testPassphrase(passphrase, subkey); - } - } - } - } - - private void testPassphrase(Passphrase passphrase, PGPSecretKey subkey) { - try { - PBESecretKeyDecryptor decryptor = ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase); - UnlockSecretKey.unlockSecretKey(subkey, decryptor); - protector.addPassphrase(subkey.getKeyID(), passphrase); - } catch (PGPException e) { - // wrong password - } - } - - @Override - public boolean hasPassphraseFor(long keyId) { - return protector.hasPassphrase(keyId); - } - - @Nullable - @Override - public PBESecretKeyDecryptor getDecryptor(long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Nullable - @Override - public PBESecretKeyEncryptor getEncryptor(long keyId) throws PGPException { - return protector.getEncryptor(keyId); - } - - /** - * Clear all known passphrases from the protector. - */ - public void clear() { - for (Passphrase passphrase : passphrases) { - passphrase.clear(); - } - - for (PGPSecretKeyRing key : keys) { - protector.forgetPassphrase(key); - } - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java deleted file mode 100644 index 9977ba28..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/NullOutputStream.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.OutputStream; - -/** - * {@link OutputStream} that simply discards bytes written to it. - */ -public class NullOutputStream extends OutputStream { - @Override - public void write(int b) { - // NOP - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java deleted file mode 100644 index 6b11b73a..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/RevokeKeyImpl.java +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.CharacterCodingException; -import java.util.ArrayList; -import java.util.List; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.bcpg.PublicKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.WrongPassphraseException; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface; -import org.pgpainless.key.util.KeyRingUtils; -import org.pgpainless.key.util.RevocationAttributes; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import org.pgpainless.util.Passphrase; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.operation.RevokeKey; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; - -public class RevokeKeyImpl implements RevokeKey { - - private final MatchMakingSecretKeyRingProtector protector = new MatchMakingSecretKeyRingProtector(); - private boolean armor = true; - - @Override - @Nonnull - public RevokeKey noArmor() { - this.armor = false; - return this; - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - @Override - @Nonnull - public RevokeKey withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - String string; - try { - string = UTF8Util.decodeUTF8(password); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Cannot UTF8-decode password."); - } - protector.addPassphrase(Passphrase.fromPassword(string)); - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream keys) throws SOPGPException.BadData { - PGPSecretKeyRingCollection secretKeyRings; - try { - secretKeyRings = KeyReader.readSecretKeys(keys, true); - } catch (IOException e) { - throw new SOPGPException.BadData("Cannot decode secret keys.", e); - } - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - protector.addSecretKey(secretKeys); - } - - final List revocationCertificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKeys : secretKeyRings) { - SecretKeyRingEditorInterface editor = PGPainless.modifyKeyRing(secretKeys); - try { - RevocationAttributes revocationAttributes = RevocationAttributes.createKeyRevocation() - .withReason(RevocationAttributes.Reason.NO_REASON) - .withoutDescription(); - if (secretKeys.getPublicKey().getVersion() == PublicKeyPacket.VERSION_6) { - PGPPublicKeyRing revocation = editor.createMinimalRevocationCertificate(protector, revocationAttributes); - revocationCertificates.add(revocation); - } else { - PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); - PGPSignature revocation = editor.createRevocation(protector, revocationAttributes); - certificate = KeyRingUtils.injectCertification(certificate, revocation); - revocationCertificates.add(certificate); - } - } catch (WrongPassphraseException e) { - throw new SOPGPException.KeyIsProtected("Missing or wrong passphrase for key " + OpenPgpFingerprint.of(secretKeys), e); - } - catch (PGPException e) { - throw new RuntimeException("Cannot generate revocation certificate.", e); - } - } - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - PGPPublicKeyRingCollection certificateCollection = new PGPPublicKeyRingCollection(revocationCertificates); - if (armor) { - ArmoredOutputStream out = ArmoredOutputStreamFactory.get(outputStream); - certificateCollection.encode(out); - out.close(); - } else { - certificateCollection.encode(outputStream); - } - } - }; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java deleted file mode 100644 index 932175d3..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPImpl.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.SOP; -import sop.SOPV; -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachedSign; -import sop.operation.DetachedVerify; -import sop.operation.InlineDetach; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.InlineSign; -import sop.operation.InlineVerify; -import sop.operation.ListProfiles; -import sop.operation.RevokeKey; -import sop.operation.Version; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
sop
API using PGPainless. - *
 {@code
- * SOP sop = new SOPImpl();
- * }
- * - * For a slimmed down interface that merely focuses on signature verification, see {@link SOPVImpl}. - */ -public class SOPImpl implements SOP { - - static { - ArmoredOutputStreamFactory.setVersionInfo(null); - } - - // Delegate for sig verification operations - private final SOPV sopv = new SOPVImpl(); - - @Override - @Nonnull - public Version version() { - // Delegate to SOPV - return sopv.version(); - } - - @Override - @Nonnull - public GenerateKey generateKey() { - return new GenerateKeyImpl(); - } - - @Override - @Nonnull - public ExtractCert extractCert() { - return new ExtractCertImpl(); - } - - @Override - @Nonnull - public DetachedSign sign() { - return detachedSign(); - } - - @Override - @Nonnull - public DetachedSign detachedSign() { - return new DetachedSignImpl(); - } - - @Override - @Nonnull - public InlineSign inlineSign() { - return new InlineSignImpl(); - } - - @Override - @Nonnull - public DetachedVerify verify() { - return detachedVerify(); - } - - @Override - @Nonnull - public DetachedVerify detachedVerify() { - // Delegate to SOPV - return sopv.detachedVerify(); - } - - @Override - @Nonnull - public InlineVerify inlineVerify() { - // Delegate to SOPV - return sopv.inlineVerify(); - } - - @Override - @Nonnull - public Encrypt encrypt() { - return new EncryptImpl(); - } - - @Override - @Nonnull - public Decrypt decrypt() { - return new DecryptImpl(); - } - - @Override - @Nonnull - public Armor armor() { - return new ArmorImpl(); - } - - @Override - @Nonnull - public Dearmor dearmor() { - return new DearmorImpl(); - } - - @Override - @Nonnull - public ListProfiles listProfiles() { - return new ListProfilesImpl(); - } - - @Override - @Nonnull - public RevokeKey revokeKey() { - return new RevokeKeyImpl(); - } - - @Override - @Nonnull - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordImpl(); - } - - @Override - @Nonnull - public InlineDetach inlineDetach() { - return new InlineDetachImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java deleted file mode 100644 index c1167a3b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SOPVImpl.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.jetbrains.annotations.NotNull; -import org.pgpainless.util.ArmoredOutputStreamFactory; -import sop.SOPV; -import sop.operation.DetachedVerify; -import sop.operation.InlineVerify; -import sop.operation.Version; - -/** - * Implementation of the
sopv
interface subset using PGPainless. - */ -public class SOPVImpl implements SOPV { - - static { - ArmoredOutputStreamFactory.setVersionInfo(null); - } - - @NotNull - @Override - public DetachedVerify detachedVerify() { - return new DetachedVerifyImpl(); - } - - @NotNull - @Override - public InlineVerify inlineVerify() { - return new InlineVerifyImpl(); - } - - @NotNull - @Override - public Version version() { - return new VersionImpl(); - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java deleted file mode 100644 index 126a5e3b..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VerificationHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import org.bouncycastle.openpgp.PGPSignature; -import org.pgpainless.decryption_verification.SignatureVerification; -import sop.Verification; -import sop.enums.SignatureMode; - -/** - * Helper class for shared methods related to {@link Verification Verifications}. - */ -public class VerificationHelper { - - /** - * Map a {@link SignatureVerification} object to a {@link Verification}. - * - * @param sigVerification signature verification - * @return verification - */ - public static Verification mapVerification(SignatureVerification sigVerification) { - return new Verification( - sigVerification.getSignature().getCreationTime(), - sigVerification.getSigningKey().getSubkeyFingerprint().toString(), - sigVerification.getSigningKey().getPrimaryKeyFingerprint().toString(), - getMode(sigVerification.getSignature()), - null); - } - - /** - * Map an OpenPGP signature type to a {@link SignatureMode} enum. - * Note: This method only maps {@link PGPSignature#BINARY_DOCUMENT} and {@link PGPSignature#CANONICAL_TEXT_DOCUMENT}. - * Other values are mapped to
null
. - * - * @param signature signature - * @return signature mode enum or null - */ - private static SignatureMode getMode(PGPSignature signature) { - - if (signature.getSignatureType() == PGPSignature.BINARY_DOCUMENT) { - return SignatureMode.binary; - } - - if (signature.getSignatureType() == PGPSignature.CANONICAL_TEXT_DOCUMENT) { - return SignatureMode.text; - } - - return null; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java deleted file mode 100644 index 3d3dd597..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/VersionImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.sop; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Locale; -import java.util.Properties; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.jetbrains.annotations.NotNull; -import sop.exception.SOPGPException; -import sop.operation.Version; - -import javax.annotation.Nonnull; - -/** - * Implementation of the
version
operation using PGPainless. - */ -public class VersionImpl implements Version { - - // draft version - private static final int SOP_VERSION = 10; - - private static final String SOPV_VERSION = "1.0"; - - @Override - @Nonnull - public String getName() { - return "PGPainless-SOP"; - } - - @Override - @Nonnull - public String getVersion() { - // See https://stackoverflow.com/a/50119235 - String version; - try { - Properties properties = new Properties(); - InputStream propertiesFileIn = getClass().getResourceAsStream("/version.properties"); - if (propertiesFileIn == null) { - throw new IOException("File version.properties not found."); - } - properties.load(propertiesFileIn); - version = properties.getProperty("version"); - } catch (IOException e) { - version = "DEVELOPMENT"; - } - return version; - } - - @Override - @Nonnull - public String getBackendVersion() { - return "PGPainless " + getVersion(); - } - - @Override - @Nonnull - public String getExtendedVersion() { - double bcVersion = new BouncyCastleProvider().getVersion(); - String FORMAT_VERSION = String.format("%02d", SOP_VERSION); - return getName() + " " + getVersion() + "\n" + - "https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop\n" + - "\n" + - "Implementation of the Stateless OpenPGP Protocol Version " + FORMAT_VERSION + "\n" + - "https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-" + FORMAT_VERSION + "\n" + - "\n" + - "Based on pgpainless-core " + getVersion() + "\n" + - "https://pgpainless.org\n" + - "\n" + - "Using " + String.format(Locale.US, "Bouncy Castle %.2f", bcVersion) + "\n" + - "https://www.bouncycastle.org/java.html"; - } - - @Override - public int getSopSpecRevisionNumber() { - return SOP_VERSION; - } - - @Override - public boolean isSopSpecImplementationIncomplete() { - return false; - } - - @Override - public String getSopSpecImplementationRemarks() { - return null; - } - - @NotNull - @Override - public String getSopVVersion() throws SOPGPException.UnsupportedOption { - return SOPV_VERSION; - } -} diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java deleted file mode 100644 index c0ce9cda..00000000 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the java-sop package using pgpainless-core. - */ -package org.pgpainless.sop; diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt new file mode 100644 index 00000000..bf7e63f1 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +import java.io.InputStream +import java.io.OutputStream +import kotlin.jvm.Throws +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.enums.ArmorLabel +import sop.exception.SOPGPException +import sop.operation.Armor + +/** Implementation of the `armor` operation using PGPainless. */ +class ArmorImpl : Armor { + + @Throws(SOPGPException.BadData::class) + override fun data(data: InputStream): Ready { + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + // By buffering the output stream, we can improve performance drastically + val bufferedOutputStream = BufferedOutputStream(outputStream) + + // Determine the nature of the given data + val openPgpIn = OpenPgpInputStream(data) + openPgpIn.reset() + + if (openPgpIn.isAsciiArmored) { + // armoring already-armored data is an idempotent operation + Streams.pipeAll(openPgpIn, bufferedOutputStream) + bufferedOutputStream.flush() + openPgpIn.close() + return + } + + val armor = ArmoredOutputStreamFactory.get(bufferedOutputStream) + Streams.pipeAll(openPgpIn, armor) + bufferedOutputStream.flush() + armor.close() + openPgpIn.close() + } + } + } + + @Deprecated("Setting custom labels is not supported.") + override fun label(label: ArmorLabel): Armor { + throw SOPGPException.UnsupportedOption("Setting custom Armor labels not supported.") + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt new file mode 100644 index 00000000..a9aaf1e4 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ChangeKeyPasswordImpl.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.ChangeKeyPassword + +/** Implementation of the `change-key-password` operation using PGPainless. */ +class ChangeKeyPasswordImpl : ChangeKeyPassword { + + private val oldProtector = MatchMakingSecretKeyRingProtector() + private var newPassphrase = Passphrase.emptyPassphrase() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val newProtector = SecretKeyRingProtector.unlockAnyKeyWith(newPassphrase) + val secretKeysCollection = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + val updatedSecretKeys = + secretKeysCollection + .map { secretKeys -> + oldProtector.addSecretKey(secretKeys) + try { + return@map KeyRingUtils.changePassphrase( + null, secretKeys, oldProtector, newProtector) + } catch (e: MissingPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + if (e.message?.contains("Exception decrypting key") == true) { + throw SOPGPException.KeyIsProtected( + "Cannot unlock key ${secretKeys.openPgpFingerprint}", e) + } + throw RuntimeException( + "Cannot change passphrase of key ${secretKeys.openPgpFingerprint}", e) + } + } + .let { PGPSecretKeyRingCollection(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + ArmoredOutputStreamFactory.get(outputStream).use { + updatedSecretKeys.encode(it) + } + } else { + updatedSecretKeys.encode(outputStream) + } + } + } + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + this.newPassphrase = Passphrase.fromPassword(newPassphrase) + } + + override fun noArmor(): ChangeKeyPassword = apply { armor = false } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + oldProtector.addPassphrase(Passphrase.fromPassword(oldPassphrase)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt new file mode 100644 index 00000000..9d196004 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DearmorImpl.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.BufferedOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.util.io.Streams +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.Dearmor + +/** Implementation of the `dearmor` operation using PGPainless. */ +class DearmorImpl : Dearmor { + + override fun data(data: InputStream): Ready { + val decoder = + try { + PGPUtil.getDecoderStream(data) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + BufferedOutputStream(outputStream).use { + Streams.pipeAll(decoder, it) + it.flush() + decoder.close() + } + } + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt new file mode 100644 index 00000000..f6a9fb1d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.exception.SOPGPException +import sop.operation.Decrypt +import sop.util.UTF8Util + +/** Implementation of the `decrypt` operation using PGPainless. */ +class DecryptImpl : Decrypt { + + private val consumerOptions = ConsumerOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + if (consumerOptions.getDecryptionKeys().isEmpty() && + consumerOptions.getDecryptionPassphrases().isEmpty() && + consumerOptions.getSessionKey() == null) { + throw SOPGPException.MissingArg("Missing decryption key, passphrase or session key.") + } + + val decryptionStream = + try { + PGPainless.decryptAndOrVerify() + .onInputStream(ciphertext) + .withOptions(consumerOptions) + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.CannotDecrypt( + "No usable decryption key or password provided.", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected() + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } finally { + // Forget passphrases after decryption + protector.clear() + } + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): DecryptionResult { + Streams.pipeAll(decryptionStream, outputStream) + decryptionStream.close() + + val metadata = decryptionStream.metadata + if (!metadata.isEncrypted) { + throw SOPGPException.BadData("Data is not encrypted.") + } + + val verificationList = + metadata.verifiedInlineSignatures.map { VerificationHelper.mapVerification(it) } + + var sessionKey: SessionKey? = null + if (metadata.sessionKey != null) { + sessionKey = + SessionKey( + metadata.sessionKey!!.algorithm.algorithmId.toByte(), + metadata.sessionKey!!.key) + } + return DecryptionResult(sessionKey, verificationList) + } + } + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotAfter(timestamp) + } + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + consumerOptions.verifyNotBefore(timestamp) + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + KeyReader.readPublicKeys(cert, true)?.let { consumerOptions.addVerificationCerts(it) } + } + + override fun withKey(key: InputStream): Decrypt = apply { + KeyReader.readSecretKeys(key, true).forEach { + protector.addSecretKey(it) + consumerOptions.addDecryptionKey(it, protector) + } + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Decrypt = apply { + consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)) + password.trimEnd().let { + if (it != password) { + consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(it)) + } + } + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + consumerOptions.setSessionKey(mapSessionKey(sessionKey)) + } + + private fun mapSessionKey(sessionKey: SessionKey): org.pgpainless.util.SessionKey = + org.pgpainless.util.SessionKey( + SymmetricKeyAlgorithm.requireFromId(sessionKey.algorithm.toInt()), sessionKey.key) +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt new file mode 100644 index 00000000..c3857ef5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.operation.DetachedSign +import sop.util.UTF8Util + +/** Implementation of the `sign` operation using PGPainless. */ +class DetachedSignImpl : DetachedSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = SignAs.binary + + override fun data(data: InputStream): ReadyWithResult { + signingKeys.forEach { + try { + signingOptions.addDetachedSignature(protector, it, modeToSigType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${it.openPgpFingerprint} cannot sign. Missing secret key.", e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${it.openPgpFingerprint} cannot be unlocked.", e) + } + } + + // When creating a detached signature, the output of the signing stream is actually + // the unmodified plaintext data, so we can discard it. + // The detached signature will later be retrieved from the metadata object instead. + val sink = NullOutputStream() + + try { + val signingStream = + PGPainless.encryptAndOrSign() + .onOutputStream(sink) + .withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor)) + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + check(!signingStream.isClosed) { "The operation is a one-shot object." } + + Streams.pipeAll(data, signingStream) + signingStream.close() + val result = signingStream.result + + // forget passphrases + protector.clear() + + val signatures = result.detachedSignatures.map { it.value }.flatten() + val out = + if (armor) ArmoredOutputStreamFactory.get(outputStream) else outputStream + + signatures.forEach { it.encode(out) } + out.close() + outputStream.close() + + return SigningResult.builder() + .setMicAlg(micAlgFromSignatures(signatures)) + .build() + } + } + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun key(key: InputStream): DetachedSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: SignAs): DetachedSign = apply { this.mode = mode } + + override fun noArmor(): DetachedSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: SignAs): DocumentSignatureType { + return when (mode) { + SignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + SignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } + + private fun micAlgFromSignatures(signatures: List): MicAlg = + signatures + .mapNotNull { HashAlgorithm.fromId(it.hashAlgorithm) } + .toSet() + .singleOrNull() + ?.let { MicAlg.fromHashAlgorithmId(it.algorithmId) } + ?: MicAlg.empty() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt new file mode 100644 index 00000000..08472144 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedVerifyImpl.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures + +/** Implementation of the `verify` operation using PGPainless. */ +class DetachedVerifyImpl : DetachedVerify { + + private val options = ConsumerOptions.get().forceNonOpenPgpData() + + override fun cert(cert: InputStream): DetachedVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.drain(verificationStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + result.verifiedDetachedSignatures.map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + options.verifyNotBefore(timestamp) + } + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + try { + options.addVerificationOfDetachedSignatures(signatures) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt new file mode 100644 index 00000000..83d60aa5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.util.Passphrase +import sop.EncryptionResult +import sop.Profile +import sop.ReadyWithResult +import sop.enums.EncryptAs +import sop.exception.SOPGPException +import sop.operation.Encrypt +import sop.util.UTF8Util + +/** Implementation of the `encrypt` operation using PGPainless. */ +class EncryptImpl : Encrypt { + + companion object { + @JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880") + + @JvmField val SUPPORTED_PROFILES = listOf(RFC4880_PROFILE) + } + + private val encryptionOptions = EncryptionOptions.get() + private var signingOptions: SigningOptions? = null + private val signingKeys = mutableListOf() + private val protector = MatchMakingSecretKeyRingProtector() + + private var profile = RFC4880_PROFILE.name + private var mode = EncryptAs.binary + private var armor = true + + override fun mode(mode: EncryptAs): Encrypt = apply { this.mode = mode } + + override fun noArmor(): Encrypt = apply { this.armor = false } + + override fun plaintext(plaintext: InputStream): ReadyWithResult { + if (!encryptionOptions.hasEncryptionMethod()) { + throw SOPGPException.MissingArg("Missing encryption method.") + } + + val options = + if (signingOptions != null) { + ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!) + } else { + ProducerOptions.encrypt(encryptionOptions) + } + .setAsciiArmor(armor) + .setEncoding(modeToStreamEncoding(mode)) + + signingKeys.forEach { + try { + signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${it.openPgpFingerprint} cannot sign", e) + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + + try { + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): EncryptionResult { + val encryptionStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(options) + Streams.pipeAll(plaintext, encryptionStream) + encryptionStream.close() + // TODO: Extract and emit session key once BC supports that + return EncryptionResult(null) + } + } + } catch (e: PGPException) { + throw IOException(e) + } + } + + override fun profile(profileName: String): Encrypt = apply { + profile = + SUPPORTED_PROFILES.find { it.name == profileName }?.name + ?: throw SOPGPException.UnsupportedProfile("encrypt", profileName) + } + + override fun signWith(key: InputStream): Encrypt = apply { + if (signingOptions == null) { + signingOptions = SigningOptions.get() + } + + val signingKey = + KeyReader.readSecretKeys(key, true).singleOrNull() + ?: throw SOPGPException.BadData( + AssertionError( + "Exactly one secret key at a time expected. Got zero or multiple instead.")) + + val info = PGPainless.inspectKeyRing(signingKey) + if (info.signingSubkeys.isEmpty()) { + throw SOPGPException.KeyCannotSign("Key ${info.fingerprint} cannot sign.") + } + + protector.addSecretKey(signingKey) + signingKeys.add(signingKey) + } + + override fun withCert(cert: InputStream): Encrypt = apply { + try { + encryptionOptions.addRecipients(KeyReader.readPublicKeys(cert, true)) + } catch (e: UnacceptableEncryptionKeyException) { + throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e) + } catch (e: IOException) { + throw SOPGPException.BadData(e) + } + } + + override fun withKeyPassword(password: ByteArray): Encrypt = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + override fun withPassword(password: String): Encrypt = apply { + encryptionOptions.addPassphrase(Passphrase.fromPassword(password)) + } + + private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { + return when (mode) { + EncryptAs.binary -> StreamEncoding.BINARY + EncryptAs.text -> StreamEncoding.UTF8 + } + } + + private fun modeToSignatureType(mode: EncryptAs): DocumentSignatureType { + return when (mode) { + EncryptAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + EncryptAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt new file mode 100644 index 00000000..7fe66ee5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ExtractCertImpl.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import org.pgpainless.PGPainless +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.Ready +import sop.operation.ExtractCert + +/** Implementation of the `extract-cert` operation using PGPainless. */ +class ExtractCertImpl : ExtractCert { + + private var armor = true + + override fun key(keyInputStream: InputStream): Ready { + val certs = + KeyReader.readSecretKeys(keyInputStream, true).map { PGPainless.extractCertificate(it) } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + if (certs.size == 1) { + val cert = certs[0] + // This way we have a nice armor header with fingerprint and user-ids + val armorOut = ArmorUtils.toAsciiArmoredStream(cert, outputStream) + cert.encode(armorOut) + armorOut.close() + } else { + // for multiple certs, add no info headers to the ASCII armor + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + certs.forEach { it.encode(armorOut) } + armorOut.close() + } + } else { + certs.forEach { it.encode(outputStream) } + } + } + } + } + + override fun noArmor(): ExtractCert = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt new file mode 100644 index 00000000..7438f0ae --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/GenerateKeyImpl.kt @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.OutputStream +import java.lang.RuntimeException +import java.security.InvalidAlgorithmParameterException +import java.security.NoSuchAlgorithmException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.KeyFlag +import org.pgpainless.key.generation.KeyRingBuilder +import org.pgpainless.key.generation.KeySpec +import org.pgpainless.key.generation.type.KeyType +import org.pgpainless.key.generation.type.eddsa.EdDSACurve +import org.pgpainless.key.generation.type.rsa.RsaLength +import org.pgpainless.key.generation.type.xdh.XDHSpec +import org.pgpainless.util.ArmorUtils +import org.pgpainless.util.Passphrase +import sop.Profile +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.GenerateKey + +/** Implementation of the `generate-key` operation using PGPainless. */ +class GenerateKeyImpl : GenerateKey { + + companion object { + @JvmField + val CURVE25519_PROFILE = + Profile( + "draft-koch-eddsa-for-openpgp-00", "Generate EdDSA / ECDH keys using Curve25519") + @JvmField val RSA4096_PROFILE = Profile("rfc4880", "Generate 4096-bit RSA keys") + + @JvmField val SUPPORTED_PROFILES = listOf(CURVE25519_PROFILE, RSA4096_PROFILE) + } + + private val userIds = mutableSetOf() + private var armor = true + private var signingOnly = false + private var passphrase = Passphrase.emptyPassphrase() + private var profile = CURVE25519_PROFILE.name + + override fun generate(): Ready { + try { + val key = generateKeyWithProfile(profile, userIds, passphrase, signingOnly) + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + if (armor) { + val armorOut = ArmorUtils.toAsciiArmoredStream(key, outputStream) + key.encode(armorOut) + armorOut.close() + } else { + key.encode(outputStream) + } + } + } + } catch (e: InvalidAlgorithmParameterException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: NoSuchAlgorithmException) { + throw SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e) + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): GenerateKey = apply { armor = false } + + override fun profile(profile: String): GenerateKey = apply { + this.profile = + SUPPORTED_PROFILES.find { it.name == profile }?.name + ?: throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + override fun signingOnly(): GenerateKey = apply { signingOnly = true } + + override fun userId(userId: String): GenerateKey = apply { userIds.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + this.passphrase = Passphrase.fromPassword(password) + } + + private fun generateKeyWithProfile( + profile: String, + userIds: Set, + passphrase: Passphrase, + signingOnly: Boolean + ): PGPSecretKeyRing { + val keyBuilder: KeyRingBuilder = + when (profile) { + CURVE25519_PROFILE.name -> + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder( + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder( + KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.XDH(XDHSpec._X25519), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + RSA4096_PROFILE.name -> { + PGPainless.buildKeyRing() + .setPrimaryKey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.CERTIFY_OTHER)) + .addSubkey( + KeySpec.getBuilder(KeyType.RSA(RsaLength._4096), KeyFlag.SIGN_DATA)) + .apply { + if (!signingOnly) { + addSubkey( + KeySpec.getBuilder( + KeyType.RSA(RsaLength._4096), + KeyFlag.ENCRYPT_COMMS, + KeyFlag.ENCRYPT_STORAGE)) + } + } + } + else -> throw SOPGPException.UnsupportedProfile("generate-key", profile) + } + + userIds.forEach { keyBuilder.addUserId(it) } + if (!passphrase.isEmpty) { + keyBuilder.setPassphrase(passphrase) + } + return keyBuilder.build() + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt new file mode 100644 index 00000000..82414a96 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import org.bouncycastle.bcpg.ArmoredInputStream +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPLiteralData +import org.bouncycastle.openpgp.PGPOnePassSignatureList +import org.bouncycastle.openpgp.PGPSignatureList +import org.bouncycastle.util.io.Streams +import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil +import org.pgpainless.exception.WrongConsumingMethodException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.ReadyWithResult +import sop.Signatures +import sop.exception.SOPGPException +import sop.operation.InlineDetach + +/** Implementation of the `inline-detach` operation using PGPainless. */ +class InlineDetachImpl : InlineDetach { + + private var armor = true + + override fun message(messageInputStream: InputStream): ReadyWithResult { + return object : ReadyWithResult() { + + private val sigOut = ByteArrayOutputStream() + + override fun writeTo(messageOutputStream: OutputStream): Signatures { + var pgpIn = OpenPgpInputStream(messageInputStream) + if (pgpIn.isNonOpenPgp) { + throw SOPGPException.BadData("Data appears to be non-OpenPGP.") + } + var signatures: PGPSignatureList? = null + + // Handle ASCII armor + if (pgpIn.isAsciiArmored) { + val armorIn = ArmoredInputStream(pgpIn) + + // Handle cleartext signature framework + if (armorIn.isClearText) { + try { + signatures = + ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( + armorIn, messageOutputStream) + if (signatures.isEmpty) { + throw SOPGPException.BadData( + "Data did not contain OpenPGP signatures.") + } + } catch (e: WrongConsumingMethodException) { + throw SOPGPException.BadData(e) + } + } + + // else just dearmor + pgpIn = OpenPgpInputStream(armorIn) + } + + // If data was not using cleartext signature framework + if (signatures == null) { + if (!pgpIn.isBinaryOpenPgp) { + throw SOPGPException.BadData( + "Data was containing ASCII armored non-OpenPGP data.") + } + + // handle binary OpenPGP data + var objectFactory = + ImplementationFactory.getInstance().getPGPObjectFactory(pgpIn) + var next: Any? + + while (objectFactory.nextObject().also { next = it } != null) { + + if (next is PGPOnePassSignatureList) { + // Skip over OPSs + continue + } + + if (next is PGPLiteralData) { + // Write out contents of Literal Data packet + val literalIn = (next as PGPLiteralData).dataStream + Streams.pipeAll(literalIn, messageOutputStream) + literalIn.close() + continue + } + + if (next is PGPCompressedData) { + // Decompress compressed data + try { + objectFactory = + ImplementationFactory.getInstance() + .getPGPObjectFactory((next as PGPCompressedData).dataStream) + } catch (e: PGPException) { + throw SOPGPException.BadData( + "Cannot decompress PGPCompressedData", e) + } + } + + if (next is PGPSignatureList) { + signatures = next as PGPSignatureList + } + } + } + + if (signatures == null) { + throw SOPGPException.BadData("Data did not contain OpenPGP signatures.") + } + + if (armor) { + ArmoredOutputStreamFactory.get(sigOut).use { armoredOut -> + signatures.forEach { it.encode(armoredOut) } + } + } else { + signatures.forEach { it.encode(sigOut) } + } + + return object : Signatures() { + override fun writeTo(outputStream: OutputStream) { + sigOut.writeTo(outputStream) + } + } + } + } + } + + override fun noArmor(): InlineDetach = apply { armor = false } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt new file mode 100644 index 00000000..6fdf59a1 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.encryption_signing.SigningOptions +import org.pgpainless.exception.KeyException.MissingSecretKeyException +import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.enums.InlineSignAs +import sop.exception.SOPGPException +import sop.operation.InlineSign +import sop.util.UTF8Util + +/** Implementation of the `inline-sign` operation using PGPainless. */ +class InlineSignImpl : InlineSign { + + private val signingOptions = SigningOptions.get() + private val protector = MatchMakingSecretKeyRingProtector() + private val signingKeys = mutableListOf() + + private var armor = true + private var mode = InlineSignAs.binary + + override fun data(data: InputStream): Ready { + signingKeys.forEach { key -> + try { + if (mode == InlineSignAs.clearsigned) { + signingOptions.addDetachedSignature(protector, key, modeToSigType(mode)) + } else { + signingOptions.addInlineSignature(protector, key, modeToSigType(mode)) + } + } catch (e: UnacceptableSigningKeyException) { + throw SOPGPException.KeyCannotSign("Key ${key.openPgpFingerprint} cannot sign.", e) + } catch (e: MissingSecretKeyException) { + throw SOPGPException.KeyCannotSign( + "Key ${key.openPgpFingerprint} does not have the secret signing key component available.", + e) + } catch (e: PGPException) { + throw SOPGPException.KeyIsProtected( + "Key ${key.openPgpFingerprint} cannot be unlocked.", e) + } + } + + val producerOptions = + ProducerOptions.sign(signingOptions).apply { + if (mode == InlineSignAs.clearsigned) { + setCleartextSigned() + setAsciiArmor(true) // CSF is always armored + } else { + setAsciiArmor(armor) + } + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + try { + val signingStream = + PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(producerOptions) + + Streams.pipeAll(data, signingStream) + signingStream.close() + + // forget passphrases + protector.clear() + } catch (e: PGPException) { + throw RuntimeException(e) + } + } + } + } + + override fun key(key: InputStream): InlineSign = apply { + KeyReader.readSecretKeys(key, true).forEach { + val info = PGPainless.inspectKeyRing(it) + if (!info.isUsableForSigning) { + throw SOPGPException.KeyCannotSign( + "Key ${info.fingerprint} does not have valid, signing capable subkeys.") + } + protector.addSecretKey(it) + signingKeys.add(it) + } + } + + override fun mode(mode: InlineSignAs): InlineSign = apply { this.mode = mode } + + override fun noArmor(): InlineSign = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + protector.addPassphrase(Passphrase.fromPassword(String(password, UTF8Util.UTF8))) + } + + private fun modeToSigType(mode: InlineSignAs): DocumentSignatureType { + return when (mode) { + InlineSignAs.binary -> DocumentSignatureType.BINARY_DOCUMENT + InlineSignAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + InlineSignAs.clearsigned -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt new file mode 100644 index 00000000..0b4e7d2f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineVerifyImpl.kt @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MissingDecryptionMethodException +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException +import sop.operation.InlineVerify + +/** Implementation of the `inline-verify` operation using PGPainless. */ +class InlineVerifyImpl : InlineVerify { + + private val options = ConsumerOptions.get() + + override fun cert(cert: InputStream): InlineVerify = apply { + options.addVerificationCerts(KeyReader.readPublicKeys(cert, true)) + } + + override fun data(data: InputStream): ReadyWithResult> { + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + try { + val verificationStream = + PGPainless.decryptAndOrVerify().onInputStream(data).withOptions(options) + + Streams.pipeAll(verificationStream, outputStream) + verificationStream.close() + + val result = verificationStream.metadata + val verifications = + if (result.isUsingCleartextSignatureFramework) { + result.verifiedDetachedSignatures + } else { + result.verifiedInlineSignatures + } + .map { VerificationHelper.mapVerification(it) } + + if (options.getCertificateSource().getExplicitCertificates().isNotEmpty() && + verifications.isEmpty()) { + throw SOPGPException.NoSignature() + } + + return verifications + } catch (e: MissingDecryptionMethodException) { + throw SOPGPException.BadData("Cannot verify encrypted message.", e) + } catch (e: MalformedOpenPgpMessageException) { + throw SOPGPException.BadData(e) + } catch (e: PGPException) { + throw SOPGPException.BadData(e) + } + } + } + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + options.verifyNotAfter(timestamp) + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + options.verifyNotBefore(timestamp) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt new file mode 100644 index 00000000..0931a3a5 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPRuntimeOperationException +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.PGPainless +import sop.exception.SOPGPException + +/** Reader for OpenPGP keys and certificates with error matching according to the SOP spec. */ +class KeyReader { + + companion object { + @JvmStatic + fun readSecretKeys( + keyInputStream: InputStream, + requireContent: Boolean + ): PGPSecretKeyRingCollection { + val keys = + try { + PGPainless.readKeyRing().secretKeyRingCollection(keyInputStream) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } + if (requireContent && keys.none()) { + throw SOPGPException.BadData(PGPException("No key data found.")) + } + + return keys + } + + @JvmStatic + fun readPublicKeys( + certIn: InputStream, + requireContent: Boolean + ): PGPPublicKeyRingCollection { + val certs = + try { + PGPainless.readKeyRing().keyRingCollection(certIn, false) + } catch (e: IOException) { + if (e.message == null) { + throw e + } + if (e.message!!.startsWith("unknown object in stream:") || + e.message!!.startsWith("invalid header encountered")) { + throw SOPGPException.BadData(e) + } + throw e + } catch (e: PGPRuntimeOperationException) { + throw SOPGPException.BadData(e) + } + + if (certs.pgpSecretKeyRingCollection.any()) { + throw SOPGPException.BadData( + "Secret key components encountered, while certificates were expected.") + } + + if (requireContent && certs.pgpPublicKeyRingCollection.none()) { + throw SOPGPException.BadData(PGPException("No cert data found.")) + } + return certs.pgpPublicKeyRingCollection + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt new file mode 100644 index 00000000..39a5151d --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ListProfilesImpl.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.Profile +import sop.exception.SOPGPException +import sop.operation.ListProfiles + +/** Implementation of the `list-profiles` operation using PGPainless. */ +class ListProfilesImpl : ListProfiles { + + override fun subcommand(command: String): List = + when (command) { + "generate-key" -> GenerateKeyImpl.SUPPORTED_PROFILES + "encrypt" -> EncryptImpl.SUPPORTED_PROFILES + else -> throw SOPGPException.UnsupportedProfile(command) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt new file mode 100644 index 00000000..13347721 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/MatchMakingSecretKeyRingProtector.kt @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor +import org.pgpainless.bouncycastle.extensions.isDecrypted +import org.pgpainless.bouncycastle.extensions.unlock +import org.pgpainless.key.protection.CachingSecretKeyRingProtector +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase + +/** + * Implementation of the [SecretKeyRingProtector] which can be handed passphrases and keys + * separately, and which then matches up passphrases and keys when needed. + */ +class MatchMakingSecretKeyRingProtector : SecretKeyRingProtector { + + private val passphrases = mutableSetOf() + private val keys = mutableSetOf() + private val protector = CachingSecretKeyRingProtector() + + fun addPassphrase(passphrase: Passphrase) = apply { + if (passphrase.isEmpty) { + return@apply + } + + if (!passphrases.add(passphrase)) { + return@apply + } + + keys.forEach { key -> + for (subkey in key) { + if (protector.hasPassphrase(subkey.keyID)) { + continue + } + + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + + fun addSecretKey(key: PGPSecretKeyRing) = apply { + if (!keys.add(key)) { + return@apply + } + + key.forEach { subkey -> + if (subkey.isDecrypted()) { + protector.addPassphrase(subkey.keyID, Passphrase.emptyPassphrase()) + } else { + passphrases.forEach { passphrase -> + if (testPassphrase(passphrase, subkey)) { + protector.addPassphrase(subkey.keyID, passphrase) + } + } + } + } + } + + private fun testPassphrase(passphrase: Passphrase, key: PGPSecretKey): Boolean = + try { + key.unlock(passphrase) + true + } catch (e: PGPException) { + // Wrong passphrase + false + } + + override fun hasPassphraseFor(keyId: Long): Boolean = protector.hasPassphrase(keyId) + + override fun getDecryptor(keyId: Long): PBESecretKeyDecryptor? = protector.getDecryptor(keyId) + + override fun getEncryptor(keyId: Long): PBESecretKeyEncryptor? = protector.getEncryptor(keyId) + + /** Clear all known passphrases from the protector. */ + fun clear() { + passphrases.forEach { it.clear() } + keys.forEach { protector.forgetPassphrase(it) } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt new file mode 100644 index 00000000..0f644881 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.OutputStream + +/** [OutputStream] that simply discards bytes written to it. */ +class NullOutputStream : OutputStream() { + + override fun write(p0: Int) { + // nop + } + + override fun write(b: ByteArray) { + // nop + } + + override fun write(b: ByteArray, off: Int, len: Int) { + // nop + } + + override fun close() { + // nop + } + + override fun flush() { + // nop + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt new file mode 100644 index 00000000..ecc87e62 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/RevokeKeyImpl.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.RuntimeException +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.pgpainless.PGPainless +import org.pgpainless.bouncycastle.extensions.openPgpFingerprint +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.util.KeyRingUtils +import org.pgpainless.key.util.RevocationAttributes +import org.pgpainless.util.ArmoredOutputStreamFactory +import org.pgpainless.util.Passphrase +import sop.Ready +import sop.exception.SOPGPException +import sop.operation.RevokeKey +import sop.util.UTF8Util + +class RevokeKeyImpl : RevokeKey { + + private val protector = MatchMakingSecretKeyRingProtector() + private var armor = true + + override fun keys(keys: InputStream): Ready { + val secretKeyRings = + try { + KeyReader.readSecretKeys(keys, true) + } catch (e: IOException) { + throw SOPGPException.BadData("Cannot decode secret keys.", e) + } + + secretKeyRings.forEach { protector.addSecretKey(it) } + + val revocationCertificates = mutableListOf() + secretKeyRings.forEach { secretKeys -> + val editor = PGPainless.modifyKeyRing(secretKeys) + try { + val attributes = + RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.NO_REASON) + .withoutDescription() + if (secretKeys.publicKey.version == 6) { + revocationCertificates.add( + editor.createMinimalRevocationCertificate(protector, attributes)) + } else { + val certificate = PGPainless.extractCertificate(secretKeys) + val revocation = editor.createRevocation(protector, attributes) + revocationCertificates.add( + KeyRingUtils.injectCertification(certificate, revocation)) + } + } catch (e: WrongPassphraseException) { + throw SOPGPException.KeyIsProtected( + "Missing or wrong passphrase for key ${secretKeys.openPgpFingerprint}", e) + } catch (e: PGPException) { + throw RuntimeException( + "Cannot generate revocation certificate for key ${secretKeys.openPgpFingerprint}", + e) + } + } + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val collection = PGPPublicKeyRingCollection(revocationCertificates) + if (armor) { + val armorOut = ArmoredOutputStreamFactory.get(outputStream) + collection.encode(armorOut) + armorOut.close() + } else { + collection.encode(outputStream) + } + } + } + } + + override fun noArmor(): RevokeKey = apply { armor = false } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + val string = + try { + UTF8Util.decodeUTF8(password) + } catch (e: CharacterCodingException) { + // TODO: Add cause + throw SOPGPException.PasswordNotHumanReadable( + "Cannot UTF8-decode password: ${e.stackTraceToString()}") + } + protector.addPassphrase(Passphrase.fromPassword(string)) + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt new file mode 100644 index 00000000..16f54a22 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPImpl.kt @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import sop.SOP +import sop.SOPV +import sop.operation.Armor +import sop.operation.ChangeKeyPassword +import sop.operation.Dearmor +import sop.operation.Decrypt +import sop.operation.DetachedSign +import sop.operation.DetachedVerify +import sop.operation.Encrypt +import sop.operation.ExtractCert +import sop.operation.GenerateKey +import sop.operation.InlineDetach +import sop.operation.InlineSign +import sop.operation.InlineVerify +import sop.operation.ListProfiles +import sop.operation.RevokeKey +import sop.operation.Version + +class SOPImpl(private val sopv: SOPV = SOPVImpl()) : SOP { + + override fun armor(): Armor = ArmorImpl() + + override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordImpl() + + override fun dearmor(): Dearmor = DearmorImpl() + + override fun decrypt(): Decrypt = DecryptImpl() + + override fun detachedSign(): DetachedSign = DetachedSignImpl() + + override fun detachedVerify(): DetachedVerify = sopv.detachedVerify() + + override fun encrypt(): Encrypt = EncryptImpl() + + override fun extractCert(): ExtractCert = ExtractCertImpl() + + override fun generateKey(): GenerateKey = GenerateKeyImpl() + + override fun inlineDetach(): InlineDetach = InlineDetachImpl() + + override fun inlineSign(): InlineSign = InlineSignImpl() + + override fun inlineVerify(): InlineVerify = sopv.inlineVerify() + + override fun listProfiles(): ListProfiles = ListProfilesImpl() + + override fun revokeKey(): RevokeKey = RevokeKeyImpl() + + override fun version(): Version = sopv.version() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt new file mode 100644 index 00000000..43b4c64f --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/SOPVImpl.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.pgpainless.util.ArmoredOutputStreamFactory +import sop.SOPV +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +class SOPVImpl : SOPV { + + init { + ArmoredOutputStreamFactory.setVersionInfo(null) + } + + override fun detachedVerify(): DetachedVerify = DetachedVerifyImpl() + + override fun inlineVerify(): InlineVerify = InlineVerifyImpl() + + override fun version(): Version = VersionImpl() +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt new file mode 100644 index 00000000..9198e3b7 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VerificationHelper.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import org.bouncycastle.openpgp.PGPSignature +import org.pgpainless.decryption_verification.SignatureVerification +import sop.Verification +import sop.enums.SignatureMode + +/** Helper class for shared methods related to [Verification] objects. */ +class VerificationHelper { + + companion object { + + /** + * Map a [SignatureVerification] object to a [Verification]. + * + * @param sigVerification signature verification + * @return verification + */ + @JvmStatic + fun mapVerification(sigVerification: SignatureVerification): Verification = + Verification( + sigVerification.signature.creationTime, + sigVerification.signingKey.subkeyFingerprint.toString(), + sigVerification.signingKey.primaryKeyFingerprint.toString(), + getMode(sigVerification.signature), + null) + + /** + * Map an OpenPGP signature type to a [SignatureMode] enum. Note: This method only maps + * [PGPSignature.BINARY_DOCUMENT] and [PGPSignature.CANONICAL_TEXT_DOCUMENT]. Other values + * are mapped to `null`. + * + * @param signature signature + * @return signature mode enum or null + */ + @JvmStatic + fun getMode(signature: PGPSignature): SignatureMode? = + when (signature.signatureType) { + PGPSignature.BINARY_DOCUMENT -> SignatureMode.binary + PGPSignature.CANONICAL_TEXT_DOCUMENT -> SignatureMode.text + else -> null + } + } +} diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt new file mode 100644 index 00000000..7ebdade9 --- /dev/null +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop + +import java.io.IOException +import java.io.InputStream +import java.util.* +import org.bouncycastle.jce.provider.BouncyCastleProvider +import sop.operation.Version + +/** Implementation of the `version` operation using PGPainless. */ +class VersionImpl : Version { + + companion object { + const val SOP_VERSION = 10 + const val SOPV_VERSION = "1.0" + } + + override fun getBackendVersion(): String = "PGPainless ${getVersion()}" + + override fun getExtendedVersion(): String { + val bcVersion = + String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version) + val specVersion = String.format("%02d", SOP_VERSION) + return """${getName()} ${getVersion()} +https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop + +Implementation of the Stateless OpenPGP Protocol Version $specVersion +https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersion + +Based on pgpainless-core ${getVersion()} +https://pgpainless.org + +Using $bcVersion +https://www.bouncycastle.org/java.html""" + } + + override fun getName(): String = "PGPainless-SOP" + + override fun getSopSpecImplementationRemarks(): String? = null + + override fun getSopSpecRevisionNumber(): Int = SOP_VERSION + + override fun getSopVVersion(): String = SOPV_VERSION + + override fun getVersion(): String { + // See https://stackoverflow.com/a/50119235 + return try { + val resourceIn: InputStream = + javaClass.getResourceAsStream("/version.properties") + ?: throw IOException("File version.properties not found.") + + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("version") + } catch (e: IOException) { + "DEVELOPMENT" + } + } + + override fun isSopSpecImplementationIncomplete(): Boolean = false +} From fe80b1185ec83180e8e36a0a006a2038abb5dbd2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 24 Mar 2024 16:43:27 +0100 Subject: [PATCH 252/351] Update man pages --- pgpainless-cli/packaging/man/pgpainless-cli-armor.1 | 7 +------ pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 | 11 +++-------- pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 | 8 ++++---- pgpainless-cli/packaging/man/pgpainless-cli-version.1 | 4 ++-- pgpainless-cli/rewriteManPages.sh | 2 ++ 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 index 98e98857..0d948dfd 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-armor.1 @@ -30,16 +30,11 @@ pgpainless\-cli\-armor \- Add ASCII Armor to standard input .SH "SYNOPSIS" .sp -\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] [\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP] +\fBpgpainless\-cli armor\fP [\fB\-\-stacktrace\fP] .SH "DESCRIPTION" .SH "OPTIONS" .sp -\fB\-\-label\fP=\fI{auto|sig|key|cert|message}\fP -.RS 4 -Label to be used in the header and tail of the armoring -.RE -.sp \fB\-\-stacktrace\fP .RS 4 Print stacktrace diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 index 9a3c80a7..eb843e18 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-decrypt.1 @@ -31,8 +31,8 @@ pgpainless\-cli\-decrypt \- Decrypt a message .SH "SYNOPSIS" .sp \fBpgpainless\-cli decrypt\fP [\fB\-\-stacktrace\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] -[\fB\-\-verify\-not\-after\fP=\fIDATE\fP] [\fB\-\-verify\-not\-before\fP=\fIDATE\fP] -[\fB\-\-verify\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... +[\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP] [\fB\-\-verify\-not\-after\fP=\fIDATE\fP] +[\fB\-\-verify\-not\-before\fP=\fIDATE\fP] [\fB\-\-verify\-with\fP=\fICERT\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-session\-key\fP=\fISESSIONKEY\fP]... [\fIKEY\fP...] .SH "DESCRIPTION" @@ -49,7 +49,7 @@ Can be used to learn the session key on successful decryption Print stacktrace .RE .sp -\fB\-\-verify\-not\-after\fP=\fIDATE\fP +\fB\-\-verifications\-out\fP=\fIVERIFICATIONS\fP, \fB\-\-verify\-not\-after\fP=\fIDATE\fP .RS 4 ISO\-8601 formatted UTC date (e.g. \(aq2020\-11\-23T16:35Z) .sp @@ -69,11 +69,6 @@ Reject signatures with a creation date not in range. Defaults to beginning of time (\(aq\-\(aq). .RE .sp -\fB\-\-verify\-out, \-\-verifications\-out\fP=\fIVERIFICATIONS\fP -.RS 4 -Emits signature verification status to the designated output -.RE -.sp \fB\-\-verify\-with\fP=\fICERT\fP .RS 4 Certificates for signature verification diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 index 29f1d1f1..3cf7aefc 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-encrypt.1 @@ -31,9 +31,9 @@ pgpainless\-cli\-encrypt \- Encrypt a message from standard input .SH "SYNOPSIS" .sp \fBpgpainless\-cli encrypt\fP [\fB\-\-[no\-]armor\fP] [\fB\-\-stacktrace\fP] [\fB\-\-as\fP=\fI{binary|text}\fP] -[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-sign\-with\fP=\fIKEY\fP]... -[\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... [\fB\-\-with\-password\fP=\fIPASSWORD\fP]... -[\fICERTS\fP...] +[\fB\-\-profile\fP=\fIPROFILE\fP] [\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP] +[\fB\-\-sign\-with\fP=\fIKEY\fP]... [\fB\-\-with\-key\-password\fP=\fIPASSWORD\fP]... +[\fB\-\-with\-password\fP=\fIPASSWORD\fP]... [\fICERTS\fP...] .SH "DESCRIPTION" .SH "OPTIONS" @@ -53,7 +53,7 @@ ASCII armor the output Profile identifier to switch between profiles .RE .sp -\fB\-\-sign\-with\fP=\fIKEY\fP +\fB\-\-session\-key\-out\fP=\fISESSIONKEY\fP, \fB\-\-sign\-with\fP=\fIKEY\fP .RS 4 Sign the output with a private key .RE diff --git a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 index 87db756a..c552bbe0 100644 --- a/pgpainless-cli/packaging/man/pgpainless-cli-version.1 +++ b/pgpainless-cli/packaging/man/pgpainless-cli-version.1 @@ -30,7 +30,7 @@ pgpainless\-cli\-version \- Display version information about the tool .SH "SYNOPSIS" .sp -\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP] +\fBpgpainless\-cli version\fP [\fB\-\-stacktrace\fP] [\fB\-\-extended\fP | \fB\-\-backend\fP | \fB\-\-pgpainless\-cli\-spec\fP | \fB\-\-sopv\fP] .SH "DESCRIPTION" .SH "OPTIONS" @@ -50,7 +50,7 @@ Print an extended version string Print the latest revision of the SOP specification targeted by the implementation .RE .sp -\fB\-\-stacktrace\fP +\fB\-\-sopv\fP, \fB\-\-stacktrace\fP .RS 4 Print stacktrace .RE \ No newline at end of file diff --git a/pgpainless-cli/rewriteManPages.sh b/pgpainless-cli/rewriteManPages.sh index 37b4fdf3..730af68d 100755 --- a/pgpainless-cli/rewriteManPages.sh +++ b/pgpainless-cli/rewriteManPages.sh @@ -13,12 +13,14 @@ do SRC="${page##*/}" DEST="${SRC/sop/pgpainless-cli}" sed \ + -e 's/sopv/PLACEHOLDERV/g' \ -e 's#.\\" Title: sop#.\\" Title: pgpainless-cli#g' \ -e 's/Manual: Sop Manual/Manual: PGPainless-CLI Manual/g' \ -e 's/.TH "SOP/.TH "PGPAINLESS\\-CLI/g' \ -e 's/"Sop Manual"/"PGPainless\\-CLI Manual"/g' \ -e 's/\\fBsop/\\fBpgpainless\\-cli/g' \ -e 's/sop/pgpainless\\-cli/g' \ + -e 's/PLACEHOLDERV/sopv/g' \ $page > $DEST_DIR/$DEST done From b96f22d0a9097f1af89f8720210ea72392f8672c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 29 Mar 2024 20:37:24 +0100 Subject: [PATCH 253/351] Add EncryptionBuilder.discardOutput() Also move NullOutputStream from pgpainless-sop to pgpainless-core --- .../encryption_signing/EncryptionBuilder.kt | 5 +++++ .../encryption_signing/EncryptionBuilderInterface.kt | 12 +++++++++++- .../kotlin/org/pgpainless/util}/NullOutputStream.kt | 2 +- .../kotlin/org/pgpainless/sop/DetachedSignImpl.kt | 7 +------ 4 files changed, 18 insertions(+), 8 deletions(-) rename {pgpainless-sop/src/main/kotlin/org/pgpainless/sop => pgpainless-core/src/main/kotlin/org/pgpainless/util}/NullOutputStream.kt (95%) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt index 13785df4..6b4713d6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilder.kt @@ -9,6 +9,7 @@ import org.pgpainless.PGPainless.Companion.getPolicy import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity +import org.pgpainless.util.NullOutputStream import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -19,6 +20,10 @@ class EncryptionBuilder : EncryptionBuilderInterface { return WithOptionsImpl(outputStream) } + override fun discardOutput(): EncryptionBuilderInterface.WithOptions { + return onOutputStream(NullOutputStream()) + } + class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions { override fun withOptions(options: ProducerOptions): EncryptionStream { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt index 586fbc6e..2db98846 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionBuilderInterface.kt @@ -8,7 +8,7 @@ import java.io.IOException import java.io.OutputStream import org.bouncycastle.openpgp.PGPException -fun interface EncryptionBuilderInterface { +interface EncryptionBuilderInterface { /** * Create a [EncryptionStream] wrapping an [OutputStream]. Data that is piped through the @@ -19,6 +19,16 @@ fun interface EncryptionBuilderInterface { */ fun onOutputStream(outputStream: OutputStream): WithOptions + /** + * Create an [EncryptionStream] that discards the data after processing it. This is useful, e.g. + * for generating detached signatures, where the resulting signature is retrieved from the + * [EncryptionResult] once the operation is finished. In this case, the plaintext data does not + * need to be retained. + * + * @return api handle + */ + fun discardOutput(): WithOptions + fun interface WithOptions { /** diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt similarity index 95% rename from pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt rename to pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt index 0f644881..5a3df702 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/NullOutputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NullOutputStream.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.pgpainless.sop +package org.pgpainless.util import java.io.OutputStream diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt index c3857ef5..a84ed6ef 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -53,15 +53,10 @@ class DetachedSignImpl : DetachedSign { } } - // When creating a detached signature, the output of the signing stream is actually - // the unmodified plaintext data, so we can discard it. - // The detached signature will later be retrieved from the metadata object instead. - val sink = NullOutputStream() - try { val signingStream = PGPainless.encryptAndOrSign() - .onOutputStream(sink) + .discardOutput() .withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor)) return object : ReadyWithResult() { From 1f9b65e3d28f962ccbf963e8023eab35f496e9ee Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 00:37:51 +0100 Subject: [PATCH 254/351] Fix missing readthedocs theme --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 1d5e0d5b..ab1cf848 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ myst-parser>=0.17 sphinxcontrib-mermaid>=0.7.1 +sphinx_rtd_theme>=2.0.0 From 32d62c66105609f1dcf9b55c06198ad71489ad5b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 18:52:49 +0100 Subject: [PATCH 255/351] Update pgpainless-cli usage documentation --- docs/source/pgpainless-cli/usage.md | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/source/pgpainless-cli/usage.md b/docs/source/pgpainless-cli/usage.md index 4bc1d166..15a87846 100644 --- a/docs/source/pgpainless-cli/usage.md +++ b/docs/source/pgpainless-cli/usage.md @@ -82,23 +82,26 @@ Stateless OpenPGP Protocol Usage: pgpainless-cli [--stacktrace] [COMMAND] Options: - --stacktrace Print Stacktrace + --stacktrace Print stacktrace Commands: - help Display usage information for the specified subcommand - armor Add ASCII Armor to standard input - dearmor Remove ASCII Armor from standard input - decrypt Decrypt a message from standard input - inline-detach Split signatures from a clearsigned message - encrypt Encrypt a message from standard input - extract-cert Extract a public key certificate from a secret key from - standard input - generate-key Generate a secret key - sign Create a detached signature on the data from standard input - verify Verify a detached signature over the data from standard input - inline-sign Create an inline-signed message from data on standard input - inline-verify Verify inline-signed data from standard input - version Display version information about the tool + version Display version information about the tool + list-profiles Emit a list of profiles supported by the identified + subcommand + generate-key Generate a secret key + change-key-password Update the password of a key + revoke-key Generate revocation certificates + extract-cert Extract a public key certificate from a secret key + sign Create a detached message signature + verify Verify a detached signature + encrypt Encrypt a message from standard input + decrypt Decrypt a message + inline-detach Split signatures from a clearsigned message + inline-sign Create an inline-signed message + inline-verify Verify an inline-signed message + armor Add ASCII Armor to standard input + dearmor Remove ASCII Armor from standard input + help Display usage information for the specified subcommand Exit Codes: 0 Successful program execution @@ -120,6 +123,9 @@ Exit Codes: 71 Unsupported special prefix (e.g. "@ENV/@FD") of indirect parameter 73 Ambiguous input (a filename matching the designator already exists) 79 Key is not signing capable + 83 Options were supplied that are incompatible with each other + 89 The requested profile is unsupported, or the indicated subcommand does + not accept profiles ``` To get help on a subcommand, e.g. `encrypt`, just call the help subcommand followed by the subcommand you From eeb5986890b8b780bf33e1c6eb9eea1903d72151 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:06:42 +0100 Subject: [PATCH 256/351] Remove notice about armor's label() option --- docs/source/pgpainless-sop/quickstart.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/source/pgpainless-sop/quickstart.md b/docs/source/pgpainless-sop/quickstart.md index 8b8281ac..55958ee4 100644 --- a/docs/source/pgpainless-sop/quickstart.md +++ b/docs/source/pgpainless-sop/quickstart.md @@ -180,14 +180,6 @@ byte[] armoredData = sop.armor() The `data(_)` method can either be called by providing a byte array, or an `InputStream`. -:::{note} -There is a `label(ArmorLabel label)` method, which could theoretically be used to define the label used in the -ASCII armor header. -However, this method is not (yet?) supported by `pgpainless-sop` and will currently throw an `UnsupportedOption` -exception. -Instead, the implementation will figure out the data type and set the respective label on its own. -::: - To remove ASCII armor from armored data, simply use the `dearmor()` API: ```java From 0b7511a2239dc0a144f8eb96e5d2c57d8efbbaa7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:07:12 +0100 Subject: [PATCH 257/351] Remove tests for armor --label --- .../java/org/pgpainless/cli/commands/ArmorCmdTest.java | 10 ---------- .../src/test/java/org/pgpainless/sop/ArmorTest.java | 8 -------- 2 files changed, 18 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java index afd5ded4..ddb3c6dd 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorCmdTest.java @@ -16,7 +16,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.slf4j.LoggerFactory; -import sop.exception.SOPGPException; public class ArmorCmdTest extends CLITest { @@ -89,15 +88,6 @@ public class ArmorCmdTest extends CLITest { assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); } - @Test - public void labelNotYetSupported() throws IOException { - pipeStringToStdin("Hello, World!\n"); - ByteArrayOutputStream out = pipeStdoutToStream(); - int exitCode = executeCommand("armor", "--label", "Message"); - assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, exitCode); - assertEquals(0, out.size()); - } - @Test public void armorAlreadyArmoredDataIsIdempotent() throws IOException { pipeStringToStdin(key); diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java index a97b968d..82688bbf 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/ArmorTest.java @@ -7,22 +7,14 @@ package org.pgpainless.sop; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; import org.pgpainless.util.ArmorUtils; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; import java.io.IOException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; public class ArmorTest { - @Test - public void labelIsNotSupported() { - assertThrows(SOPGPException.UnsupportedOption.class, () -> new SOPImpl().armor().label(ArmorLabel.sig)); - } - @Test public void armor() throws IOException { byte[] data = PGPainless.generateKeyRing().modernKeyRing("Alice").getEncoded(); From 741d72eadcdc8b285e0c6237d286612635267e27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:20:12 +0100 Subject: [PATCH 258/351] Document nature of tests in pgpainless-sop --- .../src/test/java/org/pgpainless/sop/package-info.java | 9 +++++++++ .../test/java/sop/testsuite/pgpainless/package-info.java | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java create mode 100644 pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java new file mode 100644 index 00000000..a1994b9d --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/package-info.java @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Tests for the {@link sop.SOP} API, tailored to the behavior of PGPainless' implementation specifically. + * Generalized tests can be found in {@link sop.testsuite.pgpainless}. + */ +package org.pgpainless.sop; diff --git a/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java new file mode 100644 index 00000000..5da4307d --- /dev/null +++ b/pgpainless-sop/src/test/java/sop/testsuite/pgpainless/package-info.java @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Generalized tests for the {@link sop.SOP} API. + * For tests tailored specifically to PGPainless' behavior, see {@link org.pgpainless.sop}. + */ +package sop.testsuite.pgpainless; From a6f3a223b1b994f9f95a4f14e49ee61414b6a495 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 10 Apr 2024 10:38:50 +0200 Subject: [PATCH 259/351] Reject data signatures made by non-signing primary key --- .../consumer/CertificateValidator.kt | 13 +- ...ySignatureByCertificationKeyFailsTest.java | 228 ++++++++++++++++++ 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt index e28f99b6..83b7e54e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/CertificateValidator.kt @@ -169,6 +169,17 @@ class CertificateValidator { return true } } + // Reject sigs by non-signing keys + if (userIdSignatures.none { (_, sigs) -> + sigs.any { + SignatureSubpacketsUtil.getKeyFlags(it)?.let { f -> + KeyFlag.hasKeyFlag(f.flags, KeyFlag.SIGN_DATA) + } == true + } + }) { + throw SignatureValidationException( + "Signature was generated by non-signing key.") + } } else { // signing key is subkey val subkeySigs = mutableListOf() signingSubkey @@ -183,7 +194,7 @@ class CertificateValidator { } } catch (e: SignatureValidationException) { rejections[it] = e - LOGGER.debug("REjecting subkey revocation signature: ${e.message}", e) + LOGGER.debug("Rejecting subkey revocation signature: ${e.message}", e) } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java new file mode 100644 index 00000000..3d2ea092 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/VerifySignatureByCertificationKeyFailsTest.java @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class VerifySignatureByCertificationKeyFailsTest { + + // Key with non-signing primary key and dedicated signing subkey + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: ACFC 7FFA 02BE C1E8 5002 2D35 AFE4 67B2 9A41 A0CE\n" + + "Comment: Complex RSA \n" + + "\n" + + "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\n" + + "=hhYV\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static byte[] DATA = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + // Signature by primary key (shall be rejected, as the primary key is not signing capable. + // We instead expect the signing subkey to sign). + private static final String SIG = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: PGPainless\n" + + "\n" + + "iQIcBAABCAAGBQJmEAYYAAoJEK/kZ7KaQaDOH4MP/2kaK8lQaBU+jChpWPLR2R2+\n" + + "dB7j29tFPRAqbzbazxaF+jZQxuuHWtM3bwd9Vdta9zirDc27b7XyufFLBza4Bn+R\n" + + "7fT7uHTQQts/zaX8YGxJ90rb06toFXiv/rlm531kLaGXxlACU6SpI8maqpP4im+G\n" + + "W0LgBDZiT9udFs3eeJZ8O3yDLP29Rdw8sHPa6pOyyhkkhsvo0bNaBaSt6GDW5UK9\n" + + "f5Gz+XF9ZLJgsNqQwWQM55+4ZhdkfEszRcJgAhuSCamk+ZLfvIPhEu21/7weNq3c\n" + + "Yp0hvaz27gaW7IkjEgI1FqkPrmJmyk5SVMMvaev9p0WXDgUIeDLI6CwvoXaoMCAX\n" + + "pg0Q754ccHu2pELwNb5YIxGSPSMXRVH8xUDqicZpl/50ucy3g348s5HekcVnzBtX\n" + + "UKVX3tU6r5HrkVAX7bDGht3WXE1jRE98W3uKpFWzrJBK+uQIyOtOEXeKT+z1BbNy\n" + + "CvYeDq4xjpGYB2tJY2LKXrC4+IzJ56e3XU2t75KhO0SBV+Ax1bJ0MBnmAedg0pg5\n" + + "0r+mknBqoYu+OHNwMS/N+YH1iZEV0QxP+ldLp+ff2QiIvtiDcOIQ0oNEJy0bqh0p\n" + + "TrS9PKMl+kH+FaAPUVb0ruviTd/zPljjiJ2P396bu1JBXdoncn2y4KklQOWHJSVI\n" + + "F5ZjatEBixl6pdW7I5Cr\n" + + "=vPG/\n" + + "-----END PGP SIGNATURE-----"; + + @Test + public void testSignatureByNonSigningPrimaryKeyIsRejected() throws Exception { + PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(KEY); + + DecryptionStream verifier = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(DATA)) + .withOptions(ConsumerOptions.get() + .addVerificationCert(PGPainless.extractCertificate(key)) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(SIG.getBytes(StandardCharsets.UTF_8)))); + + Streams.drain(verifier); + verifier.close(); + + MessageMetadata result = verifier.getMetadata(); + assertFalse(result.isVerifiedSigned()); + } +} From dd3ef89a5ce92fa98a9401a7a90e38fccc97d87a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 10 Apr 2024 10:47:13 +0200 Subject: [PATCH 260/351] Add (failing) test for extracting certificate from key with unknown secret key encryption method --- ...ithUnknownSecretKeyEncryptionMethodTest.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt new file mode 100644 index 00000000..253c0f19 --- /dev/null +++ b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithUnknownSecretKeyEncryptionMethodTest.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.pgpainless.PGPainless + +class KeyWithUnknownSecretKeyEncryptionMethodTest { + + // Test vector from https://gitlab.com/dkg/openpgp-hardware-secrets/-/merge_requests/2 + val KEY = + """-----BEGIN PGP PRIVATE KEY BLOCK----- + +xTQEZgWtcxYJKwYBBAHaRw8BAQdAlLK6UPQsVHR2ETk1SwVIG3tBmpiEtikYYlCy +1TIiqzb8zR08aGFyZHdhcmUtc2VjcmV0QGV4YW1wbGUub3JnPsKNBBAWCAA1AhkB +BQJmBa1zAhsDCAsJCAcKDQwLBRUKCQgLAhYCFiEEXlP8Tur0WZR+f0I33/i9Uh4O +HEkACgkQ3/i9Uh4OHEnryAD8CzH2ajJvASp46ApfI4pLPY57rjBX++d/2FQPRyqG +HJUA/RLsNNgxiFYmK5cjtQe2/DgzWQ7R6PxPC6oa3XM7xPcCxzkEZgWtcxIKKwYB +BAGXVQEFAQEHQE1YXOKeaklwG01Yab4xopP9wbu1E+pCrP1xQpiFZW5KAwEIB/zC +eAQYFggAIAUCZgWtcwIbDBYhBF5T/E7q9FmUfn9CN9/4vVIeDhxJAAoJEN/4vVIe +DhxJVTgA/1WaFrKdP3AgL0Ffdooc5XXbjQsj0uHo6FZSHRI4pchMAQCyJnKQ3RvW +/0gm41JCqImyg2fxWG4hY0N5Q7Rc6PyzDQ== +=3w/O +-----END PGP PRIVATE KEY BLOCK-----""" + + @Test + @Disabled("Disabled since BC 1.77 chokes on the test key") + fun testExtractCertificate() { + val key = PGPainless.readKeyRing().secretKeyRing(KEY)!! + val cert = PGPainless.extractCertificate(key) + + assertNotNull(cert) + // Each secret key got its public key component extracted + assertEquals( + key.secretKeys.asSequence().map { it.keyID }.toSet(), + cert.publicKeys.asSequence().map { it.keyID }.toSet()) + } +} From e9c57a9ed9a6be5e51fd323fc7e0bfd071ba97a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jun 2024 22:11:08 +0200 Subject: [PATCH 261/351] Remove support for generating EC keys over non-standard curve secp256k1 --- .../org/pgpainless/key/generation/type/ecc/EllipticCurve.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt index d9b51cb3..c3d0e2b2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/ecc/EllipticCurve.kt @@ -18,7 +18,6 @@ enum class EllipticCurve(val curveName: String, val bitStrength: Int) { _P521( "secp521r1", 521), // secp521r1 is equivalent to P-521, see https://tools.ietf.org/search/rfc4492#page-32 - _SECP256K1("secp256k1", 256), _BRAINPOOLP256R1("brainpoolP256r1", 256), _BRAINPOOLP384R1("brainpoolP384r1", 384), _BRAINPOOLP512R1("brainpoolP512r1", 512), From 0045f775510b66a14e276779d3e0dc52f45a02b4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jun 2024 22:11:47 +0200 Subject: [PATCH 262/351] Catch UnsupportedPacketVersionExceptions when parsing OnePassSignaturePackets --- .../OpenPgpMessageInputStream.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index ca2159fc..071d8927 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -197,9 +197,13 @@ class OpenPgpMessageInputStream( private fun processOnePassSignature() { syntaxVerifier.next(InputSymbol.ONE_PASS_SIGNATURE) - val ops = packetInputStream!!.readOnePassSignature() - LOGGER.debug( - "One-Pass-Signature Packet by key ${ops.keyID.openPgpKeyId()} at depth ${layerMetadata.depth} encountered.") + val ops = + try { + packetInputStream!!.readOnePassSignature() + } catch (e: UnsupportedPacketVersionException) { + LOGGER.debug("Unsupported One-Pass-Signature packet version encountered.", e) + return + } signatures.addOnePassSignature(ops) } From 185150d70f4452f8c0bcdeaf3aeb34604e5e558d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jun 2024 22:12:09 +0200 Subject: [PATCH 263/351] Bump BC to 1.78.1 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index ec1bf828..9e81459f 100644 --- a/version.gradle +++ b/version.gradle @@ -8,7 +8,7 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.77' + bouncyCastleVersion = '1.78.1' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.2.13' From 9bf41ca19152a83a913fbec6adf8c7e9920de814 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jun 2024 22:14:15 +0200 Subject: [PATCH 264/351] Update changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bb5876..afd55677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ SPDX-License-Identifier: CC0-1.0 ## 2.0.0-SNAPSHOT -- Bump `bcpg-jdk8on` to `1.77` -- Bump `bcprov-jdk18on` to `1.77` +- Bump `bcpg-jdk8on` to `1.78.1` +- Bump `bcprov-jdk18on` to `1.78.1` - Bump `logback-core` and `logback-classic` to `1.4.13` - `pgpainless-core` - Rewrote most of the codebase in Kotlin - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) + - Removed support for generating EC keys over non-standard curve `secp256k1` - `pgpainless-sop`, `pgpainless-cli` - Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key From a9a07ff47d2c5dfb144effae2fa694fce9cae422 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Jun 2024 11:33:49 +0200 Subject: [PATCH 265/351] Set java source compatibility --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 48791e21..04b8a0af 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,10 @@ allprojects { apply plugin: 'kotlin' apply plugin: 'com.diffplug.spotless' + java { + targetCompatibility = JavaVersion.VERSION_1_8 + } + compileJava { options.release = 8 } From b4d2a614596ab15f6b880c9573810685207687a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jun 2024 13:48:00 +0200 Subject: [PATCH 266/351] Add support for padding packet --- .../src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt | 1 + .../decryption_verification/OpenPgpMessageInputStream.kt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt index ecb8db35..2041450a 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt @@ -22,6 +22,7 @@ enum class OpenPgpPacket(val tag: Int) { UATTR(17), SEIPD(18), MDC(19), + PADDING(21), EXP_1(60), EXP_2(61), EXP_3(62), diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 071d8927..7c660cc2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -144,6 +144,10 @@ class OpenPgpMessageInputStream( LOGGER.debug("Skipping Marker Packet") pIn.readMarker() } + OpenPgpPacket.PADDING -> { + LOGGER.debug("Skipping Padding Packet") + pIn.readPacket() + } OpenPgpPacket.SK, OpenPgpPacket.PK, OpenPgpPacket.SSK, From 6f46f75602fcfb173951c142e7bc761b11a0c1ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 21 Jun 2024 14:12:54 +0200 Subject: [PATCH 267/351] Add PublicKeyAlgorithm entries for X25519, X448, Ed25519, Ed448 --- .../org/pgpainless/algorithm/PublicKeyAlgorithm.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt index 8cf03420..b8fc6836 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -61,6 +61,18 @@ enum class PublicKeyAlgorithm( /** Digital Signature Algorithm based on twisted Edwards Curves. */ EDDSA_LEGACY(22, true, false), + + /** X25519 encryption algorithm. */ + X25519(25, false, true), + + /** X448 encryption algorithm. */ + X448(26, false, true), + + /** Ed25519 signature algorithm. */ + ED25519(27, true, false), + + /** Ed448 signature algorithm. */ + ED448(28, true, false), ; fun isSigningCapable(): Boolean = signingCapable From 69a57ef3bc0245bafe7deb8145ea6518213f2d95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Aug 2024 13:41:51 +0200 Subject: [PATCH 268/351] Deprecate addPassphrase()/addDecryptionPassphrase in favor of addMessagePassphrase() --- .../decryption_verification/ConsumerOptions.kt | 17 ++++++++++++++++- .../encryption_signing/EncryptionOptions.kt | 13 ++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index 5007c8a5..39a4e8e4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -196,7 +196,22 @@ class ConsumerOptions { * @param passphrase passphrase * @return options */ - fun addDecryptionPassphrase(passphrase: Passphrase) = apply { + @Deprecated( + "Deprecated in favor of addMessagePassphrase", + ReplaceWith("addMessagePassphrase(passphrase)")) + fun addDecryptionPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase) + + /** + * Add a passphrase for message decryption. This passphrase will be used to try to decrypt + * messages which were symmetrically encrypted for a passphrase. + * + * See + * [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * + * @param passphrase passphrase + * @return options + */ + fun addMessagePassphrase(passphrase: Passphrase) = apply { decryptionPassphrases.add(passphrase) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index 3e232654..d26acc20 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -227,7 +227,18 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { * @param passphrase passphrase * @return this */ - fun addPassphrase(passphrase: Passphrase) = apply { + @Deprecated( + "Deprecated in favor of addMessagePassphrase", + ReplaceWith("addMessagePassphrase(passphrase)")) + fun addPassphrase(passphrase: Passphrase) = addMessagePassphrase(passphrase) + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + * @return this + */ + fun addMessagePassphrase(passphrase: Passphrase) = apply { require(!passphrase.isEmpty) { "Passphrase MUST NOT be empty." } addEncryptionMethod( ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) From 67457bbe785cdbcb824cd5000a572ae1867caa6b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 22 Aug 2024 13:42:30 +0200 Subject: [PATCH 269/351] Replace use of addPassphrase()/addDecryptionPassphrase() in tests with addMessagePassphrase() --- .../DecryptAndVerifyMessageTest.java | 2 +- .../OpenPgpMessageInputStreamTest.java | 6 +++--- ...stponeDecryptionUsingKeyWithMissingPassphraseTest.java | 2 +- .../encryption_signing/EncryptionOptionsTest.java | 2 +- .../encryption_signing/EncryptionStreamClosedTest.java | 2 +- .../encryption_signing/HideArmorHeadersTest.java | 2 +- .../src/test/java/org/pgpainless/example/Encrypt.java | 4 ++-- .../MultiPassphraseSymmetricEncryptionTest.java | 6 +++--- .../symmetric_encryption/SymmetricEncryptionTest.java | 8 ++++---- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java index 4b6d5a86..82796cb9 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptAndVerifyMessageTest.java @@ -153,6 +153,6 @@ public class DecryptAndVerifyMessageTest { () -> PGPainless.decryptAndOrVerify() .onInputStream(ciphertextIn) .withOptions(ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword("sw0rdf1sh")))); + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh")))); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java index a0ec7c25..6102372a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -282,7 +282,7 @@ public class OpenPgpMessageInputStreamTest { EncryptionStream enc = PGPainless.encryptAndOrSign() .onOutputStream(System.out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))) .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); enc.close(); @@ -409,7 +409,7 @@ public class OpenPgpMessageInputStreamTest { public void testProcessSENC_LIT(Processor processor) throws PGPException, IOException { Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); String plain = result.getA(); assertEquals(PLAINTEXT, plain); MessageMetadata metadata = result.getB(); @@ -656,7 +656,7 @@ public class OpenPgpMessageInputStreamTest { @Test public void readAfterCloseTest() throws PGPException, IOException { OpenPgpMessageInputStream pgpIn = get(SENC_LIT, ConsumerOptions.get() - .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + .addMessagePassphrase(Passphrase.fromPassword(PASSPHRASE))); Streams.drain(pgpIn); // read all byte[] buf = new byte[1024]; diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index c86a823c..8489da9a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -193,7 +193,7 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K2_PASS_K1.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(PASSPHRASE) + .addMessagePassphrase(PASSPHRASE) .addDecryptionKey(k1, protector) .addDecryptionKey(k2, protector)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 2c1cd9f6..5f6c6c15 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -131,7 +131,7 @@ public class EncryptionOptionsTest { public void testAddEmptyPassphraseFails() { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> - options.addPassphrase(Passphrase.emptyPassphrase())); + options.addMessagePassphrase(Passphrase.emptyPassphrase())); } @Test 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 4ac43630..ae640661 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 @@ -26,7 +26,7 @@ public class EncryptionStreamClosedTest { EncryptionStream stream = PGPainless.encryptAndOrSign() .onOutputStream(out) .withOptions(ProducerOptions.encrypt(EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("dummy")))); + .addMessagePassphrase(Passphrase.fromPassword("dummy")))); // No close() called => getResult throws assertThrows(IllegalStateException.class, stream::getResult); diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java index 242b430b..59ad4596 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/HideArmorHeadersTest.java @@ -23,7 +23,7 @@ public class HideArmorHeadersTest { .onOutputStream(out) .withOptions(ProducerOptions.encrypt( EncryptionOptions.get() - .addPassphrase(Passphrase.fromPassword("sw0rdf1sh"))) + .addMessagePassphrase(Passphrase.fromPassword("sw0rdf1sh"))) .setHideArmorHeaders(true)); encryptionStream.write("Hello, World!\n".getBytes()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index ba832516..d97891d8 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -195,7 +195,7 @@ public class Encrypt { .onOutputStream(ciphertext) .withOptions(ProducerOptions .encrypt(EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("p4ssphr4s3")) + .addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3")) ).setAsciiArmor(true) ); @@ -207,7 +207,7 @@ public class Encrypt { // Decrypt DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(asciiCiphertext.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionPassphrase(Passphrase.fromPassword("p4ssphr4s3"))); + .withOptions(new ConsumerOptions().addMessagePassphrase(Passphrase.fromPassword("p4ssphr4s3"))); ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); Streams.pipeAll(decryptor, plaintext); 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 5132ef57..d0d37117 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 @@ -35,8 +35,8 @@ public class MultiPassphraseSymmetricEncryptionTest { .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("p1")) - .addPassphrase(Passphrase.fromPassword("p2")) + .addMessagePassphrase(Passphrase.fromPassword("p1")) + .addMessagePassphrase(Passphrase.fromPassword("p2")) ).setAsciiArmor(false)); Streams.pipeAll(plaintextIn, encryptor); @@ -49,7 +49,7 @@ public class MultiPassphraseSymmetricEncryptionTest { DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext)) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(passphrase)); + .addMessagePassphrase(passphrase)); ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); 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 d3c503ab..dbf7ca24 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 @@ -53,7 +53,7 @@ public class SymmetricEncryptionTest { .onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(encryptionPassphrase) + .addMessagePassphrase(encryptionPassphrase) .addRecipient(encryptionKey) )); @@ -66,7 +66,7 @@ public class SymmetricEncryptionTest { DecryptionStream decryptor = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(ciphertext)) .withOptions(new ConsumerOptions() - .addDecryptionPassphrase(encryptionPassphrase)); + .addMessagePassphrase(encryptionPassphrase)); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); @@ -103,7 +103,7 @@ public class SymmetricEncryptionTest { EncryptionStream encryptor = PGPainless.encryptAndOrSign().onOutputStream(ciphertextOut) .withOptions(ProducerOptions.encrypt( EncryptionOptions.encryptCommunications() - .addPassphrase(Passphrase.fromPassword("mellon")))); + .addMessagePassphrase(Passphrase.fromPassword("mellon")))); Streams.pipeAll(new ByteArrayInputStream(bytes), encryptor); encryptor.close(); @@ -112,6 +112,6 @@ public class SymmetricEncryptionTest { .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) .withOptions(new ConsumerOptions() .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) - .addDecryptionPassphrase(Passphrase.fromPassword("meldir")))); + .addMessagePassphrase(Passphrase.fromPassword("meldir")))); } } From b719810575a4001d461b55f0553daed65d2eb60c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 16 Sep 2024 14:10:09 +0200 Subject: [PATCH 270/351] Fix linking in KDoc documentation --- .../main/kotlin/org/pgpainless/PGPainless.kt | 2 +- .../org/pgpainless/algorithm/KeyFlag.kt | 2 +- .../org/pgpainless/algorithm/SignatureType.kt | 2 +- .../encryption_signing/EncryptionOptions.kt | 32 ++++++------ .../encryption_signing/ProducerOptions.kt | 3 +- .../org/pgpainless/key/OpenPgpFingerprint.kt | 14 ++--- .../org/pgpainless/key/_64DigitFingerprint.kt | 2 +- .../key/generation/KeyRingTemplates.kt | 18 +++---- .../pgpainless/key/generation/type/KeyType.kt | 13 +++-- .../org/pgpainless/key/info/KeyRingInfo.kt | 14 ++--- .../org/pgpainless/key/util/KeyIdUtil.kt | 2 +- .../org/pgpainless/key/util/KeyRingUtils.kt | 51 +++++++++---------- .../kotlin/org/pgpainless/policy/Policy.kt | 17 +++---- .../subpackets/SignatureSubpacketsUtil.kt | 2 +- .../kotlin/org/pgpainless/util/ArmorUtils.kt | 6 +-- .../org/pgpainless/util/NotationRegistry.kt | 2 +- 16 files changed, 88 insertions(+), 94 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt index d866ac93..81d9e605 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/PGPainless.kt @@ -129,7 +129,7 @@ class PGPainless private constructor() { * *

* After making the desired changes in the builder, the modified key can be extracted using - * {@link SecretKeyRingEditorInterface#done()}. + * [org.pgpainless.key.modification.secretkeyring.SecretKeyRingEditorInterface.done]. * * @param secretKeys secret key ring * @param referenceTime reference time used as signature creation date diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt index b8bc2d96..c5c4e103 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt @@ -42,7 +42,7 @@ enum class KeyFlag(val flag: Int) { } /** - * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. + * Encode a list of [KeyFlags][KeyFlag] into a bitmask. * * @param flags list of flags * @return bitmask diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt index 3cca8655..e6b2299f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt @@ -23,7 +23,7 @@ enum class SignatureType(val code: Int) { /** * Signature of a canonical text document. This means the signer owns it, created it, or * certifies that it has not been modified. The signature is calculated over the text data with - * its line endings converted to {@code }. + * its line endings converted to ``. */ CANONICAL_TEXT_DOCUMENT(0x01), diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt index d26acc20..f261b85e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -50,16 +50,16 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { constructor() : this(EncryptionPurpose.ANY) /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * Factory method to create an [EncryptionOptions] object which will encrypt for keys which + * carry the flag [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS]. * * @return encryption options */ fun setEvaluationDate(evaluationDate: Date) = apply { this.evaluationDate = evaluationDate } /** - * Identify authenticatable certificates for the given user-ID by querying the {@link - * CertificateAuthority} for identifiable bindings. Add all acceptable bindings, whose trust + * Identify authenticatable certificates for the given user-ID by querying the + * [CertificateAuthority] for identifiable bindings. Add all acceptable bindings, whose trust * amount is larger or equal to the target amount to the list of recipients. * * @param userId userId @@ -88,8 +88,8 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { } /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) - * as recipients. + * Add all key rings in the provided [Iterable] (e.g. + * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. * * @param keys keys * @return this @@ -102,9 +102,9 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { } /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) - * as recipients. Per key ring, the selector is applied to select one or more encryption - * subkeys. + * Add all key rings in the provided [Iterable] (e.g. + * [org.bouncycastle.openpgp.PGPPublicKeyRingCollection]) as recipients. Per key ring, the + * selector is applied to select one or more encryption subkeys. * * @param keys keys * @param selector encryption key selector @@ -245,9 +245,9 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { } /** - * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. - * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) or {@link - * PGPKeyEncryptionMethodGenerator} (public key). + * Add a [PGPKeyEncryptionMethodGenerator] which will be used to encrypt the message. Method + * generators are either [org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator] + * (passphrase) or [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 @@ -278,10 +278,10 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) { } /** - * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will - * allow encryption for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} - * subpacket. This is a workaround for dealing with legacy keys that have no key flags subpacket - * but rely on the key algorithm type to convey the subkeys use. + * If this method is called, subsequent calls to [addRecipient] will allow encryption for + * subkeys that do not carry any [org.pgpainless.algorithm.KeyFlag] subpacket. This is a + * workaround for dealing with legacy keys that have no key flags subpacket but rely on the key + * algorithm type to convey the subkeys use. * * @return this */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt index 871f8950..e77ef2e3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/ProducerOptions.kt @@ -176,8 +176,7 @@ private constructor( * @return this * @see RFC4880 §5.9. * Literal Data Packet - * @deprecated options other than the default value of {@link StreamEncoding#BINARY} are - * discouraged. + * @deprecated options other than the default value of [StreamEncoding.BINARY] are discouraged. */ @Deprecated("Options other than BINARY are discouraged.") fun setEncoding(encoding: StreamEncoding) = apply { encodingField = encoding } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt index c67c3983..9a2f1f7b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/OpenPgpFingerprint.kt @@ -23,8 +23,8 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable abstract fun getVersion(): Int /** - * Return the key id of the OpenPGP public key this {@link OpenPgpFingerprint} belongs to. This - * method can be implemented for V4 and V5 fingerprints. V3 key-IDs cannot be derived from the + * Return the key id of the OpenPGP public key this [OpenPgpFingerprint] belongs to. This method + * can be implemented for V4 and V5 fingerprints. V3 key-IDs cannot be derived from the * fingerprint, but we don't care, since V3 is deprecated. * * @return key id @@ -127,10 +127,10 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable @JvmStatic fun of(keys: PGPKeyRing): OpenPgpFingerprint = of(keys.publicKey) /** - * Try to parse an {@link OpenPgpFingerprint} from the given fingerprint string. If the - * trimmed fingerprint without whitespace is 64 characters long, it is either a v5 or v6 - * fingerprint. In this case, we return a {@link _64DigitFingerprint}. Since this is - * ambiguous, it is generally recommended to know the version of the key beforehand. + * Try to parse an [OpenPgpFingerprint] from the given fingerprint string. If the trimmed + * fingerprint without whitespace is 64 characters long, it is either a v5 or v6 + * fingerprint. In this case, we return a [_64DigitFingerprint]. Since this is ambiguous, it + * is generally recommended to know the version of the key beforehand. * * @param fingerprint fingerprint * @return parsed fingerprint @@ -152,7 +152,7 @@ abstract class OpenPgpFingerprint : CharSequence, Comparable } /** - * Parse a binary OpenPGP fingerprint into an {@link OpenPgpFingerprint} object. + * Parse a binary OpenPGP fingerprint into an [OpenPgpFingerprint] object. * * @param binaryFingerprint binary representation of the fingerprint * @return parsed fingerprint diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt index 279815bd..a34dd880 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/_64DigitFingerprint.kt @@ -20,7 +20,7 @@ import org.bouncycastle.util.encoders.Hex open class _64DigitFingerprint : OpenPgpFingerprint { /** - * Create an {@link _64DigitFingerprint}. + * Create a [_64DigitFingerprint]. * * @param fingerprint uppercase hexadecimal fingerprint of length 64 */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt index e790ab17..82743661 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingTemplates.kt @@ -64,14 +64,13 @@ class KeyRingTemplates { } /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. The - * KeyPair consists of a single RSA master key which is used for signing, encryption and - * certification. + * Creates a simple RSA KeyPair of length `length` with user-id `userId`. The KeyPair consists + * of a single RSA master key which is used for signing, encryption and certification. * * @param userId user id. * @param length length in bits. * @param password Password of the key. Can be empty for unencrypted keys. - * @return {@link PGPSecretKeyRing} containing the KeyPair. + * @return [PGPSecretKeyRing] containing the KeyPair. */ @JvmOverloads fun simpleRsaKeyRing( @@ -95,14 +94,13 @@ class KeyRingTemplates { .build() /** - * Creates a simple RSA KeyPair of length {@code length} with user-id {@code userId}. The - * KeyPair consists of a single RSA master key which is used for signing, encryption and - * certification. + * Creates a simple RSA KeyPair of length `length` with user-id `userId`. The KeyPair consists + * of a single RSA master key which is used for signing, encryption and certification. * * @param userId user id. * @param length length in bits. * @param password Password of the key. Can be null or blank for unencrypted keys. - * @return {@link PGPSecretKeyRing} containing the KeyPair. + * @return [PGPSecretKeyRing] containing the KeyPair. */ fun simpleRsaKeyRing(userId: CharSequence?, length: RsaLength, password: String?) = password.let { @@ -120,7 +118,7 @@ class KeyRingTemplates { * * @param userId user-id * @param passphrase Password of the private key. Can be empty for an unencrypted key. - * @return {@link PGPSecretKeyRing} containing the key pairs. + * @return [PGPSecretKeyRing] containing the key pairs. */ @JvmOverloads fun simpleEcKeyRing( @@ -153,7 +151,7 @@ class KeyRingTemplates { * * @param userId user-id * @param passphrase Password of the private key. Can be null or blank for an unencrypted key. - * @return {@link PGPSecretKeyRing} containing the key pairs. + * @return [PGPSecretKeyRing] containing the key pairs. */ fun simpleEcKeyRing(userId: CharSequence?, password: String?): PGPSecretKeyRing = password.let { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt index ea145b7f..c7691f46 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/type/KeyType.kt @@ -41,8 +41,7 @@ interface KeyType { val bitStrength: Int /** - * Return an implementation of {@link AlgorithmParameterSpec} that can be used to generate the - * key. + * Return an implementation of [AlgorithmParameterSpec] that can be used to generate the key. * * @return algorithm parameter spec */ @@ -50,7 +49,7 @@ interface KeyType { /** * Return true if the key that is generated from this type is able to carry the SIGN_DATA key - * flag. See {@link org.pgpainless.algorithm.KeyFlag#SIGN_DATA}. + * flag. See [org.pgpainless.algorithm.KeyFlag.SIGN_DATA]. * * @return true if the key can sign. */ @@ -59,7 +58,7 @@ interface KeyType { /** * Return true if the key that is generated from this type is able to carry the CERTIFY_OTHER - * key flag. See {@link org.pgpainless.algorithm.KeyFlag#CERTIFY_OTHER}. + * key flag. See [org.pgpainless.algorithm.KeyFlag.CERTIFY_OTHER]. * * @return true if the key is able to certify other keys */ @@ -68,7 +67,7 @@ interface KeyType { /** * Return true if the key that is generated from this type is able to carry the AUTHENTICATION - * key flag. See {@link org.pgpainless.algorithm.KeyFlag#AUTHENTICATION}. + * key flag. See [org.pgpainless.algorithm.KeyFlag.AUTHENTICATION]. * * @return true if the key can be used for authentication purposes. */ @@ -77,7 +76,7 @@ interface KeyType { /** * Return true if the key that is generated from this type is able to carry the ENCRYPT_COMMS - * key flag. See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * key flag. See [org.pgpainless.algorithm.KeyFlag.ENCRYPT_COMMS]. * * @return true if the key can encrypt communication */ @@ -86,7 +85,7 @@ interface KeyType { /** * Return true if the key that is generated from this type is able to carry the ENCRYPT_STORAGE - * key flag. See {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. + * key flag. See [org.pgpainless.algorithm.KeyFlag.ENCRYPT_STORAGE]. * * @return true if the key can encrypt for storage */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 56dafe10..f4305225 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -35,7 +35,7 @@ class KeyRingInfo( private val signatures: Signatures = Signatures(keys, referenceDate, policy) - /** Primary {@link PGPPublicKey}.´ */ + /** Primary [PGPPublicKey]. */ val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) /** Primary key ID. */ @@ -73,8 +73,8 @@ class KeyRingInfo( val version: Int = publicKey.version /** - * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. The first key in the list - * being the primary key. Note that the list is unmodifiable. + * Return all [PGPPublicKeys][PGPPublicKey] of this key ring. The first key in the list being + * the primary key. Note that the list is unmodifiable. * * @return list of public keys */ @@ -448,7 +448,7 @@ class KeyRingInfo( signatures.subkeyRevocations[keyId] /** - * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. + * Return a list of [KeyFlags][KeyFlag] that apply to the subkey with the provided key id. * * @param keyId key-id * @return list of key flags @@ -478,7 +478,7 @@ class KeyRingInfo( } /** - * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. + * Return a list of [KeyFlags][KeyFlag] that apply to the given user-id. * * @param userId user-id * @return key flags @@ -622,8 +622,8 @@ class KeyRingInfo( * Return the current primary user-id of the key ring. * *

- * Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, this - * method returns the first user-id on the key, otherwise null. + * Note: If no user-id is marked as primary key using a [PrimaryUserID] packet, this method + * returns the first user-id on the key, otherwise null. * * @return primary user-id or null */ diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt index b26d84dc..3f1b98b1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyIdUtil.kt @@ -15,7 +15,7 @@ class KeyIdUtil { * Convert a long key-id into a key-id. A long key-id is a 16 digit hex string. * * @param longKeyId 16-digit hexadecimal string - * @return key-id converted to {@link Long}. + * @return key-id converted to [Long]. */ @JvmStatic @Deprecated( diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt index 2736f625..f83b5486 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -27,8 +27,8 @@ class KeyRingUtils { @JvmStatic private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. If it - * has no primary secret key, throw a {@link NoSuchElementException}. + * Return the primary [PGPSecretKey] from the provided [PGPSecretKeyRing]. If it has no + * primary secret key, throw a [NoSuchElementException]. * * @param secretKeys secret keys * @return primary secret key @@ -61,8 +61,8 @@ class KeyRingUtils { } /** - * Return the primary {@link PGPPublicKey} from the provided key ring. Throws a {@link - * NoSuchElementException} if the key ring has no primary public key. + * Return the primary [PGPPublicKey] from the provided key ring. Throws a + * [NoSuchElementException] if the key ring has no primary public key. * * @param keyRing key ring * @return primary public key @@ -74,8 +74,7 @@ class KeyRingUtils { } /** - * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has - * none. + * Return the primary [PGPPublicKey] from the provided key ring or null if it has none. * * @param keyRing key ring * @return primary public key @@ -93,7 +92,7 @@ class KeyRingUtils { /** * Require the public key with the given subKeyId from the keyRing. If no such subkey - * exists, throw an {@link NoSuchElementException}. + * exists, throw an [NoSuchElementException]. * * @param keyRing key ring * @param subKeyId subkey id @@ -108,7 +107,7 @@ class KeyRingUtils { /** * Require the secret key with the given secret subKeyId from the secret keyRing. If no such - * subkey exists, throw an {@link NoSuchElementException}. + * subkey exists, throw an [NoSuchElementException]. * * @param keyRing secret key ring * @param subKeyId subkey id @@ -131,8 +130,8 @@ class KeyRingUtils { } /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link - * PGPSecretKeyRing}. + * Extract a [PGPPublicKeyRing] containing all public keys from the provided + * [PGPSecretKeyRing]. * * @param secretKeys secret key ring * @return public key ring @@ -146,9 +145,9 @@ class KeyRingUtils { } /** - * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing - * PGPSecretKeyRings} in the given {@link PGPSecretKeyRingCollection} and return them as a - * {@link PGPPublicKeyRingCollection}. + * Extract [PGPPublicKeyRings][PGPPublicKeyRing] from all + * [PGPSecretKeyRings][PGPSecretKeyRing] in the given [PGPSecretKeyRingCollection] and + * return them as a [PGPPublicKeyRingCollection]. * * @param secretKeyRings secret key ring collection * @return public key ring collection @@ -162,8 +161,8 @@ class KeyRingUtils { } /** - * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing - * PGPPublicKeyRings}. + * Create a new [PGPPublicKeyRingCollection] from an array of + * [PGPPublicKeyRings][PGPPublicKeyRing]. * * @param certificates array of public key rings * @return key ring collection @@ -176,8 +175,8 @@ class KeyRingUtils { } /** - * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing - * PGPSecretKeyRings}. + * Create a new [PGPSecretKeyRingCollection] from an array of + * [PGPSecretKeyRings][PGPSecretKeyRing]. * * @param secretKeys array of secret key rings * @return secret key ring collection @@ -190,8 +189,8 @@ class KeyRingUtils { } /** - * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for - * the given key id. + * Return true, if the given [PGPPublicKeyRing] contains a [PGPPublicKey] for the given key + * id. * * @param certificate public key ring * @param keyId id of the key in question @@ -207,7 +206,7 @@ class KeyRingUtils { * * @param keyRing key ring * @param certification key signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] * @return key ring with injected signature */ @JvmStatic @@ -221,7 +220,7 @@ class KeyRingUtils { * @param keyRing key ring * @param certifiedKey signed public key * @param certification key signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] * @return key ring with injected signature * @throws NoSuchElementException in case that the signed key is not part of the key ring */ @@ -265,7 +264,7 @@ class KeyRingUtils { * @param keyRing key ring * @param userId signed user-id * @param certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] * @return key ring with injected certification */ @JvmStatic @@ -300,7 +299,7 @@ class KeyRingUtils { * @param keyRing key ring * @param userAttributes certified user attributes * @param certification certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] * @return key ring with injected user-attribute certification */ @JvmStatic @@ -330,11 +329,11 @@ class KeyRingUtils { } /** - * Inject a {@link PGPPublicKey} into the given key ring. + * Inject a [PGPPublicKey] into the given key ring. * * @param keyRing key ring * @param publicKey public key - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @param either [PGPPublicKeyRing] or [PGPSecretKeyRing] * @return key ring with injected public key */ @JvmStatic @@ -372,7 +371,7 @@ class KeyRingUtils { } /** - * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. + * Inject a [PGPSecretKey] into a [PGPSecretKeyRing]. * * @param secretKeys secret key ring * @param secretKey secret key diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt index cb209025..7c6bb2d3 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/policy/Policy.kt @@ -284,8 +284,8 @@ class Policy( companion object { /** - * Default {@link CompressionAlgorithmPolicy} of PGPainless. The default compression - * algorithm policy accepts any compression algorithm. + * Default [CompressionAlgorithmPolicy] of PGPainless. The default compression algorithm + * policy accepts any compression algorithm. * * @return default algorithm policy * @deprecated not expressive - might be removed in a future release @@ -385,17 +385,16 @@ class Policy( enum class SignerUserIdValidationLevel { /** - * PGPainless will verify {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets in - * signatures strictly. This means, that signatures with Signer's User-ID subpackets - * containing a value that does not match the signer key's user-id exactly, will be - * rejected. E.g. Signer's user-id "alice@pgpainless.org", User-ID: "Alice - * <alice@pgpainless.org>" does not match exactly and is therefore rejected. + * PGPainless will verify [org.bouncycastle.bcpg.sig.SignerUserID] subpackets in signatures + * strictly. This means, that signatures with Signer's User-ID subpackets containing a value + * that does not match the signer key's user-id exactly, will be rejected. E.g. Signer's + * user-id "alice@pgpainless.org", User-ID: "Alice <alice@pgpainless.org>" does not + * match exactly and is therefore rejected. */ STRICT, /** - * PGPainless will ignore {@link org.bouncycastle.bcpg.sig.SignerUserID} subpackets on - * signature. + * PGPainless will ignore [org.bouncycastle.bcpg.sig.SignerUserID] subpackets on signature. */ DISABLED } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt index 31e1f53c..dcc85630 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.kt @@ -64,7 +64,7 @@ class SignatureSubpacketsUtil { * such packet is present, return null. * * @param signature signature - * @return issuer key-id as {@link Long} + * @return issuer key-id as [Long] */ @JvmStatic fun getIssuerKeyIdAsLong(signature: PGPSignature): Long? = getIssuerKeyId(signature)?.keyID diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index 70146e0f..b5d5b839 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -153,8 +153,8 @@ class ArmorUtils { ): String = toAsciiArmoredString(bytes.inputStream(), header) /** - * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}. - * The ASCII armor will include armor headers from the given header map. + * Return the ASCII armored encoding of the OpenPGP data from the given [InputStream]. The + * ASCII armor will include armor headers from the given header map. * * @param inputStream input stream of OpenPGP data * @param header ASCII armor header map @@ -179,7 +179,7 @@ class ArmorUtils { /** * Return an [ArmoredOutputStream] prepared with headers for the given key ring, which wraps - * the given {@link OutputStream}. + * the given [OutputStream]. * * The armored output stream can be used to encode the key ring by calling * [PGPKeyRing.encode] with the armored output stream as an argument. diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt index d2295f32..96200b30 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/NotationRegistry.kt @@ -9,7 +9,7 @@ package org.pgpainless.util * notations that are not known to the application, there must be some way to tell PGPainless which * notations actually are known. * - * To add a notation name, call {@link #addKnownNotation(String)}. + * To add a notation name, call [addKnownNotation]. */ class NotationRegistry constructor(notations: Set = setOf()) { private val knownNotations: MutableSet From 3c343dc45c15c1d062b2c426ef76018fc52bcd13 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 17 Jul 2024 23:22:07 +0200 Subject: [PATCH 271/351] Prevent overreading when decompressing data --- .../OpenPgpMessageInputStream.kt | 85 +++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 7c660cc2..216106b1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -4,28 +4,57 @@ package org.pgpainless.decryption_verification +import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.util.zip.Inflater +import java.util.zip.InflaterInputStream import openpgp.openPgpKeyId import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.CompressionAlgorithmTags import org.bouncycastle.bcpg.UnsupportedPacketVersionException -import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.PGPCompressedData +import org.bouncycastle.openpgp.PGPEncryptedData +import org.bouncycastle.openpgp.PGPEncryptedDataList +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPBEEncryptedData +import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.util.io.TeeInputStream import org.pgpainless.PGPainless -import org.pgpainless.algorithm.* +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.OpenPgpPacket +import org.pgpainless.algorithm.StreamEncoding +import org.pgpainless.algorithm.SymmetricKeyAlgorithm import org.pgpainless.bouncycastle.extensions.getPublicKeyFor import org.pgpainless.bouncycastle.extensions.getSecretKeyFor import org.pgpainless.bouncycastle.extensions.issuerKeyId import org.pgpainless.bouncycastle.extensions.unlock -import org.pgpainless.decryption_verification.MessageMetadata.* +import org.pgpainless.decryption_verification.MessageMetadata.CompressedData +import org.pgpainless.decryption_verification.MessageMetadata.EncryptedData +import org.pgpainless.decryption_verification.MessageMetadata.Layer +import org.pgpainless.decryption_verification.MessageMetadata.LiteralData +import org.pgpainless.decryption_verification.MessageMetadata.Message +import org.pgpainless.decryption_verification.MessageMetadata.Nested import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil import org.pgpainless.decryption_verification.syntax_check.InputSymbol import org.pgpainless.decryption_verification.syntax_check.PDA import org.pgpainless.decryption_verification.syntax_check.StackSymbol -import org.pgpainless.exception.* +import org.pgpainless.exception.MalformedOpenPgpMessageException +import org.pgpainless.exception.MessageNotIntegrityProtectedException +import org.pgpainless.exception.MissingDecryptionMethodException +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.exception.SignatureValidationException +import org.pgpainless.exception.UnacceptableAlgorithmException import org.pgpainless.implementation.ImplementationFactory import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.util.KeyRingUtils @@ -196,7 +225,53 @@ class OpenPgpMessageInputStream( LOGGER.debug( "Compressed Data Packet (${compressionLayer.algorithm}) at depth ${layerMetadata.depth} encountered.") nestedInputStream = - OpenPgpMessageInputStream(compressedData.dataStream, options, compressionLayer, policy) + OpenPgpMessageInputStream(decompress(compressedData), options, compressionLayer, policy) + } + + private fun decompress(compressedData: PGPCompressedData): InputStream { + return when (compressedData.algorithm) { + CompressionAlgorithmTags.ZIP -> + object : InflaterInputStream(compressedData.inputStream, Inflater(true)) { + private var eof = false + + override fun fill() { + if (eof) { + throw EOFException("Unexpected end of ZIP input stream") + } + + len = `in`.read(buf, 0, buf.size) + + if (len == -1) { + buf[0] = 0 + len = 0 + eof = true + } + + inf.setInput(buf, 0, len) + } + } + CompressionAlgorithmTags.ZLIB -> + object : InflaterInputStream(compressedData.inputStream) { + private var eof = false + + override fun fill() { + if (eof) { + throw EOFException("Unexpected end of ZIP input stream") + } + + len = `in`.read(buf, 0, buf.size) + + if (len == -1) { + buf[0] = 0 + len = 0 + eof = true + } + + inf.setInput(buf, 0, len) + } + } + else -> compressedData.dataStream + } } private fun processOnePassSignature() { From ad2976dbcce4eedfc56c4f75f92e7dbeb130e659 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 11 Aug 2024 13:42:42 +0200 Subject: [PATCH 272/351] SOP: KeyReader is silent --- pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt index 0931a3a5..2ce608ca 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/KeyReader.kt @@ -49,7 +49,7 @@ class KeyReader { ): PGPPublicKeyRingCollection { val certs = try { - PGPainless.readKeyRing().keyRingCollection(certIn, false) + PGPainless.readKeyRing().keyRingCollection(certIn, true) } catch (e: IOException) { if (e.message == null) { throw e From 5dfebc5bdeeb306a6b89e069b38a8b4dbc3b185c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 11:58:04 +0200 Subject: [PATCH 273/351] Add support for LibrePGP OED packet --- .../kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt | 1 + .../decryption_verification/OpenPgpMessageInputStream.kt | 9 +++++++-- .../decryption_verification/TeeBCPGInputStream.kt | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt index 2041450a..17ec90e4 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt @@ -22,6 +22,7 @@ enum class OpenPgpPacket(val tag: Int) { UATTR(17), SEIPD(18), MDC(19), + OED(20), PADDING(21), EXP_1(60), EXP_2(61), diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 216106b1..bd24b245 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -163,7 +163,8 @@ class OpenPgpMessageInputStream( OpenPgpPacket.PKESK, OpenPgpPacket.SKESK, OpenPgpPacket.SED, - OpenPgpPacket.SEIPD -> { + OpenPgpPacket.SEIPD, + OpenPgpPacket.OED -> { if (processEncryptedData()) { break@layer } @@ -185,6 +186,10 @@ class OpenPgpMessageInputStream( OpenPgpPacket.UID, OpenPgpPacket.UATTR -> throw MalformedOpenPgpMessageException("Illegal Packet in Stream: $packet") + OpenPgpPacket.PADDING -> { + LOGGER.debug("Padding packet") + pIn.readPadding() + } OpenPgpPacket.EXP_1, OpenPgpPacket.EXP_2, OpenPgpPacket.EXP_3, @@ -319,7 +324,7 @@ class OpenPgpMessageInputStream( "Symmetrically Encrypted Data Packet at depth ${layerMetadata.depth} encountered.") syntaxVerifier.next(InputSymbol.ENCRYPTED_DATA) val encDataList = packetInputStream!!.readEncryptedDataList() - if (!encDataList.isIntegrityProtected) { + if (!encDataList.isIntegrityProtected && !encDataList.get(0).isAEAD) { LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") if (!options.isIgnoreMDCErrors()) { throw MessageNotIntegrityProtectedException() diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt index 73c10e8a..a9b353ca 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/TeeBCPGInputStream.kt @@ -14,6 +14,7 @@ import org.bouncycastle.openpgp.PGPCompressedData import org.bouncycastle.openpgp.PGPEncryptedDataList import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPOnePassSignature +import org.bouncycastle.openpgp.PGPPadding import org.bouncycastle.openpgp.PGPSignature import org.pgpainless.algorithm.OpenPgpPacket @@ -75,6 +76,10 @@ class TeeBCPGInputStream(inputStream: BCPGInputStream, outputStream: OutputStrea return (readPacket() as MarkerPacket).also { delayedTee.squeeze() } } + fun readPadding(): PGPPadding { + return PGPPadding(packetInputStream).also { delayedTee.squeeze() } + } + fun close() { packetInputStream.close() } From 19e389133a7248963f88c5f072427d4da387663d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:12:03 +0200 Subject: [PATCH 274/351] Actually bump logback-core to 1.4.14 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 9e81459f..9db85f33 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.78.1' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.2.13' + logbackVersion = '1.4.14' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '10.0.0' From 039595f5d620e8c5b3a577faa6b24915a9ba1bea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:20:16 +0200 Subject: [PATCH 275/351] Update changelog --- CHANGELOG.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd55677..c5d0a314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,26 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 2.0.0-SNAPSHOT +## 1.7.0-SNAPSHOT - Bump `bcpg-jdk8on` to `1.78.1` - Bump `bcprov-jdk18on` to `1.78.1` -- Bump `logback-core` and `logback-classic` to `1.4.13` +- Bump `logback-core` and `logback-classic` to `1.4.14` - `pgpainless-core` - Rewrote most of the codebase in Kotlin - Removed `OpenPgpMetadata` (`decryptionStream.getResult()`) in favor of `MessageMetadata` (`decryptionStream.getMetadata()`) - Removed support for generating EC keys over non-standard curve `secp256k1` + - Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) + - Do not choke on unknown signature subpackets (thanks @Jerbell) + - Prevent timing issues resulting in subkey binding signatures predating the subkey (@thanks Jerbell) + - Rename LibrePGP-related `Feature` enums: + - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` + - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` + - Properly reject signatures by non-signing primary keys + - Add `EncryptionBuilder.discardOutput()` (useful for detached signing) + - Remove support for generation of keys over non-standard `secp256k1` curve + - Add base support for padding packets + - Do not choke on LibrePGP OED packets + - Supersede `addPassphrase()`/`addDecryptionPassphrase()` methods with more clear `addMessagePassphrase()` - `pgpainless-sop`, `pgpainless-cli` - Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key @@ -21,12 +33,7 @@ SPDX-License-Identifier: CC0-1.0 - Separate signature verification operations into `SOPV` interface - Add `version --sopv` option - Throw `BadData` error when passing KEYS where CERTS are expected. -- Properly feed EOS tokens to the pushdown automaton when reaching the end of stream (thanks @iNPUTmice) -- Do not choke on unknown signature subpackets (thanks @Jerbell) -- Prevent timing issues resuting in subkey binding signatures predating the subkey (@thanks Jerbell) -- Rename LibrePGP-related `Feature` enums: - - `GNUPG_AEAD_ENCRYPTED_DATA` -> `LIBREPGP_OCB_ENCRYPTED_DATA` - - `GNUPG_VERSION_5_PUBLIC_KEY` -> `LIBREPGP_VERSION_5_PUBLIC_KEY` + - `armor`: Remove `--label` option ## 1.6.7 - SOP: Fix OOM error when detached-signing large amounts of data (fix #432) From 62c661e254ac0cdb763b9c86ccda06dc36f374bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:23:31 +0200 Subject: [PATCH 276/351] Update SECURITY.md --- SECURITY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/SECURITY.md b/SECURITY.md index 4a7785ba..a05079d3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,6 +14,7 @@ currently being supported with security updates. | Version | Supported | Note | |---------|--------------------|------------| +| 1.7.X | :white_check_mark: | | | 1.6.X | :white_check_mark: | LTS branch | | 1.5.X | :white_check_mark: | | | 1.4.X | :white_check_mark: | | From 3388d908a18a681bd059f07c0350e9de6e211a4c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:27:04 +0200 Subject: [PATCH 277/351] Update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 484234d8..5a9c1896 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0 [![Build Status](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml/badge.svg)](https://github.com/pgpainless/pgpainless/actions/workflows/gradle_push.yml) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/pgpainless/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/pgpainless?branch=main) -[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/%231/green)](https://tests.sequoia-pgp.org/) +[![Interoperability Test-Suite](https://badgen.net/badge/Sequoia%20Test%20Suite/results/green)](https://tests.sequoia-pgp.org/) [![PGP](https://img.shields.io/badge/pgp-A027%20DB2F%203E1E%20118A-blue)](https://keyoxide.org/7F9116FEA90A5983936C7CFAA027DB2F3E1E118A) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/pgpainless)](https://api.reuse.software/info/github.com/pgpainless/pgpainless) [![Documentation Status](https://readthedocs.org/projects/pgpainless/badge/?version=latest)](https://pgpainless.readthedocs.io/en/latest/?badge=latest) @@ -132,7 +132,7 @@ Still it allows you to manually specify which algorithms to use of course. .addRecipient(aliceKey) .addRecipient(bobsKey) // optionally encrypt to a passphrase - .addPassphrase(Passphrase.fromPassword("password123")) + .addMessagePassphrase(Passphrase.fromPassword("password123")) // optionally override symmetric encryption algorithm .overrideEncryptionAlgorithm(SymmetricKeyAlgorithm.AES_192), new SigningOptions() @@ -164,7 +164,7 @@ This behaviour can be modified though using the `Policy` class. DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(encryptedInputStream) .withOptions(new ConsumerOptions() - .addDecryptionKey(bobSecKeys, secretKeyProtector) + .addMessagePassphrase(bobSecKeys, secretKeyProtector) .addVerificationCert(alicePubKeys) ); @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.6.6' + implementation 'org.pgpainless:pgpainless-core:1.7.0' } ``` From 8b40062288eaa59a8a1ae3b8614695d0b802efa2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:51:16 +0200 Subject: [PATCH 278/351] PGPainless 1.7.0 --- pgpainless-sop/README.md | 4 ++-- version.gradle | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 7cdf1ef3..0673c406 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.6.6" + implementation "org.pgpainless:pgpainless-sop:1.7.0" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.6.6 + 1.7.0 ... diff --git a/version.gradle b/version.gradle index 9db85f33..33a389db 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.6.7' - isSnapshot = true + shortVersion = '1.7.0' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.4.14' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.0.0' + sopJavaVersion = '10.0.1' } } From fb71ef2193c7859f2c2dba8e0cc41c0e346b5493 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:54:26 +0200 Subject: [PATCH 279/351] PGPainless 1.7.1-SNAPSHOT --- CHANGELOG.md | 4 ++-- version.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d0a314..0f1af4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.0-SNAPSHOT +## 1.7.0 - Bump `bcpg-jdk8on` to `1.78.1` - Bump `bcprov-jdk18on` to `1.78.1` - Bump `logback-core` and `logback-classic` to `1.4.14` @@ -27,7 +27,7 @@ SPDX-License-Identifier: CC0-1.0 - Do not choke on LibrePGP OED packets - Supersede `addPassphrase()`/`addDecryptionPassphrase()` methods with more clear `addMessagePassphrase()` - `pgpainless-sop`, `pgpainless-cli` - - Bump `sop-java` to `10.0.0`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) + - Bump `sop-java` to `10.0.1`, implementing [SOP Spec Revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html) - Change API of `sop.encrypt` to return a `ReadyWithResult` to expose the session key - `decrypt --verify-with`: Fix to not throw `NoSignature` exception (exit code 3) if `VERIFICATIONS` is empty - Separate signature verification operations into `SOPV` interface diff --git a/version.gradle b/version.gradle index 33a389db..33805855 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.0' - isSnapshot = false + shortVersion = '1.7.1' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' From 52b6d5c3f7d34631fb6433dd78152df1b87870db Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:18:49 +0200 Subject: [PATCH 280/351] Fix some minor code issues --- .../src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt | 6 +++--- .../src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt | 2 +- .../src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt index f6a9fb1d..de2b2b3c 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DecryptImpl.kt @@ -92,7 +92,7 @@ class DecryptImpl : Decrypt { } override fun verifyWithCert(cert: InputStream): Decrypt = apply { - KeyReader.readPublicKeys(cert, true)?.let { consumerOptions.addVerificationCerts(it) } + KeyReader.readPublicKeys(cert, true).let { consumerOptions.addVerificationCerts(it) } } override fun withKey(key: InputStream): Decrypt = apply { @@ -107,10 +107,10 @@ class DecryptImpl : Decrypt { } override fun withPassword(password: String): Decrypt = apply { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(password)) + consumerOptions.addMessagePassphrase(Passphrase.fromPassword(password)) password.trimEnd().let { if (it != password) { - consumerOptions.addDecryptionPassphrase(Passphrase.fromPassword(it)) + consumerOptions.addMessagePassphrase(Passphrase.fromPassword(it)) } } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt index 83d60aa5..b227561e 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -136,7 +136,7 @@ class EncryptImpl : Encrypt { } override fun withPassword(password: String): Encrypt = apply { - encryptionOptions.addPassphrase(Passphrase.fromPassword(password)) + encryptionOptions.addMessagePassphrase(Passphrase.fromPassword(password)) } private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt index 82414a96..88ca8c54 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -34,7 +34,7 @@ class InlineDetachImpl : InlineDetach { private val sigOut = ByteArrayOutputStream() - override fun writeTo(messageOutputStream: OutputStream): Signatures { + override fun writeTo(outputStream: OutputStream): Signatures { var pgpIn = OpenPgpInputStream(messageInputStream) if (pgpIn.isNonOpenPgp) { throw SOPGPException.BadData("Data appears to be non-OpenPGP.") @@ -50,7 +50,7 @@ class InlineDetachImpl : InlineDetach { try { signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( - armorIn, messageOutputStream) + armorIn, outputStream) if (signatures.isEmpty) { throw SOPGPException.BadData( "Data did not contain OpenPGP signatures.") @@ -86,7 +86,7 @@ class InlineDetachImpl : InlineDetach { if (next is PGPLiteralData) { // Write out contents of Literal Data packet val literalIn = (next as PGPLiteralData).dataStream - Streams.pipeAll(literalIn, messageOutputStream) + Streams.pipeAll(literalIn, outputStream) literalIn.close() continue } From e5a0617621daf77d3b0c8a9b002757d3794969a6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:47:00 +0200 Subject: [PATCH 281/351] Fix CLI being spammed by logback by downgrading to logback-core 1.2.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 33805855..9b763f35 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.78.1' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.4.14' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '10.0.1' From ab6cde3ec6edfab8dbc67a9d9b1fc33fa1597c23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:28:50 +0200 Subject: [PATCH 282/351] Bump sop-java to 10.0.2 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 9b763f35..d32d055f 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.0.1' + sopJavaVersion = '10.0.2' } } From 60c963fca587616fbd2c0159181f61dc2770de73 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:33:47 +0200 Subject: [PATCH 283/351] Document logback spam --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index d32d055f..36bc35bc 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.78.1' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.2.13' + logbackVersion = '1.2.13' // 1.4+ cause CLI spam :/ mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '10.0.2' From fac23745b4f659cb8f337be3914fd10cc200c44d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:35:52 +0200 Subject: [PATCH 284/351] PGPainless 1.7.1 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a9c1896..3ba3b028 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.0' + implementation 'org.pgpainless:pgpainless-core:1.7.1' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 0673c406..0b79941d 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.0" + implementation "org.pgpainless:pgpainless-sop:1.7.1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.0 + 1.7.1 ... diff --git a/version.gradle b/version.gradle index 36bc35bc..69a8b0e1 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.1' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' From dabafe538f88229e2b12f19751bcbda1a42bbfef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:37:56 +0200 Subject: [PATCH 285/351] PGPainless 1.7.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 69a8b0e1..2fd08f58 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.1' - isSnapshot = false + shortVersion = '1.7.2' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' From 54569b3b02d60aa0493c70b1cb5cdb56fd754bb8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:46:22 +0200 Subject: [PATCH 286/351] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1af4ca..0b36c66b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.1 +- Bump `sop-java` to `10.0.2` +- Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam) + ## 1.7.0 - Bump `bcpg-jdk8on` to `1.78.1` From d966349032cdee9d46fc511dd62b70d8eec25a69 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 15 Oct 2024 14:45:28 +0200 Subject: [PATCH 287/351] Add test to verify proper functionality of hash algorithm policy overrides for SOP --- .../sop/VerifyLegacySignatureTest.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java new file mode 100644 index 00000000..23fd9840 --- /dev/null +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/VerifyLegacySignatureTest.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.sop; + +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.policy.Policy; +import sop.ByteArrayAndResult; +import sop.Verification; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class VerifyLegacySignatureTest { + + @Test + public void verifyLegacySignature() throws IOException { + // Key generated in 2012 using SHA1 for self sigs + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 21FA 6E60 D6C9 B7D1 0EAC 56A2 984B 91CF D303 214C\n" + + "Comment: Legacy \n" + + "\n" + + "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\n" + + "SV9g8CiU0cX7hiBYYpktcDVU3uRCR4Fkvw==\n" + + "=n8qw\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + // Sig generated in 2012 using SHA1 + String oldSig = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owEB2gEl/pANAwACAZhLkc/TAyFMAcsTYgAAAAAASGVsbG8sIFdvcmxkIYkBswQA\n" + + "AQIAJwkQmEuRz9MDIUwWIQQh+m5g1sm30Q6sVqKYS5HP0wMhTAWCUNRWwAAALxEL\n" + + "/2uhYsTLM8nUnYm2GJB6pkapX1kbQrqfAhK46IjxcPpRdl6CW4cFrG6iFegx4YLE\n" + + "fu44VKG+XGy/RTZXIEJubi9zVyOGGJM9Bwwdcp/eekO16/kJ7BsbkaO+5AG/fNeg\n" + + "bL5C8D2m6jV1seAt/+tRyM9jLkRi9odq8BsGA6ZcthAxh3MUoo1yw3QwwEcFFHg/\n" + + "gBw4ZtL8KIQN1PKDz3sSV4GXPQAiz+/uADZ2lL6mbDEK/gXAK1KevIO3U8ZU9B6l\n" + + "cOF9fJww31SCqFGDq50Lzwz7eySJB1TZ0IoehGDXoQ8JF88uTVfACkBATE0Zx7zg\n" + + "TAYIgPSjWY4TEDZ9YjdxJ0hKTMncxVfZPB+J/mYCpVADYSEhLbUJ1ntjc0s35xJD\n" + + "udLSwUWuboedVdEcaqnfgHoaaV+nKk+6F9y8NO56RK3Bfx5FmKmNZHbhfXO/qRt9\n" + + "H43UktMUD6xWxxJv7mutThOp2aizBeboa5YSJ1mxtkPW0/lyK1jr438ETHUnCeu6\n" + + "Vw==\n" + + "=TtKx\n" + + "-----END PGP MESSAGE-----"; + + SOPImpl sop = new SOPImpl(); + byte[] cert = sop.extractCert().key(KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + ByteArrayAndResult> result = sop.inlineVerify() + .cert(cert) + .data(oldSig.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult(); + + assertFalse(result.getResult().isEmpty()); + + // Adjust data signature hash policy to accept new SHA-1 sigs + PGPainless.getPolicy().setDataSignatureHashAlgorithmPolicy( + Policy.HashAlgorithmPolicy.static2022RevocationSignatureHashAlgorithmPolicy()); + + // Sig generated in 2024 using SHA1 + String newSig = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "owEB2gEl/pANAwACAZhLkc/TAyFMAcsTYgAAAAAASGVsbG8sIFdvcmxkIYkBswQA\n" + + "AQIAJwWCZw5i2AkQmEuRz9MDIUwWIQQh+m5g1sm30Q6sVqKYS5HP0wMhTAAAhVML\n" + + "+QGH+O2fEJoAY8ZxKz/mosg4it9IeSzMhBvDgZJE8Jc+VGk7EuXL0M8pfHL+Jgmv\n" + + "FMzF3chzzLS7QA4K6hbxO31/M8TNSU12geuzQiBV7Kb1hjpvIObBgEqYsX50ZV8r\n" + + "5DHcr7huABUOH6tCKmCA2OxOvr1QV8X39h856bz3WqqP9HW8kZ6H1Z6d7XWlRMtW\n" + + "mAnSevvOJbb0Z3D97obYqytSLzi2Jyv+w2R9kYzMQff2Rl6Cv4F7zsRrF9JRC0m6\n" + + "X/s+VSNuT2yG0/4F5y8vrxvNkfd8YfM8DM6irJV4yJyVuwIoZnM913XCA4F7Uo4t\n" + + "Z8ER17SY4WOYvdja/7qPcOUjX5n1dDU0W7q2muqnZXREw2JXTULiDl0MET3K4kFu\n" + + "a6FyyMGGQwFpAnZ4gDZKzw06abd95AgHx4QlkD89J7MnUBBV+AGHNAQlCPPEVPQq\n" + + "dWTInYndt4GKCUxVkJeHD6ZPLdxEEvICmEap4FQzhqM8U7weoEsSinoVoc4JmSY9\n" + + "dQ==\n" + + "=XrzP\n" + + "-----END PGP MESSAGE-----"; + result = sop.inlineVerify() + .cert(cert) + .data(newSig.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult(); + + assertFalse(result.getResult().isEmpty()); + } +} From f1610f64256828d7ba9190d73b20bd18d07b1ba1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Oct 2024 17:41:18 +0200 Subject: [PATCH 288/351] Fix returning proper value for KeyRingInfo.lastModified While porting to kotlin the code was accidentally changed to return the key ring creation time instead of the latest self-sig creation time --- .../src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index f4305225..484ae18f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -405,7 +405,7 @@ class KeyRingInfo( .plus(signatures.userIdRevocations.values) .plus(signatures.subkeyBindings.values) .plus(signatures.subkeyRevocations.values) - .maxByOrNull { creationDate } + .maxByOrNull { it.creationTime } /** * Return the creation time of the latest added subkey. * From de4a11352853b6e18146b10a87a928b2984a0fab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Oct 2024 19:06:56 +0200 Subject: [PATCH 289/351] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b36c66b..2bb0d18f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.2-SNAPSHOT +- Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) + ## 1.7.1 - Bump `sop-java` to `10.0.2` - Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam) From 34e9748d0fbc93770aa55a17fc8e7cfb35031c12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:39:56 +0100 Subject: [PATCH 290/351] Bump sop-java to 10.0.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 2fd08f58..41052ed8 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.2.13' // 1.4+ cause CLI spam :/ mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.0.2' + sopJavaVersion = '10.0.3' } } From f6c4ddd288a4d623e66805ce4bd2ba195492281b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:44:46 +0100 Subject: [PATCH 291/351] Update changelog --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb0d18f..2816afb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,14 @@ SPDX-License-Identifier: CC0-1.0 ## 1.7.2-SNAPSHOT - Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) +- Bump `sop-java` to `10.0.3` + - allow multiple arguments `--with-key-password` in `revoke-key` command + - Properly pass `--old-key-password` and `--new-key-password` options as indirect arguments in `change-key-password` command ## 1.7.1 - Bump `sop-java` to `10.0.2` - Downgrade `logback-core` and `logback-classic` to `1.2.13` (fix CLI spam) - ## 1.7.0 - Bump `bcpg-jdk8on` to `1.78.1` - Bump `bcprov-jdk18on` to `1.78.1` @@ -42,6 +44,11 @@ SPDX-License-Identifier: CC0-1.0 - Throw `BadData` error when passing KEYS where CERTS are expected. - `armor`: Remove `--label` option +## 1.6.8 +- Bump `sop-java` to `7.0.2` +- SOP `change-key-password`: Fix reading password from indirect parameter instead of erroneously passing filename (fixes #453) +- SOP `revoke-key`: Allow for multiple `--with-key-password` options + ## 1.6.7 - SOP: Fix OOM error when detached-signing large amounts of data (fix #432) - Move `CachingBcPublicKeyDataDecryptorFactory` from `org.bouncycastle` packet to `org.pgpainless.decryption_verification` to avoid package split (partially addresses #428) From df5297a66104a642684406e44e1ec2dfa161b106 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:52:45 +0100 Subject: [PATCH 292/351] PGPainless 1.7.2 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2816afb6..b410a6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.2-SNAPSHOT +## 1.7.2 - Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) - Bump `sop-java` to `10.0.3` - allow multiple arguments `--with-key-password` in `revoke-key` command diff --git a/README.md b/README.md index 3ba3b028..71a05ba2 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.1' + implementation 'org.pgpainless:pgpainless-core:1.7.2' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 0b79941d..8696d287 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.1" + implementation "org.pgpainless:pgpainless-sop:1.7.2" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.1 + 1.7.2 ... diff --git a/version.gradle b/version.gradle index 41052ed8..d13aac0a 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.2' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' From e46cbae6b8ef41de15e8df986b0955608a0c2ca7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:54:34 +0100 Subject: [PATCH 293/351] PGPainless 1.7.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index d13aac0a..eb32a54b 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.2' - isSnapshot = false + shortVersion = '1.7.3' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.78.1' From 89790a0a94391dec3bf392c8d3124f4903106ac8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 11 Nov 2024 11:52:56 +0100 Subject: [PATCH 294/351] Fix decryption example in README Fixes #456 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71a05ba2..41d3af6b 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ This behaviour can be modified though using the `Policy` class. DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(encryptedInputStream) .withOptions(new ConsumerOptions() - .addMessagePassphrase(bobSecKeys, secretKeyProtector) + .addDecryptionKey(bobSecKeys, secretKeyProtector) .addVerificationCert(alicePubKeys) ); From b99822f405eaaf9283ea5d5a959fa08d6d318306 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 19 Nov 2024 13:58:54 +0100 Subject: [PATCH 295/351] Ignore certificate signatures of unknown type --- .../decryption_verification/OpenPgpInputStream.java | 9 +++++---- .../exception/SignatureValidationException.java | 9 ++++++++- .../pgpainless/key/util/OpenPgpKeyAttributeUtil.java | 10 +++++++--- .../kotlin/org/pgpainless/algorithm/SignatureType.kt | 3 ++- .../bouncycastle/extensions/PGPSignatureExtensions.kt | 5 +++-- .../signature/consumer/SignatureValidator.kt | 6 ++++-- .../pgpainless/signature/consumer/SignatureVerifier.kt | 5 +++-- .../org/pgpainless/algorithm/SignatureTypeTest.java | 3 +-- 8 files changed, 33 insertions(+), 17 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index ff020a7b..3522f509 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -32,6 +32,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.NoSuchElementException; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.openpgp.PGPCompressedData; @@ -208,8 +209,8 @@ public class OpenPgpInputStream extends BufferedInputStream { } try { - SignatureType.valueOf(sigType); - } catch (IllegalArgumentException e) { + SignatureType.requireFromCode(sigType); + } catch (NoSuchElementException e) { return; } @@ -236,8 +237,8 @@ public class OpenPgpInputStream extends BufferedInputStream { if (opsVersion == 3) { int opsSigType = bcpgIn.read(); try { - SignatureType.valueOf(opsSigType); - } catch (IllegalArgumentException e) { + SignatureType.requireFromCode(opsSigType); + } catch (NoSuchElementException e) { return; } int opsHashAlg = bcpgIn.read(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java index b5b8941d..2141ec5c 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/SignatureValidationException.java @@ -28,7 +28,14 @@ public class SignatureValidationException extends PGPException { StringBuilder sb = new StringBuilder(); sb.append(rejections.size()).append(" rejected signatures:\n"); for (PGPSignature signature : rejections.keySet()) { - sb.append(SignatureType.valueOf(signature.getSignatureType())).append(' ') + String typeString; + SignatureType type = SignatureType.fromCode(signature.getSignatureType()); + if (type == null) { + typeString = "0x" + Long.toHexString(signature.getSignatureType()); + } else { + typeString = type.toString(); + } + sb.append(typeString).append(' ') .append(signature.getCreationTime()).append(": ") .append(rejections.get(signature).getMessage()).append('\n'); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java index e97a2d7a..f7a78404 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/OpenPgpKeyAttributeUtil.java @@ -34,7 +34,11 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); + SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); + if (signatureType == null) { + // unknown signature type + continue; + } if (signatureType == SignatureType.POSITIVE_CERTIFICATION || signatureType == SignatureType.GENERIC_CERTIFICATION) { int[] hashAlgos = signature.getHashedSubPackets().getPreferredHashAlgorithms(); @@ -71,8 +75,8 @@ public final class OpenPgpKeyAttributeUtil { continue; } - SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType()); - if (signatureType != SignatureType.POSITIVE_CERTIFICATION + SignatureType signatureType = SignatureType.fromCode(signature.getSignatureType()); + if (signatureType == null || signatureType != SignatureType.POSITIVE_CERTIFICATION && signatureType != SignatureType.GENERIC_CERTIFICATION) { continue; } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt index e6b2299f..be6917df 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt @@ -170,7 +170,8 @@ enum class SignatureType(val code: Int) { @JvmStatic fun isRevocationSignature(signatureType: Int): Boolean { - return isRevocationSignature(valueOf(signatureType)) + val sigType = fromCode(signatureType) + return sigType?.let { isRevocationSignature(it) } ?: false } @JvmStatic diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt index df40c461..1393883c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/PGPSignatureExtensions.kt @@ -77,7 +77,8 @@ fun PGPSignature.wasIssuedBy(key: PGPPublicKey): Boolean = wasIssuedBy(OpenPgpFi /** Return true, if this signature is a hard revocation. */ val PGPSignature.isHardRevocation get() = - when (SignatureType.requireFromCode(signatureType)) { + when (SignatureType.fromCode(signatureType)) { + null -> false SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION, SignatureType.CERTIFICATION_REVOCATION -> { @@ -104,4 +105,4 @@ val PGPSignature.signatureHashAlgorithm: HashAlgorithm get() = HashAlgorithm.requireFromId(hashAlgorithm) fun PGPSignature.isOfType(type: SignatureType): Boolean = - SignatureType.requireFromCode(signatureType) == type + SignatureType.fromCode(signatureType) == type diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt index e16ef158..951beb0f 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -235,7 +235,8 @@ abstract class SignatureValidator { signature: PGPSignature, policy: Policy ): Policy.HashAlgorithmPolicy { - return when (SignatureType.requireFromCode(signature.signatureType)) { + return when (SignatureType.fromCode(signature.signatureType)) { + null -> policy.certificationSignatureHashAlgorithmPolicy SignatureType.CERTIFICATION_REVOCATION, SignatureType.KEY_REVOCATION, SignatureType.SUBKEY_REVOCATION -> policy.revocationSignatureHashAlgorithmPolicy @@ -598,7 +599,8 @@ abstract class SignatureValidator { if (signatureType.none { signature.isOfType(it) }) { throw SignatureValidationException( "Signature is of type" + - " ${SignatureType.requireFromCode(signature.signatureType)}, " + + " ${SignatureType.fromCode(signature.signatureType) ?: + ("0x" + signature.signatureType.toString(16))}, " + "while only ${signatureType.contentToString()} are allowed here.") } } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt index 77793c90..d51b2379 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureVerifier.kt @@ -59,12 +59,13 @@ class SignatureVerifier { policy: Policy, referenceTime: Date ): Boolean { - val type = SignatureType.requireFromCode(signature.signatureType) + val type = SignatureType.fromCode(signature.signatureType) return when (type) { SignatureType.GENERIC_CERTIFICATION, SignatureType.NO_CERTIFICATION, SignatureType.CASUAL_CERTIFICATION, - SignatureType.POSITIVE_CERTIFICATION -> + SignatureType.POSITIVE_CERTIFICATION, + null -> verifyUserIdCertification( userId, signature, signingKey, keyWithUserId, policy, referenceTime) SignatureType.CERTIFICATION_REVOCATION -> diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java index 1bf78776..7f2bc2a6 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/SignatureTypeTest.java @@ -5,7 +5,6 @@ package org.pgpainless.algorithm; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; @@ -31,6 +30,6 @@ public class SignatureTypeTest { assertFalse(SignatureType.isRevocationSignature(SignatureType.STANDALONE.getCode())); assertFalse(SignatureType.isRevocationSignature(SignatureType.TIMESTAMP.getCode())); - assertThrows(IllegalArgumentException.class, () -> SignatureType.isRevocationSignature(-3)); + assertFalse(SignatureType.isRevocationSignature(-3)); } } From fdf49cfafbea21f1d6ae30c2de5f4b152e213e1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 19 Nov 2024 13:59:41 +0100 Subject: [PATCH 296/351] Improve error message when no acceptable certificate signature is found Relates to #457 --- .../src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index 11fe1643..f65bb7bc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -71,7 +71,9 @@ abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: S return info.latestDirectKeySelfSignature } - return info.getCurrentSubkeyBindingSignature(key.subkeyId)!! + return info.getCurrentSubkeyBindingSignature(key.subkeyId) + ?: throw NoSuchElementException( + "Key does not carry acceptable self-signature signature.") } } From 391549a7d6776a1f10b01b69112e5b1dbb410103 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 10:27:06 +0100 Subject: [PATCH 297/351] Revert "Ensure proper compatibility with keys with missing direct-key or certification self-sigs" This reverts commit 620c1fc96a55c04d9607632b6967f66105681e55. --- .../org/pgpainless/key/info/KeyAccessor.kt | 4 - .../org/pgpainless/key/info/KeyRingInfo.kt | 3 - .../pgpainless/key/KeyWithoutSelfSigsTest.kt | 111 ------------------ 3 files changed, 118 deletions(-) delete mode 100644 pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index f65bb7bc..935c4f48 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -67,10 +67,6 @@ abstract class KeyAccessor(protected val info: KeyRingInfo, protected val key: S info.getLatestUserIdCertification(userId).let { if (it != null) return it } } - if (info.latestDirectKeySelfSignature != null) { - return info.latestDirectKeySelfSignature - } - return info.getCurrentSubkeyBindingSignature(key.subkeyId) ?: throw NoSuchElementException( "Key does not carry acceptable self-signature signature.") diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt index 484ae18f..ce4fbe56 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyRingInfo.kt @@ -172,11 +172,8 @@ class KeyRingInfo( primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { - /* throw NoSuchElementException( "No direct-key signature and no user-id signature found.") - */ - return null } if (directKeyExpirationDate != null && userIdExpirationDate == null) { return directKeyExpirationDate diff --git a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt b/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt deleted file mode 100644 index c7879483..00000000 --- a/pgpainless-core/src/test/kotlin/org/pgpainless/key/KeyWithoutSelfSigsTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key - -import java.io.ByteArrayOutputStream -import org.bouncycastle.openpgp.PGPPublicKey -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.util.io.Streams -import org.junit.jupiter.api.Test -import org.pgpainless.PGPainless -import org.pgpainless.algorithm.KeyFlag -import org.pgpainless.decryption_verification.ConsumerOptions -import org.pgpainless.encryption_signing.EncryptionOptions -import org.pgpainless.encryption_signing.ProducerOptions -import org.pgpainless.encryption_signing.SigningOptions -import org.pgpainless.key.generation.KeySpec -import org.pgpainless.key.generation.type.KeyType -import org.pgpainless.key.generation.type.eddsa_legacy.EdDSALegacyCurve -import org.pgpainless.key.generation.type.xdh_legacy.XDHLegacySpec -import org.pgpainless.key.protection.SecretKeyRingProtector - -class KeyWithoutSelfSigsTest { - - @Test - fun signAndVerify() { - val key = PGPainless.readKeyRing().secretKeyRing(KEY) - val cert = PGPainless.extractCertificate(key!!) - - val ciphertextOut = ByteArrayOutputStream() - val encryptionStream = - PGPainless.encryptAndOrSign() - .onOutputStream(ciphertextOut) - .withOptions( - ProducerOptions.signAndEncrypt( - EncryptionOptions.encryptCommunications().addRecipient(cert), - SigningOptions.get() - .addSignature(SecretKeyRingProtector.unprotectedKeys(), key))) - encryptionStream.write("Hello, World!\n".toByteArray()) - encryptionStream.close() - - val plaintextOut = ByteArrayOutputStream() - val decryptionStream = - PGPainless.decryptAndOrVerify() - .onInputStream(ciphertextOut.toByteArray().inputStream()) - .withOptions( - ConsumerOptions.get() - .addVerificationCert(cert) - .addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys())) - Streams.pipeAll(decryptionStream, plaintextOut) - decryptionStream.close() - } - - fun generateKey() { - val key = - PGPainless.buildKeyRing() - .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519))) - .addSubkey( - KeySpec.getBuilder( - KeyType.EDDSA_LEGACY(EdDSALegacyCurve._Ed25519), KeyFlag.SIGN_DATA)) - .addSubkey( - KeySpec.getBuilder( - KeyType.XDH_LEGACY(XDHLegacySpec._X25519), - KeyFlag.ENCRYPT_STORAGE, - KeyFlag.ENCRYPT_COMMS)) - .build() - .let { - var cert = PGPainless.extractCertificate(it) - cert = - PGPPublicKeyRing( - buildList { - val iterator = cert.publicKeys - val primaryKey = iterator.next() - add( - PGPPublicKey.removeCertification( - primaryKey, primaryKey.signatures.next())) - while (iterator.hasNext()) { - add(iterator.next()) - } - }) - PGPSecretKeyRing.replacePublicKeys(it, cert) - } - println(PGPainless.asciiArmor(key)) - } - - companion object { - - const val KEY = - "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Version: PGPainless\n" + - "Comment: DA3E CC77 1CD6 46F0 C6C4 4FDA 86A3 7B22 7802 2FC7\n" + - "\n" + - "lFgEZUuWuhYJKwYBBAHaRw8BAQdAuXfarON/+UG1qwhVy4/VCYuEb9iLFLb8KGQt\n" + - "KfX4Se0AAQDgqGHsb2M43F+6wK5Hla+oZzFkTUsBx8HMpRx2yeQT6hFAnFgEZUuW\n" + - "uhYJKwYBBAHaRw8BAQdAx0OHISLtekltdUVGGrG/Gs3asc/jG/nqCkBEZ5uyELwA\n" + - "AP0faf8bprP3fj248/NacfynKEVnjzc1gocfhGiWrnVgAxC1iNUEGBYKAH0FAmVL\n" + - "lroCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJlS5a6AAoJED9gFx9r\n" + - "B25syqoA/0JR3Zcs6fHQ0jW7+u6330SD5h8WvG78IKsE6AfChBLXAP4hlXGidztq\n" + - "5sOHEQvXD2KPCHEJ6MuQ+rbNSSf0fQhgDwAKCRCGo3sieAIvxzmIAP9+9vRoevUM\n" + - "luQhZzQ7DgYqTCyNkeq2cpVgOfa0lyVDgwEApwrd5DlU3GorGHAQHFS6jhw1IOoG\n" + - "FGQ3zpWaOXd7XwKcXQRlS5a6EgorBgEEAZdVAQUBAQdAZIY7ISyNzp0oMoK0dgb8\n" + - "dX6t/i4Uh+l0jnxM0Z1dEB8DAQgHAAD/fhL5dzdJQ7hFhr78AmDEZKFE4txZFPvd\n" + - "ZVFvIWTthFgQ5Ih1BBgWCgAdBQJlS5a6Ap4BApsMBRYCAwEABAsJCAcFFQoJCAsA\n" + - "CgkQhqN7IngCL8cIGgEAzydjTfKvdrTvzXXu97j8TAoOxk89QnLqsM6BU0VsVmkA\n" + - "/1IzH+PXgPPW9ff+elxTi2NWmK+P033P6i5b5Jdf41YD\n" + - "=GBVS\n" + - "-----END PGP PRIVATE KEY BLOCK-----" - } -} From 3e96af54508863e2fb9d3095fa6e5febce9b9fe9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 11:14:37 +0100 Subject: [PATCH 298/351] Remove dependency on deprecated com.ginsberg:junit5-system-exit library --- pgpainless-cli/build.gradle | 2 - .../java/org/pgpainless/cli/ExitCodeTest.java | 33 +++--- .../RoundTripEncryptDecryptCmdTest.java | 2 - .../RoundTripInlineSignVerifyCmdTest.java | 101 ++++-------------- .../misc/SignUsingPublicKeyBehaviorTest.java | 73 +++---------- 5 files changed, 56 insertions(+), 155 deletions(-) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 3d9a6a09..006fae7c 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -32,8 +32,6 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation 'com.ginsberg:junit5-system-exit:1.1.2' // implementation "ch.qos.logback:logback-core:1.2.6" // We want logback logging in tests and in the app diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java index 07c9bf68..b1ca143a 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/ExitCodeTest.java @@ -4,28 +4,35 @@ package org.pgpainless.cli; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.Test; +import org.pgpainless.cli.commands.CLITest; +import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; -public class ExitCodeTest { +import java.io.IOException; - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) - public void testUnknownCommand_69() { - PGPainlessCLI.main(new String[] {"generate-kex"}); +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ExitCodeTest extends CLITest { + + public ExitCodeTest() { + super(LoggerFactory.getLogger(ExitCodeTest.class)); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void testCommandWithUnknownOption_37() { - PGPainlessCLI.main(new String[] {"generate-key", "-k", "\"k is unknown\""}); + public void testUnknownCommand_69() throws IOException { + assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, + executeCommand("unsupported-subcommand")); } @Test - @FailOnSystemExit - public void successfulExecutionDoesNotTerminateJVM() { - PGPainlessCLI.main(new String[] {"version"}); + public void testCommandWithUnknownOption_37() throws IOException { + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, + executeCommand("generate-key", "-k", "\"k is unknown\"")); + } + + @Test + public void successfulExecutionDoesNotTerminateJVM() throws IOException { + assertSuccess(executeCommand("version")); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java index 9969298a..f8d56bc3 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripEncryptDecryptCmdTest.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing; @@ -83,7 +82,6 @@ public class RoundTripEncryptDecryptCmdTest extends CLITest { "-----END PGP PUBLIC KEY BLOCK-----"; @Test - @FailOnSystemExit public void encryptAndDecryptAMessage() throws IOException { // Juliets key and cert File julietKeyFile = pipeStdoutToFile("juliet.key"); diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java index 82fda430..cf350e66 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignVerifyCmdTest.java @@ -4,104 +4,49 @@ package org.pgpainless.cli.commands; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import com.ginsberg.junit.exit.FailOnSystemExit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; +import org.slf4j.LoggerFactory; -public class RoundTripInlineSignVerifyCmdTest { - private static File tempDir; - private static PrintStream originalSout; +public class RoundTripInlineSignVerifyCmdTest extends CLITest { - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + public RoundTripInlineSignVerifyCmdTest() { + super(LoggerFactory.getLogger(RoundTripInlineSignVerifyCmdTest.class)); } @Test - @FailOnSystemExit public void encryptAndDecryptAMessage() throws IOException { - originalSout = System.out; - File sigmundKeyFile = new File(tempDir, "sigmund.key"); - assertTrue(sigmundKeyFile.createNewFile()); - - File sigmundCertFile = new File(tempDir, "sigmund.cert"); - assertTrue(sigmundCertFile.createNewFile()); - - File msgFile = new File(tempDir, "signed.asc"); - assertTrue(msgFile.createNewFile()); - - File passwordFile = new File(tempDir, "password"); - assertTrue(passwordFile.createNewFile()); - // write password file - FileOutputStream passwordOut = new FileOutputStream(passwordFile); - passwordOut.write("sw0rdf1sh".getBytes(StandardCharsets.UTF_8)); - passwordOut.close(); + File password = writeFile("password", "sw0rdf1sh"); // generate key - OutputStream sigmundKeyOut = new FileOutputStream(sigmundKeyFile); - System.setOut(new PrintStream(sigmundKeyOut)); - PGPainlessCLI.execute("generate-key", - "--with-key-password=" + passwordFile.getAbsolutePath(), - "Sigmund Freud "); - sigmundKeyOut.close(); + File sigmundKey = pipeStdoutToFile("sigmund.key"); + assertSuccess(executeCommand("generate-key", "--with-key-password=" + password.getAbsolutePath(), + "Sigmund Freud ")); // extract cert - FileInputStream sigmundKeyIn = new FileInputStream(sigmundKeyFile); - System.setIn(sigmundKeyIn); - OutputStream sigmundCertOut = new FileOutputStream(sigmundCertFile); - System.setOut(new PrintStream(sigmundCertOut)); - PGPainlessCLI.execute("extract-cert"); - sigmundKeyIn.close(); - sigmundCertOut.close(); + File sigmundCert = pipeStdoutToFile("sigmund.cert"); + pipeFileToStdin(sigmundKey); + assertSuccess(executeCommand("extract-cert")); // sign message - String msg = "Hello World!\n"; - ByteArrayInputStream msgIn = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8)); - System.setIn(msgIn); - OutputStream msgAscOut = new FileOutputStream(msgFile); - System.setOut(new PrintStream(msgAscOut)); - PGPainlessCLI.execute("inline-sign", - "--with-key-password=" + passwordFile.getAbsolutePath(), - sigmundKeyFile.getAbsolutePath()); - msgAscOut.close(); + pipeBytesToStdin("Hello, World!\n".getBytes(StandardCharsets.UTF_8)); + File signedMsg = pipeStdoutToFile("signed.asc"); + assertSuccess(executeCommand("inline-sign", "--with-key-password=" + password.getAbsolutePath(), + sigmundKey.getAbsolutePath())); - File verifyFile = new File(tempDir, "verify.txt"); + // verify message + File verifyFile = nonExistentFile("verify.txt"); + pipeFileToStdin(signedMsg); + assertSuccess(executeCommand("inline-verify", "--verifications-out", verifyFile.getAbsolutePath(), + sigmundCert.getAbsolutePath())); - FileInputStream msgAscIn = new FileInputStream(msgFile); - System.setIn(msgAscIn); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintStream pOut = new PrintStream(out); - System.setOut(pOut); - PGPainlessCLI.execute("inline-verify", - "--verifications-out", verifyFile.getAbsolutePath(), - sigmundCertFile.getAbsolutePath()); - msgAscIn.close(); - - assertEquals(msg, out.toString()); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + String verifications = readStringFromFile(verifyFile); + assertFalse(verifications.trim().isEmpty()); } } diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java index affe621e..d6065b82 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/misc/SignUsingPublicKeyBehaviorTest.java @@ -4,28 +4,18 @@ package org.pgpainless.cli.misc; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.bouncycastle.util.io.Streams; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.pgpainless.cli.PGPainlessCLI; -import org.pgpainless.cli.TestUtils; +import org.pgpainless.cli.commands.CLITest; +import org.slf4j.LoggerFactory; import sop.exception.SOPGPException; -public class SignUsingPublicKeyBehaviorTest { +public class SignUsingPublicKeyBehaviorTest extends CLITest { public static final String KEY_THAT_IS_A_CERT = "" + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + @@ -89,61 +79,24 @@ public class SignUsingPublicKeyBehaviorTest { "=oJQ2\n" + "-----END PGP PUBLIC KEY BLOCK-----"; - - private static File tempDir; - private static PrintStream originalSout; - - @BeforeAll - public static void prepare() throws IOException { - tempDir = TestUtils.createTempDirectory(); + public SignUsingPublicKeyBehaviorTest() { + super(LoggerFactory.getLogger(SignUsingPublicKeyBehaviorTest.class)); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) public void testSignatureCreationAndVerification() throws IOException { - originalSout = System.out; - InputStream originalIn = System.in; - // Write alice key to disc - File aliceKeyFile = new File(tempDir, "alice.key"); - assertTrue(aliceKeyFile.createNewFile()); - OutputStream aliceKeyOut = new FileOutputStream(aliceKeyFile); - Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceKeyOut); - aliceKeyOut.close(); - - // Write alice pub key to disc - File aliceCertFile = new File(tempDir, "alice.pub"); - assertTrue(aliceCertFile.createNewFile()); - OutputStream aliceCertOut = new FileOutputStream(aliceCertFile); - Streams.pipeAll(new ByteArrayInputStream(KEY_THAT_IS_A_CERT.getBytes(StandardCharsets.UTF_8)), aliceCertOut); - aliceCertOut.close(); + File aliceKeyFile = writeFile("alice.key", KEY_THAT_IS_A_CERT); // Write test data to disc - String data = "If privacy is outlawed, only outlaws will have privacy.\n"; - - File dataFile = new File(tempDir, "data"); - assertTrue(dataFile.createNewFile()); - FileOutputStream dataOut = new FileOutputStream(dataFile); - Streams.pipeAll(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), dataOut); - dataOut.close(); + File dataFile = writeFile("data", "If privacy is outlawed, only outlaws will have privacy.\n"); // Sign test data - FileInputStream dataIn = new FileInputStream(dataFile); - System.setIn(dataIn); - File sigFile = new File(tempDir, "sig.asc"); - assertTrue(sigFile.createNewFile()); - FileOutputStream sigOut = new FileOutputStream(sigFile); - System.setOut(new PrintStream(sigOut)); - PGPainlessCLI.main(new String[] {"sign", "--armor", aliceKeyFile.getAbsolutePath()}); + File sigFile = pipeStdoutToFile("sig.asc"); + pipeFileToStdin(dataFile); + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, + executeCommand("sign", "--armor", aliceKeyFile.getAbsolutePath())); - System.setIn(originalIn); - } - - @AfterAll - public static void after() { - System.setOut(originalSout); - // CHECKSTYLE:OFF - System.out.println(tempDir.getAbsolutePath()); - // CHECKSTYLE:ON + assertTrue(readStringFromFile(sigFile).trim().isEmpty()); } } From a43ae437221ef6dd81aeb9dc058a4918019c169d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 12:43:05 +0100 Subject: [PATCH 299/351] Bump logback to 1.5.13 I hope I mitigated logback spam by modifying the logback.xml file and by setting 'slf4j.internal.verbosity' to 'WARN'. See https://github.com/pgpainless/pgpainless/issues/426 for reference --- .../org/pgpainless/cli/PGPainlessCLI.java | 4 ++++ pgpainless-cli/src/main/resources/logback.xml | 19 +------------------ version.gradle | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java index 938bf1aa..7625dd17 100644 --- a/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java +++ b/pgpainless-cli/src/main/java/org/pgpainless/cli/PGPainlessCLI.java @@ -14,6 +14,10 @@ import sop.cli.picocli.SopCLI; public class PGPainlessCLI { static { + // Prevent slf4j initialization logging + // https://github.com/qos-ch/slf4j/issues/422#issuecomment-2277280185 + System.setProperty("slf4j.internal.verbosity", "WARN"); + SopCLI.EXECUTABLE_NAME = "pgpainless-cli"; SopCLI.setSopInstance(new SOPImpl()); } diff --git a/pgpainless-cli/src/main/resources/logback.xml b/pgpainless-cli/src/main/resources/logback.xml index 559589ef..8451d6a4 100644 --- a/pgpainless-cli/src/main/resources/logback.xml +++ b/pgpainless-cli/src/main/resources/logback.xml @@ -5,22 +5,5 @@ SPDX-License-Identifier: Apache-2.0 --> - - System.err - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - System.out - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - + \ No newline at end of file diff --git a/version.gradle b/version.gradle index eb32a54b..07c66ec5 100644 --- a/version.gradle +++ b/version.gradle @@ -11,7 +11,7 @@ allprojects { bouncyCastleVersion = '1.78.1' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' - logbackVersion = '1.2.13' // 1.4+ cause CLI spam :/ + logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '10.0.3' From 501838631815cbeea7a668529d6dee52cb038af8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 13:24:05 +0100 Subject: [PATCH 300/351] Extract pgpainless-sop-version property via resource filtering --- pgpainless-cli/build.gradle | 19 ------------------- pgpainless-sop/build.gradle | 8 +++++++- .../kotlin/org/pgpainless/sop/VersionImpl.kt | 4 ++-- .../main/resources/pgpainless-sop.properties | 1 + 4 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 pgpainless-sop/src/main/resources/pgpainless-sop.properties diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 006fae7c..d6550139 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -6,25 +6,6 @@ plugins { id 'application' id "com.github.johnrengelman.shadow" version "6.1.0" } -def generatedVersionDir = "${buildDir}/generated-version" - -sourceSets { - main { - output.dir(generatedVersionDir, builtBy: 'generateVersionProperties') - } -} - -task generateVersionProperties { - doLast { - def propertiesFile = file "$generatedVersionDir/version.properties" - propertiesFile.parentFile.mkdirs() - propertiesFile.createNewFile() - // Instead of using a Properties object here, we directly write to the file - // since Properties adds a timestamp, ruining reproducibility - propertiesFile.write("version="+rootProject.version.toString()) - } -} -processResources.dependsOn generateVersionProperties dependencies { diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index 26beec67..aa0f5993 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 - +import org.apache.tools.ant.filters.* plugins { id 'java-library' } @@ -30,6 +30,12 @@ dependencies { implementation "com.google.code.findbugs:jsr305:3.0.2" } +processResources { + filter ReplaceTokens, tokens: [ + "project.version": project.version.toString() + ] +} + test { useJUnitPlatform() environment("test.implementation", "sop.testsuite.pgpainless.PGPainlessSopInstanceFactory") diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt index 7ebdade9..6b7f5968 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -49,11 +49,11 @@ https://www.bouncycastle.org/java.html""" // See https://stackoverflow.com/a/50119235 return try { val resourceIn: InputStream = - javaClass.getResourceAsStream("/version.properties") + javaClass.getResourceAsStream("/pgpainless-sop.properties") ?: throw IOException("File version.properties not found.") val properties = Properties().apply { load(resourceIn) } - properties.getProperty("version") + properties.getProperty("pgpainless-sop-version") } catch (e: IOException) { "DEVELOPMENT" } diff --git a/pgpainless-sop/src/main/resources/pgpainless-sop.properties b/pgpainless-sop/src/main/resources/pgpainless-sop.properties new file mode 100644 index 00000000..d71e996b --- /dev/null +++ b/pgpainless-sop/src/main/resources/pgpainless-sop.properties @@ -0,0 +1 @@ +pgpainless-sop-version=@project.version@ \ No newline at end of file From 4dbc633d7d1c7ab7d5b4f375b191a63af0f7a124 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 13:41:37 +0100 Subject: [PATCH 301/351] version --extended: Include sop-java version Fixes #454 --- .../kotlin/org/pgpainless/sop/VersionImpl.kt | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt index 6b7f5968..c6761a39 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -8,6 +8,7 @@ import java.io.IOException import java.io.InputStream import java.util.* import org.bouncycastle.jce.provider.BouncyCastleProvider +import sop.SOP import sop.operation.Version /** Implementation of the `version` operation using PGPainless. */ @@ -32,7 +33,7 @@ https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersi Based on pgpainless-core ${getVersion()} https://pgpainless.org - +${formatSopJavaVersion()} Using $bcVersion https://www.bouncycastle.org/java.html""" } @@ -49,8 +50,8 @@ https://www.bouncycastle.org/java.html""" // See https://stackoverflow.com/a/50119235 return try { val resourceIn: InputStream = - javaClass.getResourceAsStream("/pgpainless-sop.properties") - ?: throw IOException("File version.properties not found.") + SOP::class.java.getResourceAsStream("/pgpainless-sop.properties") + ?: throw IOException("File pgpainless-sop.properties not found.") val properties = Properties().apply { load(resourceIn) } properties.getProperty("pgpainless-sop-version") @@ -59,5 +60,29 @@ https://www.bouncycastle.org/java.html""" } } + private fun formatSopJavaVersion(): String { + return getSopJavaVersion()?.let { + """ + + sop-java $it + + """ + .trimIndent() + } + ?: "" + } + + private fun getSopJavaVersion(): String? { + return try { + val resourceIn: InputStream = + javaClass.getResourceAsStream("/sop-java-version.properties") + ?: throw IOException("File sop-java-version.properties not found.") + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("sop-java-version") + } catch (e: IOException) { + null + } + } + override fun isSopSpecImplementationIncomplete(): Boolean = false } From 02330a5aa1aa0353b8a199006c0026fd79dabff5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 14:32:42 +0100 Subject: [PATCH 302/351] Reproducible file order --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 04b8a0af..29e054c2 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ allprojects { // without this we would generate an empty pgpainless.jar for the project root // https://stackoverflow.com/a/25445035 jar { + reproducibleFileOrder = true onlyIf { !sourceSets.main.allSource.files.isEmpty() } } From 1db59acf0ded75dbf36d520527f0cc33ac2d58b2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 10 Jan 2025 10:40:56 +0100 Subject: [PATCH 303/351] Add reuse license header to properties file --- pgpainless-sop/src/main/resources/pgpainless-sop.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pgpainless-sop/src/main/resources/pgpainless-sop.properties b/pgpainless-sop/src/main/resources/pgpainless-sop.properties index d71e996b..d0f47796 100644 --- a/pgpainless-sop/src/main/resources/pgpainless-sop.properties +++ b/pgpainless-sop/src/main/resources/pgpainless-sop.properties @@ -1 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 pgpainless-sop-version=@project.version@ \ No newline at end of file From 3af506fedb3117f06da4f1d155dacdd7cdf2cb30 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 10 Jan 2025 10:41:11 +0100 Subject: [PATCH 304/351] Migrate reuse from dep5 to REUSE.toml --- .reuse/dep5 | 86 ------------------------------------------ REUSE.toml | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 86 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index 7703aa47..00000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,86 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: PGPainless -Upstream-Contact: Paul Schaub -Source: https://pgpainless.org - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -# GitBlameIgnore -Files: .git-blame-ignore-revs -Copyright: 2023 Paul Schaub -License: CC0-1.0 - -# Documentation -Files: docs/* -Copyright: 2022 Paul Schaub -License: CC-BY-3.0 - -Files: .readthedocs.yaml -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -# Gradle build tool -Files: gradle* -Copyright: 2015 the original author or authors. -License: Apache-2.0 - -# Editorconfig -Files: .editorconfig -Copyright: Facebook -License: Apache-2.0 - -# PGPainless Logo -Files: assets/repository-open-graph.png -Copyright: 2021 Paul Schaub -License: CC-BY-3.0 - -Files: assets/pgpainless.svg -Copyright: 2021 Paul Schaub -License: CC-BY-3.0 - -Files: assets/logo.png -Copyright: 2022 Paul Schaub -License: CC-BY-3.0 - -Files: assets/test_vectors/* -Copyright: 2018 Paul Schaub -License: CC0-1.0 - -Files: pgpainless-core/src/test/resources/* -Copyright: 2020 Paul Schaub -License: CC0-1.0 - -Files: audit/* -Copyright: 2021 Paul Schaub -License: CC0-1.0 - -# GH Pages -Files: CNAME -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -Files: _config.yml -Copyright: 2022 Paul Schaub -License: CC0-1.0 - -Files: _layouts/* -Copyright: 2022 Paul Schaub , 2017 Steve Smith -License: CC-BY-SA-3.0 - -# Man Pages -Files: pgpainless-cli/rewriteManPages.sh -Copyright: 2022 Paul Schaub -License: Apache-2.0 - -Files: pgpainless-cli/packaging/man/* -Copyright: 2022 Paul Schaub -License: Apache-2.0 - -# Github Issue Templates -Files: .github/ISSUE_TEMPLATE/* -Copyright: 2024 Paul Schaub -License: CC0-1.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..991b2099 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,106 @@ +version = 1 +SPDX-PackageName = "PGPainless" +SPDX-PackageSupplier = "Paul Schaub " +SPDX-PackageDownloadLocation = "https://pgpainless.org" + +[[annotations]] +path = ".git-blame-ignore-revs" +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "docs/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = ".readthedocs.yaml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "gradle**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2015 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".editorconfig" +precedence = "aggregate" +SPDX-FileCopyrightText = "Facebook" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "assets/repository-open-graph.png" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/pgpainless.svg" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/logo.png" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC-BY-3.0" + +[[annotations]] +path = "assets/test_vectors/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2018 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "pgpainless-core/src/test/resources/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2020 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "audit/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2021 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "CNAME" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "_config.yml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "_layouts/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub , 2017 Steve Smith" +SPDX-License-Identifier = "CC-BY-SA-3.0" + +[[annotations]] +path = "pgpainless-cli/rewriteManPages.sh" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "pgpainless-cli/packaging/man/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".github/ISSUE_TEMPLATE/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" From 588b9d7469a0908d54ac69772540370e9f07038e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:21:46 +0100 Subject: [PATCH 305/351] Add REUSE.toml file to REUSE.toml file --- REUSE.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index 991b2099..d250e0be 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -3,6 +3,12 @@ SPDX-PackageName = "PGPainless" SPDX-PackageSupplier = "Paul Schaub " SPDX-PackageDownloadLocation = "https://pgpainless.org" +[[annotations]] +path = "REUSE.toml" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Paul Schaub " +SPDX-License-Identifier = "CC0-1.0" + [[annotations]] path = ".git-blame-ignore-revs" precedence = "aggregate" From c5bae9d50df08dd21b5572d600f36ee2152f6c6b Mon Sep 17 00:00:00 2001 From: Eric Duffy Date: Fri, 14 Feb 2025 10:35:15 -0600 Subject: [PATCH 306/351] Fix a typo on signature creation bounds check message --- .../org/pgpainless/signature/consumer/SignatureValidator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt index 951beb0f..7cc384e1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/consumer/SignatureValidator.kt @@ -690,7 +690,7 @@ abstract class SignatureValidator { } if (notAfter != null && timestamp > notAfter) { throw SignatureValidationException( - "Signature was made before the latest allowed signature creation time." + + "Signature was made after the latest allowed signature creation time." + " Created: ${timestamp.formatUTC()}," + " latest allowed: ${notAfter.formatUTC()}") } From 8d03810bf360800c85c6a95c8a4de13c5552b160 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Feb 2025 14:25:59 +0100 Subject: [PATCH 307/351] Fix typo in test --- .../pgpainless/decryption_verification/MessageMetadataTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java index 2c29d62a..7c443829 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -24,7 +24,7 @@ public class MessageMetadataTest { @Test public void processTestMessage_COMP_ENC_ENC_LIT() { // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness - // and randomness cannot be encrypted. + // and randomness cannot be compressed. // For the sake of testing though, this is okay. MessageMetadata.Message message = new MessageMetadata.Message(); From 25c720b033059e30c974c0fb9862ab6489707f10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Feb 2025 13:37:51 +0100 Subject: [PATCH 308/351] SOP inline-sign: Wrap data in LiteralData(text) packet if signing mode is text Fixes #465 --- .../org/pgpainless/sop/InlineSignImpl.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt index 6fdf59a1..ed514a5f 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -12,6 +12,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless import org.pgpainless.algorithm.DocumentSignatureType +import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.bouncycastle.extensions.openPgpFingerprint import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions @@ -56,11 +57,20 @@ class InlineSignImpl : InlineSign { val producerOptions = ProducerOptions.sign(signingOptions).apply { - if (mode == InlineSignAs.clearsigned) { - setCleartextSigned() - setAsciiArmor(true) // CSF is always armored - } else { - setAsciiArmor(armor) + when (mode) { + InlineSignAs.clearsigned -> { + setCleartextSigned() + setAsciiArmor(true) // CSF is always armored + setEncoding(StreamEncoding.TEXT) + applyCRLFEncoding() + } + InlineSignAs.text -> { + setEncoding(StreamEncoding.TEXT) + applyCRLFEncoding() + } + else -> { + setAsciiArmor(armor) + } } } From cb6dde4e39573afdff28abca917acea98e18abb9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Feb 2025 15:46:36 +0100 Subject: [PATCH 309/351] SOP: encrypt: Apply CRLF encoding if text encoding is used Fixes #466 --- .../src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt index b227561e..daf23b55 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -65,6 +65,10 @@ class EncryptImpl : Encrypt { .setAsciiArmor(armor) .setEncoding(modeToStreamEncoding(mode)) + if (modeToStreamEncoding(mode) != StreamEncoding.BINARY) { + options.applyCRLFEncoding() + } + signingKeys.forEach { try { signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) From bfdbac0f2d71b9a974abbe6a5d41b73f00852cc4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 22 Feb 2025 17:26:21 +0100 Subject: [PATCH 310/351] Fix test by comparing to CRLF'd plaintext --- .../commands/RoundTripInlineSignInlineVerifyCmdTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java index 0676f213..057cec98 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/RoundTripInlineSignInlineVerifyCmdTest.java @@ -138,6 +138,10 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { "\n" + "There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power."; + private static final String MESSAGE_CRLF = "One does not simply use OpenPGP!\r\n" + + "\r\n" + + "There is only one Lord of the Keys, only one who can bend them to his will. And he does not share power."; + @Test public void createCleartextSignedMessage() throws IOException { File key = writeFile("key.asc", KEY_1); @@ -153,7 +157,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { String cleartextSigned = ciphertextOut.toString(); assertTrue(cleartextSigned.startsWith("-----BEGIN PGP SIGNED MESSAGE-----\n" + "Hash: ")); - assertTrue(cleartextSigned.contains(MESSAGE)); + assertTrue(cleartextSigned.contains(MESSAGE_CRLF)); assertTrue(cleartextSigned.contains("\n-----BEGIN PGP SIGNATURE-----\n")); assertTrue(cleartextSigned.endsWith("-----END PGP SIGNATURE-----\n")); } @@ -203,7 +207,7 @@ public class RoundTripInlineSignInlineVerifyCmdTest extends CLITest { "--verifications-out", verifications.getAbsolutePath(), cert.getAbsolutePath())); - assertEquals(MESSAGE, plaintextOut.toString()); + assertEquals(MESSAGE_CRLF, plaintextOut.toString()); String verificationString = readStringFromFile(verifications); assertTrue(verificationString.contains(CERT_1_SIGNING_KEY)); } From 74b28afd4a7f44e4213a2530f2801fafe2be7edc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 5 Mar 2025 14:30:13 +0100 Subject: [PATCH 311/351] SOP: inline-sign, detached-sign: Do not apply compression --- .../src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt | 6 +++++- .../src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt index a84ed6ef..19bc782b 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/DetachedSignImpl.kt @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.bouncycastle.extensions.openPgpFingerprint @@ -57,7 +58,10 @@ class DetachedSignImpl : DetachedSign { val signingStream = PGPainless.encryptAndOrSign() .discardOutput() - .withOptions(ProducerOptions.sign(signingOptions).setAsciiArmor(armor)) + .withOptions( + ProducerOptions.sign(signingOptions) + .setAsciiArmor(armor) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)) return object : ReadyWithResult() { override fun writeTo(outputStream: OutputStream): SigningResult { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt index ed514a5f..bd77b553 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineSignImpl.kt @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless +import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.algorithm.StreamEncoding import org.pgpainless.bouncycastle.extensions.openPgpFingerprint @@ -72,6 +73,7 @@ class InlineSignImpl : InlineSign { setAsciiArmor(armor) } } + overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) } return object : Ready() { From 4185bf0326165cbf52c261851d77cc91232bcc81 Mon Sep 17 00:00:00 2001 From: Bastien JANSEN Date: Tue, 11 Mar 2025 17:57:55 +0100 Subject: [PATCH 312/351] Fix #469 --- .../encryption_signing/CRLFGeneratorStream.kt | 2 +- .../CanonicalizedDataEncryptionTest.java | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt index badc8b48..76d747e0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/CRLFGeneratorStream.kt @@ -37,7 +37,7 @@ class CRLFGeneratorStream(private val crlfOut: OutputStream, encoding: StreamEnc } override fun close() { - if (!isBinary && lastB == 'r'.code) { + if (!isBinary && lastB == '\r'.code) { crlfOut.write('\n'.code) } crlfOut.close() diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index 36e473ac..f9936db0 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -13,6 +13,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; @@ -30,6 +31,9 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; @@ -288,11 +292,9 @@ public class CanonicalizedDataEncryptionTest { } } - @Test - public void resultOfDecryptionIsCRLFEncoded() throws PGPException, IOException { - String before = "Foo\nBar!\n"; - String after = "Foo\r\nBar!\r\n"; - + @ParameterizedTest + @MethodSource("resultOfDecryptionIsCRLFEncodedArguments") + public void resultOfDecryptionIsCRLFEncoded(String before, String after) throws PGPException, IOException { String encrypted = encryptAndSign(before, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); ByteArrayInputStream in = new ByteArrayInputStream(encrypted.getBytes(StandardCharsets.UTF_8)); @@ -309,6 +311,16 @@ public class CanonicalizedDataEncryptionTest { assertArrayEquals(after.getBytes(StandardCharsets.UTF_8), decrypted.toByteArray()); } + private static Stream resultOfDecryptionIsCRLFEncodedArguments() { + return Stream.of( + Arguments.of("foo", "foo"), + Arguments.of("rrr", "rrr"), + Arguments.of("Foo\nBar!\n", "Foo\r\nBar!\r\n"), + Arguments.of("Foo\rBar!\r", "Foo\r\nBar!\r\n"), + Arguments.of("Foo\r\nBar!\r\n", "Foo\r\nBar!\r\n") + ); + } + @Test public void resultOfDecryptionIsNotCRLFEncoded() throws PGPException, IOException { String beforeAndAfter = "Foo\nBar!\n"; From 885442920597ed5e022cce6c5e1c87c4da6fcf06 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 20:53:24 +0100 Subject: [PATCH 313/351] Revert "SOP: encrypt: Apply CRLF encoding if text encoding is used" This reverts commit cb6dde4e39573afdff28abca917acea98e18abb9. --- .../src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt index daf23b55..b227561e 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt @@ -65,10 +65,6 @@ class EncryptImpl : Encrypt { .setAsciiArmor(armor) .setEncoding(modeToStreamEncoding(mode)) - if (modeToStreamEncoding(mode) != StreamEncoding.BINARY) { - options.applyCRLFEncoding() - } - signingKeys.forEach { try { signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode)) From 13c3295e64655ce5528431047ba556f301d0d0f9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:00:26 +0100 Subject: [PATCH 314/351] Bump sop-java to 10.1.0 --- pgpainless-sop/build.gradle | 2 +- .../src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt | 6 ------ .../main/kotlin/org/pgpainless/sop/VersionImpl.kt | 12 ------------ version.gradle | 2 +- 4 files changed, 2 insertions(+), 20 deletions(-) diff --git a/pgpainless-sop/build.gradle b/pgpainless-sop/build.gradle index aa0f5993..53b499c6 100644 --- a/pgpainless-sop/build.gradle +++ b/pgpainless-sop/build.gradle @@ -22,7 +22,7 @@ dependencies { testImplementation "ch.qos.logback:logback-classic:$logbackVersion" // Depend on "shared" sop-java test suite (fixtures are turned into tests by inheritance inside test sources) - testImplementation(testFixtures("org.pgpainless:sop-java:$sopJavaVersion")) + testImplementation "org.pgpainless:sop-java-testfixtures:$sopJavaVersion" implementation(project(":pgpainless-core")) api "org.pgpainless:sop-java:$sopJavaVersion" diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt index bf7e63f1..40ac811d 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -12,7 +12,6 @@ import org.bouncycastle.util.io.Streams import org.pgpainless.decryption_verification.OpenPgpInputStream import org.pgpainless.util.ArmoredOutputStreamFactory import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException import sop.operation.Armor @@ -46,9 +45,4 @@ class ArmorImpl : Armor { } } } - - @Deprecated("Setting custom labels is not supported.") - override fun label(label: ArmorLabel): Armor { - throw SOPGPException.UnsupportedOption("Setting custom Armor labels not supported.") - } } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt index c6761a39..46aa4f14 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -72,17 +72,5 @@ https://www.bouncycastle.org/java.html""" ?: "" } - private fun getSopJavaVersion(): String? { - return try { - val resourceIn: InputStream = - javaClass.getResourceAsStream("/sop-java-version.properties") - ?: throw IOException("File sop-java-version.properties not found.") - val properties = Properties().apply { load(resourceIn) } - properties.getProperty("sop-java-version") - } catch (e: IOException) { - null - } - } - override fun isSopSpecImplementationIncomplete(): Boolean = false } diff --git a/version.gradle b/version.gradle index 07c66ec5..4d36ca57 100644 --- a/version.gradle +++ b/version.gradle @@ -14,6 +14,6 @@ allprojects { logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.0.3' + sopJavaVersion = '10.1.0' } } From 9a1a01fe050fe14c5a0afcfed6ac54d659b408b3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:00:51 +0100 Subject: [PATCH 315/351] pgpainless-cli version: Fix repository URL --- .../src/main/kotlin/org/pgpainless/sop/VersionImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt index 46aa4f14..94b9c016 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/VersionImpl.kt @@ -26,7 +26,7 @@ class VersionImpl : Version { String.format(Locale.US, "Bouncy Castle %.2f", BouncyCastleProvider().version) val specVersion = String.format("%02d", SOP_VERSION) return """${getName()} ${getVersion()} -https://codeberg.org/PGPainless/pgpainless/src/branch/master/pgpainless-sop +https://codeberg.org/PGPainless/pgpainless/src/branch/main/pgpainless-sop Implementation of the Stateless OpenPGP Protocol Version $specVersion https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-$specVersion From 883eb80a63381e310bf96390ac72d856266aa8f0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 22:04:03 +0100 Subject: [PATCH 316/351] Bump bcpg, bcprov to 1.80, add bcutil dependency Adding bcutil as a dependency is apparently required now. See https://github.com/bcgit/bc-java/issues/1977 --- pgpainless-core/build.gradle | 1 + .../CachingBcPublicKeyDataDecryptorFactory.kt | 33 ++++++++++++++++--- .../CustomPublicKeyDataDecryptorFactory.kt | 5 +-- .../HardwareSecurity.kt | 16 ++++++--- ...stomPublicKeyDataDecryptorFactoryTest.java | 4 +-- version.gradle | 2 +- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/pgpainless-core/build.gradle b/pgpainless-core/build.gradle index bab6ecf1..64d538d5 100644 --- a/pgpainless-core/build.gradle +++ b/pgpainless-core/build.gradle @@ -22,6 +22,7 @@ dependencies { // Bouncy Castle api "org.bouncycastle:bcprov-jdk18on:$bouncyCastleVersion" api "org.bouncycastle:bcpg-jdk18on:$bouncyPgVersion" + api "org.bouncycastle:bcutil-jdk18on:$bouncyCastleVersion" // api(files("../libs/bcpg-jdk18on-1.70.jar")) // @Nullable, @Nonnull annotations diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt index 3d07065e..9bafa6da 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CachingBcPublicKeyDataDecryptorFactory.kt @@ -4,7 +4,11 @@ package org.pgpainless.decryption_verification +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket import org.bouncycastle.openpgp.PGPPrivateKey +import org.bouncycastle.openpgp.PGPSessionKey +import org.bouncycastle.openpgp.operator.PGPDataDecryptor import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory import org.bouncycastle.util.encoders.Base64 import org.pgpainless.key.SubkeyIdentifier @@ -21,16 +25,34 @@ import org.pgpainless.key.SubkeyIdentifier class CachingBcPublicKeyDataDecryptorFactory( privateKey: PGPPrivateKey, override val subkeyIdentifier: SubkeyIdentifier -) : BcPublicKeyDataDecryptorFactory(privateKey), CustomPublicKeyDataDecryptorFactory { +) : CustomPublicKeyDataDecryptorFactory() { + private val decryptorFactory: BcPublicKeyDataDecryptorFactory = + BcPublicKeyDataDecryptorFactory(privateKey) private val cachedSessions: MutableMap = mutableMapOf() + override fun createDataDecryptor(p0: Boolean, p1: Int, p2: ByteArray?): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1, p2) + } + + override fun createDataDecryptor(p0: AEADEncDataPacket?, p1: PGPSessionKey?): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1) + } + + override fun createDataDecryptor( + p0: SymmetricEncIntegrityPacket?, + p1: PGPSessionKey? + ): PGPDataDecryptor { + return decryptorFactory.createDataDecryptor(p0, p1) + } + override fun recoverSessionData( keyAlgorithm: Int, - secKeyData: Array + secKeyData: Array, + pkeskVersion: Int ): ByteArray = lookupSessionKeyData(secKeyData) - ?: costlyRecoverSessionData(keyAlgorithm, secKeyData).also { + ?: costlyRecoverSessionData(keyAlgorithm, secKeyData, pkeskVersion).also { cacheSessionKeyData(secKeyData, it) } @@ -39,8 +61,9 @@ class CachingBcPublicKeyDataDecryptorFactory( private fun costlyRecoverSessionData( keyAlgorithm: Int, - secKeyData: Array - ): ByteArray = super.recoverSessionData(keyAlgorithm, secKeyData) + secKeyData: Array, + pkeskVersion: Int + ): ByteArray = decryptorFactory.recoverSessionData(keyAlgorithm, secKeyData, pkeskVersion) private fun cacheSessionKeyData(secKeyData: Array, sessionKey: ByteArray) { cachedSessions[toKey(secKeyData)] = sessionKey.clone() diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt index 4a0dbeba..cb6254dc 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt @@ -4,6 +4,7 @@ package org.pgpainless.decryption_verification +import org.bouncycastle.openpgp.operator.AbstractPublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.pgpainless.key.SubkeyIdentifier @@ -14,7 +15,7 @@ import org.pgpainless.key.SubkeyIdentifier * * @see [ConsumerOptions.addCustomDecryptorFactory] */ -interface CustomPublicKeyDataDecryptorFactory : PublicKeyDataDecryptorFactory { +abstract class CustomPublicKeyDataDecryptorFactory : AbstractPublicKeyDataDecryptorFactory() { /** * Identifier for the subkey for which this particular [CustomPublicKeyDataDecryptorFactory] is @@ -22,5 +23,5 @@ interface CustomPublicKeyDataDecryptorFactory : PublicKeyDataDecryptorFactory { * * @return subkey identifier */ - val subkeyIdentifier: SubkeyIdentifier + abstract val subkeyIdentifier: SubkeyIdentifier } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt index 1974e290..50ef3e02 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -29,11 +29,17 @@ class HardwareSecurity { * @param keyId id of the key * @param keyAlgorithm algorithm * @param sessionKeyData encrypted session key + * @param pkeskVersion version of the Public-Key-Encrypted-Session-Key packet (3 or 6) * @return decrypted session key * @throws HardwareSecurityException exception */ @Throws(HardwareSecurityException::class) - fun decryptSessionKey(keyId: Long, keyAlgorithm: Int, sessionKeyData: ByteArray): ByteArray + fun decryptSessionKey( + keyId: Long, + keyAlgorithm: Int, + sessionKeyData: ByteArray, + pkeskVersion: Int + ): ByteArray } /** @@ -44,7 +50,7 @@ class HardwareSecurity { class HardwareDataDecryptorFactory( override val subkeyIdentifier: SubkeyIdentifier, private val callback: DecryptionCallback, - ) : CustomPublicKeyDataDecryptorFactory { + ) : CustomPublicKeyDataDecryptorFactory() { // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. private val factory: PublicKeyDataDecryptorFactory = BcPublicKeyDataDecryptorFactory(null) @@ -73,10 +79,12 @@ class HardwareSecurity { override fun recoverSessionData( keyAlgorithm: Int, - secKeyData: Array + secKeyData: Array, + pkeskVersion: Int ): ByteArray { return try { - callback.decryptSessionKey(subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0]) + callback.decryptSessionKey( + subkeyIdentifier.subkeyId, keyAlgorithm, secKeyData[0], pkeskVersion) } catch (e: HardwareSecurityException) { throw PGPException("Hardware-backed decryption failed.", e) } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java index 73c3bf56..71fbf9be 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -55,14 +55,14 @@ public class CustomPublicKeyDataDecryptorFactoryTest { HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { @Override - public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData, int pkeskVersion) throws HardwareSecurity.HardwareSecurityException { // Emulate hardware decryption. try { PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); - return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}, pkeskVersion); } catch (PGPException e) { throw new HardwareSecurity.HardwareSecurityException(); } diff --git a/version.gradle b/version.gradle index 4d36ca57..25dae7be 100644 --- a/version.gradle +++ b/version.gradle @@ -8,7 +8,7 @@ allprojects { isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 - bouncyCastleVersion = '1.78.1' + bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.5.13' From d7e08186ac150bcc1c8e4d1d07a98d783390c72a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 22:20:00 +0100 Subject: [PATCH 317/351] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b410a6fb..88f39db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.3 +- Bump `bcpg-jdk8on` to `1.80` +- Bump `bcprov-jdk18on` to `1.80` +- Add dependency on `bcutil-jdk18on` as a workaround +- Ignore unknown type signatures on certificates +- Fix typo on signature creation bounds check (thanks @elduffy) +- Fix superfluous newline added in CRLF encoding (thanks @bjansen) +- Bump `sop-java` to `1.10.0` + - SOP inline-sign: Do not apply compression + ## 1.7.2 - Fix bug in `KeyRingInfo.lastModified` (thanks to @Jerbell, @sosnovsky for reporting) - Bump `sop-java` to `10.0.3` From 3b1dbf4102961b3f767d9221ffb0cc893db91611 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 22:40:44 +0100 Subject: [PATCH 318/351] PGPainless 1.7.3 --- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 41d3af6b..1ae88818 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.2' + implementation 'org.pgpainless:pgpainless-core:1.7.3' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 8696d287..d041bc0e 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.2" + implementation "org.pgpainless:pgpainless-sop:1.7.3" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.2 + 1.7.3 ... diff --git a/version.gradle b/version.gradle index 25dae7be..1d32c6bf 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.3' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.80' From bb9393d948142a7f517ed62306eef4a3360b29ed Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 22:43:46 +0100 Subject: [PATCH 319/351] PGPainless 1.7.4-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 1d32c6bf..541df81e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.3' - isSnapshot = false + shortVersion = '1.7.4' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.80' From effc9e747aa806e119f974ddda96be293b99176c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 22:55:43 +0100 Subject: [PATCH 320/351] Remove -werror flag from javadoc task --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 29e054c2..52240b95 100644 --- a/build.gradle +++ b/build.gradle @@ -279,7 +279,7 @@ if (JavaVersion.current().isJava8Compatible()) { // gradle. See https://github.com/gradle/gradle/issues/2354 // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) // for information about the -Xwerror option. - options.addStringOption('Xwerror', '-quiet') + // options.addStringOption('Xwerror', '-quiet') } } From 62b0d0a560e27e396e12c2e50fb10bfe0b5d6c83 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Mar 2025 13:44:24 +0100 Subject: [PATCH 321/351] Remove YourKit profiler usage --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 1ae88818..b8e578f7 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,6 @@ Parts of PGPainless development ([project page](https://nlnet.nl/project/PGPainl NGI Assure is made possible with financial support from the [European Commission](https://ec.europa.eu/)'s [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en). [![NGI Assure Logo](https://blog.jabberhead.tk/wp-content/uploads/2022/05/NGIAssure_tag.svg)](https://nlnet.nl/assure/) -Thanks to [YourKit](https://www.yourkit.com/) for providing a free license of the [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) to support PGPainless Development! -[![YourKit Logo](https://www.yourkit.com/images/yklogo.png)](https://www.yourkit.com/) - Big thank you also to those who decided to support the work by donating! Notably @msfjarvis From c861a5eb732c72e28e15ec89545c74d59fd7edbc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Mar 2025 17:31:30 +0100 Subject: [PATCH 322/351] Set Java release version to 8 to fix Kotlin desugaring Fixes https://github.com/pgpainless/pgpainless/issues/471 --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 52240b95..b084a89c 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,10 @@ allprojects { options.release = 8 } + tasks.withType(JavaCompile).configureEach { + options.release = 8 + } + // Only generate jar for submodules // without this we would generate an empty pgpainless.jar for the project root // https://stackoverflow.com/a/25445035 From cb51bb64f358566384b331d78bf2f4fc31deb983 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Mar 2025 17:35:08 +0100 Subject: [PATCH 323/351] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f39db0..5386a787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.4-SNAPSHOT +- Fix proper Kotlin desugaring for Java 8 + ## 1.7.3 - Bump `bcpg-jdk8on` to `1.80` - Bump `bcprov-jdk18on` to `1.80` From f22cada0cef1933f2f52bc9142bd6311a0e5a30d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Mar 2025 18:02:07 +0100 Subject: [PATCH 324/351] PGPainless 1.7.4 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5386a787..c9f79e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.4-SNAPSHOT +## 1.7.4 - Fix proper Kotlin desugaring for Java 8 ## 1.7.3 diff --git a/README.md b/README.md index b8e578f7..f2810265 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.3' + implementation 'org.pgpainless:pgpainless-core:1.7.4' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index d041bc0e..50551fec 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.3" + implementation "org.pgpainless:pgpainless-sop:1.7.4" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.3 + 1.7.4 ... diff --git a/version.gradle b/version.gradle index 541df81e..709d1863 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.4' - isSnapshot = true + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.80' From 7dc4329c52cfd8d4fa6bf9eb3a2b501f2ea4e16d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Mar 2025 18:05:37 +0100 Subject: [PATCH 325/351] PGPainless 1.7.5-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 709d1863..05fff865 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.4' - isSnapshot = false + shortVersion = '1.7.5' + isSnapshot = true pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.80' From eebd02e309116139af06fec41683feb5f880f152 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 20 Mar 2025 18:18:40 +0100 Subject: [PATCH 326/351] Perform coveralls task after jacocoRootReport --- .github/workflows/gradle_push.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle_push.yml b/.github/workflows/gradle_push.yml index e4624726..8c03a399 100644 --- a/.github/workflows/gradle_push.yml +++ b/.github/workflows/gradle_push.yml @@ -28,9 +28,13 @@ jobs: with: java-version: '11' distribution: 'temurin' - - name: Build, Check and Coverage + - name: Build and Check + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: check jacocoRootReport + - name: Coveralls uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} with: - arguments: check jacocoRootReport coveralls \ No newline at end of file + arguments: coveralls From a0254f47fb89d4d0cf827619417adfd84828c138 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 11:03:22 +0100 Subject: [PATCH 327/351] Simplify mavenCentralChecksums task --- build.gradle | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/build.gradle b/build.gradle index b084a89c..f8cfbc9d 100644 --- a/build.gradle +++ b/build.gradle @@ -296,34 +296,13 @@ task mavenCentralChecksums() { description 'Fetch and display checksums for artifacts published to Maven Central' String ver = project.hasProperty('release') ? release : shortVersion doLast { - Process p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-core/${ver}/pgpainless-core-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-core/build/libs/pgpainless-core-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-sop/${ver}/pgpainless-sop-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-sop/build/libs/pgpainless-sop-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}-all.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-cli/build/libs/pgpainless-cli-${ver}-all.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/pgpainless-cli/${ver}/pgpainless-cli-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " pgpainless-cli/build/libs/pgpainless-cli-${ver}.jar" - } - - p = "curl -f https://repo1.maven.org/maven2/org/pgpainless/hsregex/${ver}/hsregex-${ver}.jar.sha256".execute() - if (p.waitFor() == 0) { - print p.text.trim() - println " hsregex/build/libs/hsregex-${ver}.jar" + for (Project p : rootProject.subprojects) { + String url = "https://repo1.maven.org/maven2/org/pgpainless/${p.name}/${ver}/${p.name}-${ver}.jar.sha256" + Process fetch = "curl -f $url".execute() + if (fetch.waitFor() == 0) { + print fetch.text.trim() + println " ${p.name}/build/libs/${p.name}-${ver}.jar" + } } } } From 65113a6d82cc0671f5065eb4b0d96bbb41d1132f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 11:22:16 +0100 Subject: [PATCH 328/351] Rework gradle, making use of toolchain feature --- build.gradle | 44 ++++++++----------------------------- pgpainless-cli/build.gradle | 19 ---------------- version.gradle | 3 +-- 3 files changed, 10 insertions(+), 56 deletions(-) diff --git a/build.gradle b/build.gradle index f8cfbc9d..d05845b4 100644 --- a/build.gradle +++ b/build.gradle @@ -33,18 +33,6 @@ allprojects { apply plugin: 'kotlin' apply plugin: 'com.diffplug.spotless' - java { - targetCompatibility = JavaVersion.VERSION_1_8 - } - - compileJava { - options.release = 8 - } - - tasks.withType(JavaCompile).configureEach { - options.release = 8 - } - // Only generate jar for submodules // without this we would generate an empty pgpainless.jar for the project root // https://stackoverflow.com/a/25445035 @@ -68,8 +56,6 @@ allprojects { description = "Simple to use OpenPGP API for Java based on Bouncycastle" version = shortVersion - sourceCompatibility = javaSourceCompatibility - repositories { mavenCentral() mavenLocal() @@ -84,6 +70,10 @@ allprojects { fileMode = 0644 } + kotlin { + jvmToolchain(javaSourceCompatibility) + } + // Compatibility of default implementations in kotlin interfaces with Java implementations. tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { @@ -124,7 +114,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.enabled true + xml.required = true } } @@ -142,15 +132,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' + archiveClassifier = 'tests' from sourceSets.test.output } @@ -247,7 +237,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.enabled true + xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -258,10 +248,6 @@ task jacocoRootReport(type: JacocoReport) { } task javadocAll(type: Javadoc) { - def currentJavaVersion = JavaVersion.current() - if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) { - options.addStringOption("-release", "8"); - } source subprojects.collect {project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'javadoc') @@ -275,18 +261,6 @@ task javadocAll(type: Javadoc) { ] as String[] } -if (JavaVersion.current().isJava8Compatible()) { - tasks.withType(Javadoc) { - // The '-quiet' as second argument is actually a hack, - // since the one paramater addStringOption doesn't seem to - // work, we extra add '-quiet', which is added anyway by - // gradle. See https://github.com/gradle/gradle/issues/2354 - // See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363) - // for information about the -Xwerror option. - // options.addStringOption('Xwerror', '-quiet') - } -} - /** * Fetch sha256 checksums of artifacts published to maven central. * diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index d6550139..b3bc192c 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,7 +4,6 @@ plugins { id 'application' - id "com.github.johnrengelman.shadow" version "6.1.0" } dependencies { @@ -31,22 +30,6 @@ mainClassName = 'org.pgpainless.cli.PGPainlessCLI' application { mainClass = mainClassName } -/** -jar { - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) - manifest { - attributes 'Main-Class': "$mainClassName" - } - - from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } - } { - exclude "META-INF/*.SF" - exclude "META-INF/*.DSA" - exclude "META-INF/*.RSA" - } -} - */ run { // https://stackoverflow.com/questions/59445306/pipe-into-gradle-run @@ -56,5 +39,3 @@ run { args Eval.me(appArgs) } } - -// tasks."jar".dependsOn(":pgpainless-core:assemble", ":pgpainless-sop:assemble") diff --git a/version.gradle b/version.gradle index 05fff865..72b388a6 100644 --- a/version.gradle +++ b/version.gradle @@ -6,8 +6,7 @@ allprojects { ext { shortVersion = '1.7.5' isSnapshot = true - pgpainlessMinAndroidSdk = 10 - javaSourceCompatibility = 1.8 + javaSourceCompatibility = 11 bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' From f054c30460cae0887c4c73b5124f1c7484515aeb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 11:58:55 +0100 Subject: [PATCH 329/351] Upgrade gradle-wrapper to 8.8 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684..0d184210 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2b9c5ea2724baac3a03b0866f776b95eac15e849 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 12:01:29 +0100 Subject: [PATCH 330/351] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f79e45..a593249b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.5-SNAPSHOT +- Actually attempt to fix Kotlin desugaring. +- Bump javaSourceCompatibility and javaTargetCompatibility to 11 +- Bump gradle-wrapper to 8.8 + ## 1.7.4 - Fix proper Kotlin desugaring for Java 8 From de5ef4de004eceaaecccab997e37b48618791238 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 12:05:33 +0100 Subject: [PATCH 331/351] PGPainless 1.7.5 --- CHANGELOG.md | 2 +- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a593249b..8edff65d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.5-SNAPSHOT +## 1.7.5 - Actually attempt to fix Kotlin desugaring. - Bump javaSourceCompatibility and javaTargetCompatibility to 11 - Bump gradle-wrapper to 8.8 diff --git a/README.md b/README.md index f2810265..44eb180d 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.4' + implementation 'org.pgpainless:pgpainless-core:1.7.5' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index 50551fec..e21c821f 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.4" + implementation "org.pgpainless:pgpainless-sop:1.7.5" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.4 + 1.7.5 ... diff --git a/version.gradle b/version.gradle index 72b388a6..54933725 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.5' - isSnapshot = true + isSnapshot = false javaSourceCompatibility = 11 bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion From a6f8058fb43b97d25e0acb25ebede521f5e8dbe6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 25 Mar 2025 12:09:43 +0100 Subject: [PATCH 332/351] PGPainless 1.7.6-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 54933725..73c8c686 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.5' - isSnapshot = false + shortVersion = '1.7.6' + isSnapshot = true javaSourceCompatibility = 11 bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion From 811f72ffef7a00edcca041e51e50697a66eeddf7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Mar 2025 15:00:59 +0100 Subject: [PATCH 333/351] Fix RevocationSignatureBuilder properly calculating 3rd-party delegation revocations --- .../pgpainless/signature/builder/RevocationSignatureBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt index d8c594fe..36589613 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/builder/RevocationSignatureBuilder.kt @@ -52,7 +52,7 @@ class RevocationSignatureBuilder : AbstractSignatureBuilder Date: Wed, 26 Mar 2025 15:04:22 +0100 Subject: [PATCH 334/351] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8edff65d..75b18280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.5-SNAPSHOT +- Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations) + ## 1.7.5 - Actually attempt to fix Kotlin desugaring. - Bump javaSourceCompatibility and javaTargetCompatibility to 11 From 143c9777d644fe3ccaff8d6f93ade2c9aaa5699c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Apr 2025 14:59:08 +0200 Subject: [PATCH 335/351] Implement graal nativeimage compilation Requires sop-java 10.1.1-SNAPSHOT for now, as that version includes picocli configurations files --- pgpainless-cli/build.gradle | 8 +++++++- version.gradle | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index b3bc192c..6c1d283a 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -4,6 +4,11 @@ plugins { id 'application' + id 'org.graalvm.buildtools.native' version '0.10.6' +} + +graalvmNative { + toolchainDetection = true } dependencies { @@ -16,7 +21,8 @@ dependencies { // implementation "ch.qos.logback:logback-core:1.2.6" // We want logback logging in tests and in the app testImplementation "ch.qos.logback:logback-classic:$logbackVersion" - implementation "ch.qos.logback:logback-classic:$logbackVersion" + // implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation "org.slf4j:slf4j-nop:$slf4jVersion" implementation(project(":pgpainless-sop")) implementation "org.pgpainless:sop-java-picocli:$sopJavaVersion" diff --git a/version.gradle b/version.gradle index 73c8c686..a4ace4c2 100644 --- a/version.gradle +++ b/version.gradle @@ -13,6 +13,6 @@ allprojects { logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.1.0' + sopJavaVersion = '10.1.1-SNAPSHOT' } } From 2d0608cf0f6f2da15ad6e2fcc5babaa962dbd882 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 13 Apr 2025 19:45:12 +0200 Subject: [PATCH 336/351] Re-add shadow plugin --- pgpainless-cli/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index 6c1d283a..e86ddedf 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -5,6 +5,7 @@ plugins { id 'application' id 'org.graalvm.buildtools.native' version '0.10.6' + id 'com.github.johnrengelman.shadow' version '8.1.1' } graalvmNative { From d20a3b75569d529d05ff11716a165268dcc812f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 15:30:58 +0200 Subject: [PATCH 337/351] Add config files for nativeimage Those were generated by running the following commands in order: gradle -Pagent test gradle metadataCopy --task test --dir src/main/resources/META-INF/native-image gradle nativeCompile The resulting nativeimage can resolve method calls that use reflection. Yay --- .../META-INF/native-image/jni-config.json | 2 + .../predefined-classes-config.json | 7 + .../META-INF/native-image/proxy-config.json | 2 + .../META-INF/native-image/reflect-config.json | 991 ++++++++++++++++++ .../native-image/resource-config.json | 95 ++ .../native-image/serialization-config.json | 41 + 6 files changed, 1138 insertions(+) create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json create mode 100644 pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json new file mode 100644 index 00000000..32960f8c --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/jni-config.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json new file mode 100644 index 00000000..84789507 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/predefined-classes-config.json @@ -0,0 +1,7 @@ +[ + { + "type":"agent-extracted", + "classes":[ + ] + } +] diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json new file mode 100644 index 00000000..32960f8c --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/proxy-config.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 00000000..25ad3faf --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,991 @@ +[ +{ + "name":"[B" +}, +{ + "name":"[Ljava.lang.Object;" +}, +{ + "name":"[Ljava.lang.String;" +}, +{ + "name":"[Lsun.security.pkcs.SignerInfo;" +}, +{ + "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.joran.SerializedModelConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.classic.util.DefaultJoranConfigurator", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"ch.qos.logback.core.ConsoleAppender", + "queryAllPublicMethods":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"setTarget","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.OutputStreamAppender", + "methods":[{"name":"setEncoder","parameterTypes":["ch.qos.logback.core.encoder.Encoder"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.Encoder", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.encoder.LayoutWrappingEncoder", + "methods":[{"name":"setParent","parameterTypes":["ch.qos.logback.core.spi.ContextAware"] }] +}, +{ + "name":"ch.qos.logback.core.pattern.PatternLayoutEncoderBase", + "methods":[{"name":"setPattern","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"ch.qos.logback.core.spi.ContextAware", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"groovy.lang.Closure" +}, +{ + "name":"java.io.FilePermission" +}, +{ + "name":"java.lang.Enum" +}, +{ + "name":"java.lang.Object", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"java.lang.RuntimePermission" +}, +{ + "name":"java.lang.String" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"console","parameterTypes":[] }] +}, +{ + "name":"java.lang.invoke.MethodHandle" +}, +{ + "name":"java.net.NetPermission" +}, +{ + "name":"java.net.SocketPermission" +}, +{ + "name":"java.net.URLPermission", + "methods":[{"name":"","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.nio.channels.SelectionKey", + "fields":[{"name":"attachment"}] +}, +{ + "name":"java.nio.file.Path" +}, +{ + "name":"java.nio.file.Paths", + "methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.AllPermission" +}, +{ + "name":"java.security.MessageDigestSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.security.SecurityPermission" +}, +{ + "name":"java.security.cert.PKIXRevocationChecker" +}, +{ + "name":"java.security.interfaces.DSAPrivateKey" +}, +{ + "name":"java.security.interfaces.DSAPublicKey" +}, +{ + "name":"java.security.interfaces.RSAPrivateKey" +}, +{ + "name":"java.security.interfaces.RSAPublicKey" +}, +{ + "name":"java.security.spec.DSAParameterSpec" +}, +{ + "name":"java.sql.Connection" +}, +{ + "name":"java.sql.Driver" +}, +{ + "name":"java.sql.DriverManager", + "methods":[{"name":"getConnection","parameterTypes":["java.lang.String"] }, {"name":"getDriver","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.sql.Time", + "methods":[{"name":"","parameterTypes":["long"] }] +}, +{ + "name":"java.sql.Timestamp", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.Duration", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Instant", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalDate", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.LocalTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.MonthDay", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.OffsetDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.OffsetTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Period", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.Year", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.YearMonth", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.time.ZoneId", + "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.ZoneOffset", + "methods":[{"name":"of","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.time.ZonedDateTime", + "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] +}, +{ + "name":"java.util.Date" +}, +{ + "name":"java.util.HashSet" +}, +{ + "name":"java.util.LinkedHashSet" +}, +{ + "name":"java.util.PropertyPermission" +}, +{ + "name":"java.util.concurrent.ArrayBlockingQueue" +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.locks.AbstractOwnableSynchronizer" +}, +{ + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer" +}, +{ + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock$NonfairSync" +}, +{ + "name":"java.util.concurrent.locks.ReentrantLock$Sync" +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"javax.smartcardio.CardPermission" +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.Dilithium$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EXTERNAL$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.Falcon$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$EdDSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.KeyPairGeneratorSpi$XDH", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.ExitCodeTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"successfulExecutionDoesNotTerminateJVM","parameterTypes":[] }, {"name":"testCommandWithUnknownOption_37","parameterTypes":[] }, {"name":"testUnknownCommand_69","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.TestUtils", + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"org.pgpainless.cli.commands.ArmorCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"armorAlreadyArmoredDataIsIdempotent","parameterTypes":[] }, {"name":"armorMessage","parameterTypes":[] }, {"name":"armorPublicKey","parameterTypes":[] }, {"name":"armorSecretKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.CLITest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "methods":[{"name":"cleanup","parameterTypes":[] }, {"name":"setup","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.DearmorCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"dearmorBrokenArmoredKeyFails","parameterTypes":[] }, {"name":"dearmorCertificate","parameterTypes":[] }, {"name":"dearmorGarbageEmitsEmpty","parameterTypes":[] }, {"name":"dearmorMessage","parameterTypes":[] }, {"name":"dearmorSecretKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.ExtractCertCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"extractCertFromGarbageFails","parameterTypes":[] }, {"name":"testExtractCert","parameterTypes":[] }, {"name":"testExtractCertFromCertFails","parameterTypes":[] }, {"name":"testExtractCertUnarmored","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.GenerateKeyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testGenerateBinaryKey","parameterTypes":[] }, {"name":"testGenerateKey","parameterTypes":[] }, {"name":"testGenerateKeyWithMultipleUserIds","parameterTypes":[] }, {"name":"testGeneratePasswordProtectedKey_missingPasswordFile","parameterTypes":[] }, {"name":"testPasswordProtectedKey","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.InlineDetachCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessage","parameterTypes":[] }, {"name":"detachInbandSignatureAndMessageNoArmor","parameterTypes":[] }, {"name":"detachMissingSignaturesFromCleartextSignedMessageFails","parameterTypes":[] }, {"name":"detachNonOpenPgpDataFails","parameterTypes":[] }, {"name":"existingSignatureOutCausesException","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.ListProfilesCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"listProfileOfGenerateKey","parameterTypes":[] }, {"name":"listProfilesOfEncrypt","parameterTypes":[] }, {"name":"listProfilesWithoutCommand","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripEncryptDecryptCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"decryptGarbageFails","parameterTypes":[] }, {"name":"decryptMalformedMessageYieldsBadData","parameterTypes":[] }, {"name":"decryptMessageWithSessionKey","parameterTypes":[] }, {"name":"decryptMessageWithWrongKeyFails","parameterTypes":[] }, {"name":"decryptWithPasswordWithPendingWhitespaceWorks","parameterTypes":[] }, {"name":"decryptWithWhitespacePasswordWorks","parameterTypes":[] }, {"name":"decrypt_verifyWithGarbageCertFails","parameterTypes":[] }, {"name":"decrypt_withGarbageKeyFails","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }, {"name":"encryptAndDecryptMessageWithPassphrase","parameterTypes":[] }, {"name":"encryptWithGarbageCertFails","parameterTypes":[] }, {"name":"encryptWithPasswordADecryptWithPasswordBFails","parameterTypes":[] }, {"name":"encryptWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"encryptWithTrailingWhitespaceDecryptWithoutWorks","parameterTypes":[] }, {"name":"encrypt_signWithGarbageKeyFails","parameterTypes":[] }, {"name":"testDecryptVerifyOut_withoutVerifyWithFails","parameterTypes":[] }, {"name":"testDecryptWithSessionKeyVerifyWithYieldsExpectedVerifications","parameterTypes":[] }, {"name":"testDecryptWithoutDecryptionOptionFails","parameterTypes":[] }, {"name":"testEncryptDecryptRoundTripWithPasswordProtectedKey","parameterTypes":[] }, {"name":"testEncryptDecryptWithFreshRSAKey","parameterTypes":[] }, {"name":"testEncryptWithIncapableCert","parameterTypes":[] }, {"name":"testEncrypt_SignWithCertFails","parameterTypes":[] }, {"name":"testMissingArgumentsIfNoArgsSupplied","parameterTypes":[] }, {"name":"testSessionKeyOutWritesSessionKeyOut","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testVerificationsOutAlreadyExistFails","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripInlineSignInlineVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"cannotVerifyEncryptedMessage","parameterTypes":[] }, {"name":"cannotVerifyMalformedMessage","parameterTypes":[] }, {"name":"createAndVerifyCleartextSignedMessage","parameterTypes":[] }, {"name":"createAndVerifyMultiKeyBinarySignedMessage","parameterTypes":[] }, {"name":"createAndVerifyTextSignedMessage","parameterTypes":[] }, {"name":"createCleartextSignedMessage","parameterTypes":[] }, {"name":"createMalformedMessage","parameterTypes":[] }, {"name":"createSignedMessageWithKeyAAndVerifyWithKeyBFails","parameterTypes":[] }, {"name":"createTextSignedMessageInlineDetachAndDetachedVerify","parameterTypes":[] }, {"name":"signWithProtectedKeyWithWrongPassphraseFails","parameterTypes":[] }, {"name":"testInlineSignWithMissingSecretKeysFails","parameterTypes":[] }, {"name":"testUnlockKeyWithOneOfMultiplePasswords","parameterTypes":[] }, {"name":"verifyPrependedSignedMessage","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripInlineSignVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"encryptAndDecryptAMessage","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.RoundTripSignVerifyCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"createArmoredSignature","parameterTypes":[] }, {"name":"createUnarmoredSignature","parameterTypes":[] }, {"name":"signWithProtectedKey","parameterTypes":[] }, {"name":"signWithProtectedKey_missingPassphraseFails","parameterTypes":[] }, {"name":"signWithProtectedKey_wrongPassphraseFails","parameterTypes":[] }, {"name":"testNotAfter","parameterTypes":[] }, {"name":"testNotBefore","parameterTypes":[] }, {"name":"testSignWithIncapableKey","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }, {"name":"unarmorArmoredSigAndVerify","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.commands.VersionCmdTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testExtendedVersion","parameterTypes":[] }, {"name":"testGetBackendVersion","parameterTypes":[] }, {"name":"testSopSpecVersion","parameterTypes":[] }, {"name":"testVersion","parameterTypes":[] }] +}, +{ + "name":"org.pgpainless.cli.misc.SignUsingPublicKeyBehaviorTest", + "allDeclaredFields":true, + "allDeclaredClasses":true, + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"testSignatureCreationAndVerification","parameterTypes":[] }] +}, +{ + "name":"picocli.AutoComplete$GenerateCompletion", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"picocli.CommandLine$AutoHelpMixin", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"picocli.CommandLine$HelpCommand", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.SopCLI", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.SopCLI$InitLocale", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.AbstractSopCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.ArmorCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ChangeKeyPasswordCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.DearmorCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.DecryptCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.EncryptCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ExtractCertCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.GenerateKeyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineDetachCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineSignCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.InlineVerifyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.ListProfilesCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.RevokeKeyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"sop.cli.picocli.commands.SignCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VerifyCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VersionCmd", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sop.cli.picocli.commands.VersionCmd$Exclusive", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.DSA$SHA256withDSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.DSAKeyFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.DSAParameters", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSASignature$SHA256withRSA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.util.ObjectIdentifier" +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificateExtensions" +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 00000000..af650dc9 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,95 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E" + }, { + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.time.zone.ZoneRulesProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/javax.xml.parsers.SAXParserFactory\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.engine.TestEngine\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.LauncherSessionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.PostDiscoveryFilter\\E" + }, { + "pattern":"\\QMETA-INF/services/org.junit.platform.launcher.TestExecutionListener\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qjunit-platform.properties\\E" + }, { + "pattern":"\\Qlogback-test.scmo\\E" + }, { + "pattern":"\\Qlogback-test.xml\\E" + }, { + "pattern":"\\Qlogback.scmo\\E" + }, { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, { + "pattern":"\\Qpgpainless-sop.properties\\E" + }, { + "pattern":"\\Qsop-java-version.properties\\E" + }, { + "pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E" + }, { + "pattern":"java.base:\\Qsun/text/resources/nfkc.icu\\E" + }]}, + "bundles":[{ + "name":"msg_armor", + "locales":["de", "und"] + }, { + "name":"msg_change-key-password", + "locales":["de", "und"] + }, { + "name":"msg_dearmor", + "locales":["de", "und"] + }, { + "name":"msg_decrypt", + "locales":["de", "und"] + }, { + "name":"msg_detached-sign", + "locales":["de", "und"] + }, { + "name":"msg_detached-verify", + "locales":["de", "und"] + }, { + "name":"msg_encrypt", + "locales":["de", "und"] + }, { + "name":"msg_extract-cert", + "locales":["de", "und"] + }, { + "name":"msg_generate-key", + "locales":["de", "und"] + }, { + "name":"msg_inline-detach", + "locales":["de", "und"] + }, { + "name":"msg_inline-sign", + "locales":["de", "und"] + }, { + "name":"msg_inline-verify", + "locales":["de", "und"] + }, { + "name":"msg_list-profiles", + "locales":["de", "und"] + }, { + "name":"msg_revoke-key", + "locales":["de", "und"] + }, { + "name":"msg_sop", + "locales":["de", "und"] + }, { + "name":"msg_version", + "locales":["de", "und"] + }] +} \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json new file mode 100644 index 00000000..3f06d9a8 --- /dev/null +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/serialization-config.json @@ -0,0 +1,41 @@ +{ + "types":[ + { + "name":"java.lang.Enum" + }, + { + "name":"java.lang.Object[]" + }, + { + "name":"java.util.HashSet" + }, + { + "name":"java.util.LinkedHashSet" + }, + { + "name":"java.util.concurrent.ArrayBlockingQueue" + }, + { + "name":"java.util.concurrent.locks.AbstractOwnableSynchronizer" + }, + { + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer" + }, + { + "name":"java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock$NonfairSync" + }, + { + "name":"java.util.concurrent.locks.ReentrantLock$Sync" + } + ], + "lambdaCapturingTypes":[ + ], + "proxies":[ + ] +} \ No newline at end of file From 05c84835e69d5de69c64d1f007b5b5838da77b8a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 15:31:27 +0200 Subject: [PATCH 338/351] Bump SOP-Java to 10.1.1 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index a4ace4c2..f3c4f100 100644 --- a/version.gradle +++ b/version.gradle @@ -13,6 +13,6 @@ allprojects { logbackVersion = '1.5.13' mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' - sopJavaVersion = '10.1.1-SNAPSHOT' + sopJavaVersion = '10.1.1' } } From 83613250efecc50daf6171c8f46a944597b496a9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 15:55:29 +0200 Subject: [PATCH 339/351] PGPainless 1.7.6 --- CHANGELOG.md | 4 +++- README.md | 2 +- pgpainless-sop/README.md | 4 ++-- version.gradle | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b18280..b1f63d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog -## 1.7.5-SNAPSHOT +## 1.7.6 - Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations) +- Enable support for native images +- Re-enable shadow plugin and build fat-jar ## 1.7.5 - Actually attempt to fix Kotlin desugaring. diff --git a/README.md b/README.md index 44eb180d..e305e43a 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.7.5' + implementation 'org.pgpainless:pgpainless-core:1.7.6' } ``` diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index e21c821f..da112604 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.7.5" + implementation "org.pgpainless:pgpainless-sop:1.7.6" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.7.5 + 1.7.6 ... diff --git a/version.gradle b/version.gradle index f3c4f100..7ed1f065 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '1.7.6' - isSnapshot = true + isSnapshot = false javaSourceCompatibility = 11 bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion From 3b92ccc59d8c6f8c9ef4ccc5f5e8317659fee729 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 16:05:05 +0200 Subject: [PATCH 340/351] PGPainless 1.7.7-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 7ed1f065..003de912 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.7.6' - isSnapshot = false + shortVersion = '1.7.7' + isSnapshot = true javaSourceCompatibility = 11 bouncyCastleVersion = '1.80' bouncyPgVersion = bouncyCastleVersion From 5a413f53a42a26169901ce634b6dfa7cc002f22b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Apr 2025 19:11:52 +0200 Subject: [PATCH 341/351] Specify license information for native-image metadata --- REUSE.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index d250e0be..66b5e867 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -93,6 +93,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2022 Paul Schaub , 2017 Steve Smith" SPDX-License-Identifier = "CC-BY-SA-3.0" +[[annotations]] +path = "pgpainless-cli/src/main/resources/META-INF/native-image/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = "pgpainless-cli/rewriteManPages.sh" precedence = "aggregate" From 0649c041cdead499b391f874830a0bdc2c17e246 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 21 Apr 2025 19:12:10 +0200 Subject: [PATCH 342/351] gradle: migrate to new shadow plugin namespace --- pgpainless-cli/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-cli/build.gradle b/pgpainless-cli/build.gradle index e86ddedf..e4c3f060 100644 --- a/pgpainless-cli/build.gradle +++ b/pgpainless-cli/build.gradle @@ -5,7 +5,7 @@ plugins { id 'application' id 'org.graalvm.buildtools.native' version '0.10.6' - id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.gradleup.shadow' version '8.3.6' } graalvmNative { From 7953ade13673568487eb0a3b7b141f96a08af585 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 12:37:04 +0200 Subject: [PATCH 343/351] Bump checkstyle to 10.25.0 Fixes https://github.com/pgpainless/pgpainless/security/dependabot/24 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d05845b4..93100f4d 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ allprojects { // checkstyle checkstyle { - toolVersion = '10.12.1' + toolVersion = '10.25.0' } spotless { From 5f30df6d166962f780fbfcb870f9934d84e6e771 Mon Sep 17 00:00:00 2001 From: Felix Hagemans Date: Wed, 4 Jun 2025 16:02:23 +0200 Subject: [PATCH 344/351] Fixed typo in sop readme --- pgpainless-sop/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index da112604..7f7267cd 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -67,7 +67,7 @@ byte[] encrypted = sop.encrypt() // Decrypt a message ByteArrayAndResult messageAndVerifications = sop.decrypt() - .verifyWith(cert) + .verifyWithCert(cert) .withKey(key) .ciphertext(encrypted) .toByteArrayAndResult(); From 0f54cc615c5409b5fa12d61182f63d022e78f828 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 10:30:42 +0200 Subject: [PATCH 345/351] Bump BC to 1.81, update native-image reflect-config, resource-config --- .../META-INF/native-image/reflect-config.json | 108 +----------------- .../native-image/resource-config.json | 2 - version.gradle | 2 +- 3 files changed, 5 insertions(+), 107 deletions(-) diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json index 25ad3faf..63bdf5f3 100644 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/reflect-config.json @@ -1,16 +1,7 @@ [ -{ - "name":"[B" -}, { "name":"[Ljava.lang.Object;" }, -{ - "name":"[Ljava.lang.String;" -}, -{ - "name":"[Lsun.security.pkcs.SignerInfo;" -}, { "name":"ch.qos.logback.classic.encoder.PatternLayoutEncoder", "queryAllPublicMethods":true, @@ -70,9 +61,6 @@ { "name":"java.lang.RuntimePermission" }, -{ - "name":"java.lang.String" -}, { "name":"java.lang.System", "methods":[{"name":"console","parameterTypes":[] }] @@ -101,9 +89,6 @@ "name":"java.nio.file.Paths", "methods":[{"name":"get","parameterTypes":["java.lang.String","java.lang.String[]"] }] }, -{ - "name":"java.security.AlgorithmParametersSpi" -}, { "name":"java.security.AllPermission" }, @@ -119,21 +104,6 @@ { "name":"java.security.cert.PKIXRevocationChecker" }, -{ - "name":"java.security.interfaces.DSAPrivateKey" -}, -{ - "name":"java.security.interfaces.DSAPublicKey" -}, -{ - "name":"java.security.interfaces.RSAPrivateKey" -}, -{ - "name":"java.security.interfaces.RSAPublicKey" -}, -{ - "name":"java.security.spec.DSAParameterSpec" -}, { "name":"java.sql.Connection" }, @@ -208,9 +178,6 @@ "name":"java.time.ZonedDateTime", "methods":[{"name":"parse","parameterTypes":["java.lang.CharSequence"] }] }, -{ - "name":"java.util.Date" -}, { "name":"java.util.HashSet" }, @@ -245,11 +212,6 @@ { "name":"java.util.concurrent.locks.ReentrantLock$Sync" }, -{ - "name":"javax.security.auth.x500.X500Principal", - "fields":[{"name":"thisX500Name"}], - "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] -}, { "name":"javax.smartcardio.CardPermission" }, @@ -333,6 +295,10 @@ "name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", "methods":[{"name":"","parameterTypes":[] }] @@ -914,18 +880,6 @@ "queryAllDeclaredMethods":true, "methods":[{"name":"","parameterTypes":[] }] }, -{ - "name":"sun.security.provider.DSA$SHA256withDSA", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.provider.DSAKeyFactory", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.provider.DSAParameters", - "methods":[{"name":"","parameterTypes":[] }] -}, { "name":"sun.security.provider.NativePRNG", "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] @@ -933,59 +887,5 @@ { "name":"sun.security.provider.SHA", "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.provider.SHA2$SHA256", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.provider.X509Factory", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.rsa.RSAKeyFactory$Legacy", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.rsa.RSASignature$SHA256withRSA", - "methods":[{"name":"","parameterTypes":[] }] -}, -{ - "name":"sun.security.util.ObjectIdentifier" -}, -{ - "name":"sun.security.x509.AuthorityInfoAccessExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.AuthorityKeyIdentifierExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.BasicConstraintsExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.CRLDistributionPointsExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.CertificateExtensions" -}, -{ - "name":"sun.security.x509.CertificatePoliciesExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.ExtendedKeyUsageExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.KeyUsageExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, -{ - "name":"sun.security.x509.SubjectKeyIdentifierExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] } ] \ No newline at end of file diff --git a/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json index af650dc9..3c66a520 100644 --- a/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json +++ b/pgpainless-cli/src/main/resources/META-INF/native-image/resource-config.json @@ -40,8 +40,6 @@ "pattern":"\\Qsop-java-version.properties\\E" }, { "pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E" - }, { - "pattern":"java.base:\\Qsun/text/resources/nfkc.icu\\E" }]}, "bundles":[{ "name":"msg_armor", diff --git a/version.gradle b/version.gradle index 003de912..bc2515a4 100644 --- a/version.gradle +++ b/version.gradle @@ -7,7 +7,7 @@ allprojects { shortVersion = '1.7.7' isSnapshot = true javaSourceCompatibility = 11 - bouncyCastleVersion = '1.80' + bouncyCastleVersion = '1.81' bouncyPgVersion = bouncyCastleVersion junitVersion = '5.8.2' logbackVersion = '1.5.13' From 4cf6c6b16a2f8d352fe54d3b0276eff936caeb03 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 10:42:50 +0200 Subject: [PATCH 346/351] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f63d6e..13c93c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.7.7-SNAPSHOT +- Bump `bcpg-jdk8on` to `1.81` +- Bump `bcprov-jdk18on` to `1.81` + ## 1.7.6 - Fix `RevocationSignatureBuilder` properly calculating third-party signatures of type `KeyRevocation` (delegation revocations) - Enable support for native images From f2cbde43bee0da523717b65f10f95defc4ad6f60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Jul 2025 10:54:06 +0200 Subject: [PATCH 347/351] Update codeql action to v3 --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 031ba56d..d45b16a3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'java' ] + language: [ 'java-kotlin' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support @@ -46,7 +46,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -71,4 +71,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 0ee31b232ac7b4beda0940bcbd52fc078209dfc4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:23:34 +0200 Subject: [PATCH 348/351] Allow UserIDs with trailing/leading whitespace and escape newlines in ASCII armor --- .../org/pgpainless/key/generation/KeyRingBuilder.kt | 2 +- .../modification/secretkeyring/SecretKeyRingEditor.kt | 7 ++++--- .../src/main/kotlin/org/pgpainless/util/ArmorUtils.kt | 3 ++- .../test/java/org/pgpainless/sop/GenerateKeyTest.java | 10 ++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt index 05adf7d9..aacfcceb 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/generation/KeyRingBuilder.kt @@ -45,7 +45,7 @@ class KeyRingBuilder : KeyRingBuilderInterface { } override fun addUserId(userId: CharSequence): KeyRingBuilder = apply { - userIds[userId.toString().trim()] = null + userIds[userId.toString()] = null } override fun addUserId(userId: ByteArray): KeyRingBuilder = diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index ec93c6d6..56fb7695 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -569,9 +569,10 @@ class SecretKeyRingEditor( } private fun sanitizeUserId(userId: CharSequence): CharSequence = - // TODO: Further research how to sanitize user IDs. - // e.g. what about newlines? - userId.toString().trim() + // I'm not sure, what kind of sanitization is needed. + // Newlines are allowed, they just need to be escaped when emitted in an ASCII armor header + // Trailing/Leading whitespace is also fine. + userId.toString() private fun callbackFromRevocationAttributes(attributes: RevocationAttributes?) = object : RevocationSignatureSubpackets.Callback { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index b5d5b839..b6e802b2 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -247,7 +247,8 @@ class ArmorUtils { .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) // Primary / First User ID (primary ?: first)?.let { - headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() }.add(it) + headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() } + .add(it.replace("\n", "\\n").replace("\r", "\\r")) } // X-1 further identities when (userIds.size) { diff --git a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java index ca6df790..521cdfe0 100644 --- a/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java +++ b/pgpainless-sop/src/test/java/org/pgpainless/sop/GenerateKeyTest.java @@ -100,4 +100,14 @@ public class GenerateKeyTest { assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop.generateKey().profile("invalid")); } + + @Test + public void generateKeyWithNewlinesInUserId() throws IOException { + byte[] keyBytes = sop.generateKey() + .userId("Foo\n\nBar") + .generate() + .getBytes(); + + assertTrue(new String(keyBytes).contains("Foo\\n\\nBar")); + } } From 9b0a3cd4c7ffe8866ab9622fde866b79cd34a62b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:24:10 +0200 Subject: [PATCH 349/351] Do not trim passphrases automatically --- .../main/kotlin/org/pgpainless/util/Passphrase.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt index 4d1e49d2..bd25f2b9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/Passphrase.kt @@ -11,14 +11,9 @@ import org.bouncycastle.util.Arrays * * @param chars may be null for empty passwords. */ -class Passphrase(chars: CharArray?) { +class Passphrase(private val chars: CharArray?) { private val lock = Any() private var valid = true - private val chars: CharArray? - - init { - this.chars = trimWhitespace(chars) - } /** * Return a copy of the underlying char array. A return value of null represents an empty @@ -67,6 +62,13 @@ class Passphrase(chars: CharArray?) { override fun hashCode(): Int = getChars()?.let { String(it) }.hashCode() + /** + * Return a copy of this [Passphrase], but with whitespace characters trimmed off. + * + * @return copy with trimmed whitespace + */ + fun withTrimmedWhitespace(): Passphrase = Passphrase(trimWhitespace(chars)) + companion object { /** From 0d807cb6b8317f2e878509b5cb5f4a9a91e5287d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Jul 2025 11:25:31 +0200 Subject: [PATCH 350/351] Fix typo in error message --- .../key/modification/secretkeyring/SecretKeyRingEditor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt index 56fb7695..5480442d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.kt @@ -478,7 +478,7 @@ class SecretKeyRingEditor( val prevBinding = inspectKeyRing(secretKeyRing).getCurrentSubkeyBindingSignature(keyId) ?: throw NoSuchElementException( - "Previous subkey binding signaure for ${keyId.openPgpKeyId()} MUST NOT be null.") + "Previous subkey binding signature for ${keyId.openPgpKeyId()} MUST NOT be null.") val bindingSig = reissueSubkeyBindingSignature(subkey, expiration, protector, prevBinding) secretKeyRing = injectCertification(secretKeyRing, subkey, bindingSig) } From 0fe3a7abf660ee7814e87255c13b5775030e73f9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Aug 2025 10:36:40 +0200 Subject: [PATCH 351/351] Fix ArmorUtils spotless complaint --- .../src/main/kotlin/org/pgpainless/util/ArmorUtils.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index b6e802b2..db1cb54d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -247,7 +247,8 @@ class ArmorUtils { .add(OpenPgpFingerprint.of(publicKey).prettyPrint()) // Primary / First User ID (primary ?: first)?.let { - headerMap.getOrPut(HEADER_COMMENT) { mutableSetOf() } + headerMap + .getOrPut(HEADER_COMMENT) { mutableSetOf() } .add(it.replace("\n", "\\n").replace("\r", "\\r")) } // X-1 further identities

- * After making the desired changes in the builder, the modified key can be extracted using {@link SecretKeyRingEditorInterface#done()}. + * After making the desired changes in the builder, the modified key can be extracted using + * {@link SecretKeyRingEditorInterface#done()}. * * @param secretKeys secret key ring * @param referenceTime reference time used as signature creation date @@ -141,11 +138,12 @@ class PGPainless private constructor() { @JvmStatic @JvmOverloads fun modifyKeyRing(secretKey: PGPSecretKeyRing, referenceTime: Date = Date()) = - SecretKeyRingEditor(secretKey, referenceTime) + SecretKeyRingEditor(secretKey, referenceTime) /** - * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / [PGPSecretKeyRing]. - * This method can be used to determine expiration dates, key flags and other information about a key at a specific time. + * Quickly access information about a [org.bouncycastle.openpgp.PGPPublicKeyRing] / + * [PGPSecretKeyRing]. This method can be used to determine expiration dates, key flags and + * other information about a key at a specific time. * * @param keyRing key ring * @param referenceTime date of inspection @@ -154,22 +152,20 @@ class PGPainless private constructor() { @JvmStatic @JvmOverloads fun inspectKeyRing(key: PGPKeyRing, referenceTime: Date = Date()) = - KeyRingInfo(key, referenceTime) + KeyRingInfo(key, referenceTime) /** * Access, and make changes to PGPainless policy on acceptable/default algorithms etc. * * @return policy */ - @JvmStatic - fun getPolicy() = Policy.getInstance() + @JvmStatic fun getPolicy() = Policy.getInstance() /** * Create different kinds of signatures on other keys. * * @return builder */ - @JvmStatic - fun certify() = CertifyCertificate() + @JvmStatic fun certify() = CertifyCertificate() } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt index 61672122..253b37dd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AEADAlgorithm.kt @@ -4,10 +4,7 @@ package org.pgpainless.algorithm -enum class AEADAlgorithm( - val algorithmId: Int, - val ivLength: Int, - val tagLength: Int) { +enum class AEADAlgorithm(val algorithmId: Int, val ivLength: Int, val tagLength: Int) { EAX(1, 16, 16), OCB(2, 15, 16), GCM(3, 12, 16), @@ -16,15 +13,12 @@ enum class AEADAlgorithm( companion object { @JvmStatic fun fromId(id: Int): AEADAlgorithm? { - return values().firstOrNull { - algorithm -> algorithm.algorithmId == id - } + return values().firstOrNull { algorithm -> algorithm.algorithmId == id } } @JvmStatic fun requireFromId(id: Int): AEADAlgorithm { - return fromId(id) ?: - throw NoSuchElementException("No AEADAlgorithm found for id $id") + return fromId(id) ?: throw NoSuchElementException("No AEADAlgorithm found for id $id") } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt index 0e4997fc..867bf1b8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/AlgorithmSuite.kt @@ -5,9 +5,10 @@ package org.pgpainless.algorithm class AlgorithmSuite( - symmetricKeyAlgorithms: List, - hashAlgorithms: List, - compressionAlgorithms: List) { + symmetricKeyAlgorithms: List, + hashAlgorithms: List, + compressionAlgorithms: List +) { val symmetricKeyAlgorithms: Set = symmetricKeyAlgorithms.toSet() val hashAlgorithms: Set = hashAlgorithms.toSet() @@ -16,30 +17,31 @@ class AlgorithmSuite( companion object { @JvmStatic - val defaultSymmetricKeyAlgorithms = listOf( + val defaultSymmetricKeyAlgorithms = + listOf( SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128) @JvmStatic - val defaultHashAlgorithms = listOf( + val defaultHashAlgorithms = + listOf( HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224) @JvmStatic - val defaultCompressionAlgorithms = listOf( + val defaultCompressionAlgorithms = + listOf( CompressionAlgorithm.ZLIB, CompressionAlgorithm.BZIP2, CompressionAlgorithm.ZIP, CompressionAlgorithm.UNCOMPRESSED) @JvmStatic - val defaultAlgorithmSuite = AlgorithmSuite( - defaultSymmetricKeyAlgorithms, - defaultHashAlgorithms, - defaultCompressionAlgorithms) + val defaultAlgorithmSuite = + AlgorithmSuite( + defaultSymmetricKeyAlgorithms, defaultHashAlgorithms, defaultCompressionAlgorithms) } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt index 33025ad6..5617109c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CertificationType.kt @@ -4,18 +4,17 @@ package org.pgpainless.algorithm -enum class CertificationType( - val signatureType: SignatureType -) { +enum class CertificationType(val signatureType: SignatureType) { /** - * The issuer of this certification does not make any particular assertion as to how well the certifier has - * checked that the owner of the key is in fact the person described by the User ID. + * The issuer of this certification does not make any particular assertion as to how well the + * certifier has checked that the owner of the key is in fact the person described by the User + * ID. */ GENERIC(SignatureType.GENERIC_CERTIFICATION), /** - * The issuer of this certification has not done any verification of the claim that the owner of this key is - * the User ID specified. + * The issuer of this certification has not done any verification of the claim that the owner of + * this key is the User ID specified. */ NONE(SignatureType.NO_CERTIFICATION), @@ -31,4 +30,4 @@ enum class CertificationType( ; fun asSignatureType() = signatureType -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt index 73179722..da4085bd 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/CompressionAlgorithm.kt @@ -20,22 +20,20 @@ enum class CompressionAlgorithm(val algorithmId: Int) { companion object { /** - * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. - * If an invalid id is provided, null is returned. + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. If + * an invalid id is provided, null is returned. * * @param id id * @return compression algorithm */ @JvmStatic fun fromId(id: Int): CompressionAlgorithm? { - return values().firstOrNull { - c -> c.algorithmId == id - } + return values().firstOrNull { c -> c.algorithmId == id } } /** - * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. - * If an invalid id is provided, throw an [NoSuchElementException]. + * Return the [CompressionAlgorithm] value that corresponds to the provided numerical id. If + * an invalid id is provided, throw an [NoSuchElementException]. * * @param id id * @return compression algorithm @@ -43,8 +41,8 @@ enum class CompressionAlgorithm(val algorithmId: Int) { */ @JvmStatic fun requireFromId(id: Int): CompressionAlgorithm { - return fromId(id) ?: - throw NoSuchElementException("No CompressionAlgorithm found for id $id") + return fromId(id) + ?: throw NoSuchElementException("No CompressionAlgorithm found for id $id") } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt index b3cd17ac..e41a4605 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/DocumentSignatureType.kt @@ -6,15 +6,11 @@ package org.pgpainless.algorithm enum class DocumentSignatureType(val signatureType: SignatureType) { - /** - * Signature is calculated over the unchanged binary data. - */ + /** Signature is calculated over the unchanged binary data. */ BINARY_DOCUMENT(SignatureType.BINARY_DOCUMENT), /** - * The signature is calculated over the text data with its line endings - * converted to ``. + * The signature is calculated over the text data with its line endings converted to ``. */ CANONICAL_TEXT_DOCUMENT(SignatureType.CANONICAL_TEXT_DOCUMENT), - ; -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt index f7e5ce2d..1b4bbe6e 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/EncryptionPurpose.kt @@ -5,18 +5,10 @@ package org.pgpainless.algorithm enum class EncryptionPurpose { - /** - * The stream will encrypt communication that goes over the wire. - * E.g. EMail, Chat... - */ + /** The stream will encrypt communication that goes over the wire. E.g. EMail, Chat... */ COMMUNICATIONS, - /** - * The stream will encrypt data at rest. - * E.g. Encrypted backup... - */ + /** The stream will encrypt data at rest. E.g. Encrypted backup... */ STORAGE, - /** - * The stream will use keys with either flags to encrypt the data. - */ + /** The stream will use keys with either flags to encrypt the data. */ ANY -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt index 2e0058b5..3f9be1f5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Feature.kt @@ -12,42 +12,44 @@ package org.pgpainless.algorithm enum class Feature(val featureId: Byte) { /** - * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using Modification - * Detection Code Packets. + * Support for Symmetrically Encrypted Integrity Protected Data Packets (version 1) using + * Modification Detection Code Packets. * - * See [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14) + * See + * [RFC-4880 §5.14: Modification Detection Code Packet](https://tools.ietf.org/html/rfc4880#section-5.14) */ MODIFICATION_DETECTION(0x01), /** - * Support for Authenticated Encryption with Additional Data (AEAD). - * If a key announces this feature, it signals support for consuming AEAD Encrypted Data Packets. + * Support for Authenticated Encryption with Additional Data (AEAD). If a key announces this + * feature, it signals support for consuming AEAD Encrypted Data Packets. * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * NOTE: This value is currently RESERVED. + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. * - * See [AEAD Encrypted Data Packet](https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-) + * See + * [AEAD Encrypted Data Packet](https://openpgp-wg.gitlab.io/rfc4880bis/#name-aead-encrypted-data-packet-) */ GNUPG_AEAD_ENCRYPTED_DATA(0x02), /** - * If a key announces this feature, it is a version 5 public key. - * The version 5 format is similar to the version 4 format except for the addition of a count for the key material. - * This count helps to parse secret key packets (which are an extension of the public key packet format) in the case - * of an unknown algorithm. - * In addition, fingerprints of version 5 keys are calculated differently from version 4 keys. + * If a key announces this feature, it is a version 5 public key. The version 5 format is + * similar to the version 4 format except for the addition of a count for the key material. This + * count helps to parse secret key packets (which are an extension of the public key packet + * format) in the case of an unknown algorithm. In addition, fingerprints of version 5 keys are + * calculated differently from version 4 keys. * - * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! - * NOTE: This value is currently RESERVED. + * NOTE: PGPAINLESS DOES NOT YET SUPPORT THIS FEATURE!!! NOTE: This value is currently RESERVED. * - * See [Public-Key Packet Formats](https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats) + * See + * [Public-Key Packet Formats](https://openpgp-wg.gitlab.io/rfc4880bis/#name-public-key-packet-formats) */ GNUPG_VERSION_5_PUBLIC_KEY(0x04), /** * Support for Symmetrically Encrypted Integrity Protected Data packet version 2. * - * See [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd) + * See + * [crypto-refresh-06 §5.13.2. Version 2 Sym. Encrypted Integrity Protected Data Packet Format](https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-06.html#version-two-seipd) */ MODIFICATION_DETECTION_2(0x08), ; @@ -55,29 +57,26 @@ enum class Feature(val featureId: Byte) { companion object { @JvmStatic fun fromId(id: Byte): Feature? { - return values().firstOrNull { - f -> f.featureId == id - } + return values().firstOrNull { f -> f.featureId == id } } @JvmStatic fun requireFromId(id: Byte): Feature { - return fromId(id) ?: - throw NoSuchElementException("Unknown feature id encountered: $id") + return fromId(id) ?: throw NoSuchElementException("Unknown feature id encountered: $id") } @JvmStatic fun fromBitmask(bitmask: Int): List { - return values().filter { - it.featureId.toInt() and bitmask != 0 - } + return values().filter { it.featureId.toInt() and bitmask != 0 } } @JvmStatic fun toBitmask(vararg features: Feature): Byte { - return features.map { it.featureId.toInt() } - .reduceOrNull { mask, f -> mask or f }?.toByte() - ?: 0 + return features + .map { it.featureId.toInt() } + .reduceOrNull { mask, f -> mask or f } + ?.toByte() + ?: 0 } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt index 9c433efe..3360e7fe 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/HashAlgorithm.kt @@ -11,36 +11,33 @@ package org.pgpainless.algorithm */ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { - @Deprecated("MD5 is deprecated") - MD5 (1, "MD5"), - SHA1 (2, "SHA1"), - RIPEMD160 (3, "RIPEMD160"), - SHA256 (8, "SHA256"), - SHA384 (9, "SHA384"), - SHA512 (10, "SHA512"), - SHA224 (11, "SHA224"), - SHA3_256 (12, "SHA3-256"), - SHA3_512 (14, "SHA3-512"), + @Deprecated("MD5 is deprecated") MD5(1, "MD5"), + SHA1(2, "SHA1"), + RIPEMD160(3, "RIPEMD160"), + SHA256(8, "SHA256"), + SHA384(9, "SHA384"), + SHA512(10, "SHA512"), + SHA224(11, "SHA224"), + SHA3_256(12, "SHA3-256"), + SHA3_512(14, "SHA3-512"), ; companion object { /** - * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, null is returned. + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. If an + * invalid algorithm id was provided, null is returned. * * @param id numeric id * @return enum value */ @JvmStatic fun fromId(id: Int): HashAlgorithm? { - return values().firstOrNull { - h -> h.algorithmId == id - } + return values().firstOrNull { h -> h.algorithmId == id } } /** - * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. - * If an invalid algorithm id was provided, throw a [NoSuchElementException]. + * Return the [HashAlgorithm] value that corresponds to the provided algorithm id. If an + * invalid algorithm id was provided, throw a [NoSuchElementException]. * * @param id algorithm id * @return enum value @@ -48,15 +45,15 @@ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { */ @JvmStatic fun requireFromId(id: Int): HashAlgorithm { - return fromId(id) ?: - throw NoSuchElementException("No HashAlgorithm found for id $id") + return fromId(id) ?: throw NoSuchElementException("No HashAlgorithm found for id $id") } /** - * Return the [HashAlgorithm] value that corresponds to the provided name. - * If an invalid algorithm name was provided, null is returned. + * Return the [HashAlgorithm] value that corresponds to the provided name. If an invalid + * algorithm name was provided, null is returned. * - * See [RFC4880: §9.4 Hash Algorithms](https://datatracker.ietf.org/doc/html/rfc4880#section-9.4) + * See + * [RFC4880: §9.4 Hash Algorithms](https://datatracker.ietf.org/doc/html/rfc4880#section-9.4) * for a list of algorithms and names. * * @param name text name @@ -65,12 +62,9 @@ enum class HashAlgorithm(val algorithmId: Int, val algorithmName: String) { @JvmStatic fun fromName(name: String): HashAlgorithm? { return name.uppercase().let { algoName -> - values().firstOrNull { - it.algorithmName == algoName - } ?: values().firstOrNull { - it.algorithmName == algoName.replace("-", "") - } + values().firstOrNull { it.algorithmName == algoName } + ?: values().firstOrNull { it.algorithmName == algoName.replace("-", "") } } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt index d8686cd6..b8bc2d96 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/KeyFlag.kt @@ -6,40 +6,26 @@ package org.pgpainless.algorithm enum class KeyFlag(val flag: Int) { - /** - * This key may be used to certify third-party keys. - */ - CERTIFY_OTHER (1), + /** This key may be used to certify third-party keys. */ + CERTIFY_OTHER(1), - /** - * This key may be used to sign data. - */ - SIGN_DATA (2), + /** This key may be used to sign data. */ + SIGN_DATA(2), - /** - * This key may be used to encrypt communications. - */ - ENCRYPT_COMMS (4), + /** This key may be used to encrypt communications. */ + ENCRYPT_COMMS(4), - /** - * This key may be used to encrypt storage. - */ + /** This key may be used to encrypt storage. */ ENCRYPT_STORAGE(8), - /** - * The private component of this key may have been split by a secret-sharing mechanism. - */ - SPLIT (16), + /** The private component of this key may have been split by a secret-sharing mechanism. */ + SPLIT(16), - /** - * This key may be used for authentication. - */ - AUTHENTICATION (32), + /** This key may be used for authentication. */ + AUTHENTICATION(32), - /** - * The private component of this key may be in the possession of more than one person. - */ - SHARED (128), + /** The private component of this key may be in the possession of more than one person. */ + SHARED(128), ; companion object { @@ -52,9 +38,7 @@ enum class KeyFlag(val flag: Int) { */ @JvmStatic fun fromBitmask(bitmask: Int): List { - return values().filter { - it.flag and bitmask != 0 - } + return values().filter { it.flag and bitmask != 0 } } /** @@ -65,13 +49,12 @@ enum class KeyFlag(val flag: Int) { */ @JvmStatic fun toBitmask(vararg flags: KeyFlag): Int { - return flags.map { it.flag }.reduceOrNull { mask, f -> mask or f } - ?: 0 + return flags.map { it.flag }.reduceOrNull { mask, f -> mask or f } ?: 0 } /** - * Return true if the provided bitmask has the bit for the provided flag set. - * Return false if the mask does not contain the flag. + * Return true if the provided bitmask has the bit for the provided flag set. Return false + * if the mask does not contain the flag. * * @param mask bitmask * @param flag flag to be tested for @@ -84,9 +67,7 @@ enum class KeyFlag(val flag: Int) { @JvmStatic fun containsAny(mask: Int, vararg flags: KeyFlag): Boolean { - return flags.any { - hasKeyFlag(mask, it) - } + return flags.any { hasKeyFlag(mask, it) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt index f05599bd..ecb8db35 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/OpenPgpPacket.kt @@ -22,7 +22,6 @@ enum class OpenPgpPacket(val tag: Int) { UATTR(17), SEIPD(18), MDC(19), - EXP_1(60), EXP_2(61), EXP_3(62), @@ -32,15 +31,13 @@ enum class OpenPgpPacket(val tag: Int) { companion object { @JvmStatic fun fromTag(tag: Int): OpenPgpPacket? { - return values().firstOrNull { - it.tag == tag - } + return values().firstOrNull { it.tag == tag } } @JvmStatic fun requireFromTag(tag: Int): OpenPgpPacket { - return fromTag(tag) ?: - throw NoSuchElementException("No OpenPGP packet known for tag $tag") + return fromTag(tag) + ?: throw NoSuchElementException("No OpenPGP packet known for tag $tag") } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt index 9fad8e7d..4a218673 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/PublicKeyAlgorithm.kt @@ -10,87 +10,73 @@ package org.pgpainless.algorithm * See [RFC4880: Public-Key Algorithms](https://tools.ietf.org/html/rfc4880#section-9.1) */ enum class PublicKeyAlgorithm( - val algorithmId: Int, - val signingCapable: Boolean, - val encryptionCapable: Boolean) { + val algorithmId: Int, + val signingCapable: Boolean, + val encryptionCapable: Boolean +) { - /** - * RSA capable of encryption and signatures. - */ - RSA_GENERAL (1, true, true), + /** RSA capable of encryption and signatures. */ + RSA_GENERAL(1, true, true), /** * RSA with usage encryption. * - * @deprecated see Deprecation notice + * @deprecated see Deprecation + * notice */ - @Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", - ReplaceWith("RSA_GENERAL")) - RSA_ENCRYPT (2, false, true), + @Deprecated("RSA_ENCRYPT is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) + RSA_ENCRYPT(2, false, true), /** * RSA with usage of creating signatures. * - * @deprecated see Deprecation notice + * @deprecated see Deprecation + * notice */ - @Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", - ReplaceWith("RSA_GENERAL")) - RSA_SIGN (3, true, false), + @Deprecated("RSA_SIGN is deprecated in favor of RSA_GENERAL", ReplaceWith("RSA_GENERAL")) + RSA_SIGN(3, true, false), - /** - * ElGamal with usage encryption. - */ - ELGAMAL_ENCRYPT (16, false, true), + /** ElGamal with usage encryption. */ + ELGAMAL_ENCRYPT(16, false, true), - /** - * Digital Signature Algorithm. - */ - DSA (17, true, false), + /** Digital Signature Algorithm. */ + DSA(17, true, false), - /** - * Elliptic Curve Diffie-Hellman. - */ - ECDH (18, false, true), + /** Elliptic Curve Diffie-Hellman. */ + ECDH(18, false, true), - /** - * Elliptic Curve Digital Signature Algorithm. - */ - ECDSA (19, true, false), + /** Elliptic Curve Digital Signature Algorithm. */ + ECDSA(19, true, false), /** * ElGamal General. * - * @deprecated see Deprecation notice + * @deprecated see Deprecation + * notice */ - @Deprecated("ElGamal is deprecated") - ELGAMAL_GENERAL (20, true, true), + @Deprecated("ElGamal is deprecated") ELGAMAL_GENERAL(20, true, true), - /** - * Diffie-Hellman key exchange algorithm. - */ - DIFFIE_HELLMAN (21, false, true), + /** Diffie-Hellman key exchange algorithm. */ + DIFFIE_HELLMAN(21, false, true), - /** - * Digital Signature Algorithm based on twisted Edwards Curves. - */ - EDDSA (22, true, false), + /** Digital Signature Algorithm based on twisted Edwards Curves. */ + EDDSA(22, true, false), ; fun isSigningCapable(): Boolean = signingCapable + fun isEncryptionCapable(): Boolean = encryptionCapable companion object { @JvmStatic fun fromId(id: Int): PublicKeyAlgorithm? { - return values().firstOrNull { - it.algorithmId == id - } + return values().firstOrNull { it.algorithmId == id } } @JvmStatic fun requireFromId(id: Int): PublicKeyAlgorithm { - return fromId(id) ?: - throw NoSuchElementException("No PublicKeyAlgorithm found for id $id") + return fromId(id) + ?: throw NoSuchElementException("No PublicKeyAlgorithm found for id $id") } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt index d5d95d45..ba288469 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationState.kt @@ -4,53 +4,48 @@ package org.pgpainless.algorithm -import org.pgpainless.util.DateUtil import java.lang.AssertionError import java.util.* import kotlin.NoSuchElementException +import org.pgpainless.util.DateUtil -class RevocationState private constructor( - val type: RevocationStateType, - private val _date: Date?): Comparable { +class RevocationState private constructor(val type: RevocationStateType, private val _date: Date?) : + Comparable { val date: Date get() { if (!isSoftRevocation()) { - throw NoSuchElementException("RevocationStateType is not equal to 'softRevoked'. Cannot extract date.") + throw NoSuchElementException( + "RevocationStateType is not equal to 'softRevoked'. Cannot extract date.") } return _date!! } - private constructor(type: RevocationStateType): this(type, null) + private constructor(type: RevocationStateType) : this(type, null) fun isSoftRevocation() = type == RevocationStateType.softRevoked + fun isHardRevocation() = type == RevocationStateType.hardRevoked + fun isNotRevoked() = type == RevocationStateType.notRevoked companion object { - @JvmStatic - fun notRevoked() = RevocationState(RevocationStateType.notRevoked) + @JvmStatic fun notRevoked() = RevocationState(RevocationStateType.notRevoked) @JvmStatic fun softRevoked(date: Date) = RevocationState(RevocationStateType.softRevoked, date) - @JvmStatic - fun hardRevoked() = RevocationState(RevocationStateType.hardRevoked) + @JvmStatic fun hardRevoked() = RevocationState(RevocationStateType.hardRevoked) } override fun compareTo(other: RevocationState): Int { - return when(type) { - RevocationStateType.notRevoked -> - if (other.isNotRevoked()) 0 - else -1 + return when (type) { + RevocationStateType.notRevoked -> if (other.isNotRevoked()) 0 else -1 RevocationStateType.softRevoked -> if (other.isNotRevoked()) 1 // Compare soft dates in reverse - else if (other.isSoftRevocation()) other.date.compareTo(date) - else -1 - RevocationStateType.hardRevoked -> - if (other.isHardRevocation()) 0 - else 1 + else if (other.isSoftRevocation()) other.date.compareTo(date) else -1 + RevocationStateType.hardRevoked -> if (other.isHardRevocation()) 0 else 1 else -> throw AssertionError("Unknown type: $type") } } @@ -80,8 +75,9 @@ class RevocationState private constructor( return false } if (isSoftRevocation()) { - return DateUtil.toSecondsPrecision(date).time == DateUtil.toSecondsPrecision(other.date).time + return DateUtil.toSecondsPrecision(date).time == + DateUtil.toSecondsPrecision(other.date).time } return true } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt index bf18e27f..1f95ad7b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/RevocationStateType.kt @@ -5,18 +5,12 @@ package org.pgpainless.algorithm enum class RevocationStateType { - /** - * Certificate is not revoked. - */ + /** Certificate is not revoked. */ notRevoked, - /** - * Certificate is revoked with a soft revocation. - */ + /** Certificate is revoked with a soft revocation. */ softRevoked, - /** - * Certificate is revoked with a hard revocation. - */ + /** Certificate is revoked with a hard revocation. */ hardRevoked -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt index fcb0c90a..a0efd618 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureSubpacket.kt @@ -7,23 +7,24 @@ package org.pgpainless.algorithm import org.bouncycastle.bcpg.SignatureSubpacketTags.* /** - * Enumeration of possible subpackets that might be found in the hashed and unhashed area of an OpenPGP signature. + * Enumeration of possible subpackets that might be found in the hashed and unhashed area of an + * OpenPGP signature. * - * See [RFC4880: Signature Subpacket Specification](https://tools.ietf.org/html/rfc4880#section-5.2.3.1) + * See + * [RFC4880: Signature Subpacket Specification](https://tools.ietf.org/html/rfc4880#section-5.2.3.1) */ enum class SignatureSubpacket(val code: Int) { /** - * The time the signature was made. - * MUST be present in the hashed area of the signature. + * The time the signature was made. MUST be present in the hashed area of the signature. * * See [Signature Creation Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.4) */ signatureCreationTime(2), /** - * The validity period of the signature. This is the number of seconds - * after the signature creation time that the signature expires. If - * this is not present or has a value of zero, it never expires. + * The validity period of the signature. This is the number of seconds after the signature + * creation time that the signature expires. If this is not present or has a value of zero, it + * never expires. * * See [Signature Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.10) */ @@ -37,90 +38,76 @@ enum class SignatureSubpacket(val code: Int) { exportableCertification(4), /** - * Signer asserts that the key is not only valid but also trustworthy at - * the specified level. Level 0 has the same meaning as an ordinary - * validity signature. Level 1 means that the signed key is asserted to - * be a valid, trusted introducer, with the 2nd octet of the body - * specifying the degree of trust. Level 2 means that the signed key is - * asserted to be trusted to issue level 1 trust signatures, i.e., that - * it is a "meta introducer". Generally, a level n trust signature - * asserts that a key is trusted to issue level n-1 trust signatures. - * The trust amount is in a range from 0-255, interpreted such that - * values less than 120 indicate partial trust and values of 120 or - * greater indicate complete trust. Implementations SHOULD emit values - * of 60 for partial trust and 120 for complete trust. + * Signer asserts that the key is not only valid but also trustworthy at the specified level. + * Level 0 has the same meaning as an ordinary validity signature. Level 1 means that the signed + * key is asserted to be a valid, trusted introducer, with the 2nd octet of the body specifying + * the degree of trust. Level 2 means that the signed key is asserted to be trusted to issue + * level 1 trust signatures, i.e., that it is a "meta introducer". Generally, a level n trust + * signature asserts that a key is trusted to issue level n-1 trust signatures. The trust amount + * is in a range from 0-255, interpreted such that values less than 120 indicate partial trust + * and values of 120 or greater indicate complete trust. Implementations SHOULD emit values of + * 60 for partial trust and 120 for complete trust. * * See [Trust Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.13) */ trustSignature(5), /** - * Used in conjunction with trust Signature packets (of level greater 0) to - * limit the scope of trust that is extended. Only signatures by the - * target key on User IDs that match the regular expression in the body - * of this packet have trust extended by the trust Signature subpacket. - * The regular expression uses the same syntax as the Henry Spencer's - * "almost public domain" regular expression [REGEX] package. A - * description of the syntax is found in Section 8 below. + * Used in conjunction with trust Signature packets (of level greater 0) to limit the scope of + * trust that is extended. Only signatures by the target key on User IDs that match the regular + * expression in the body of this packet have trust extended by the trust Signature subpacket. + * The regular expression uses the same syntax as the Henry Spencer's "almost public domain" + * regular expression [REGEX] package. A description of the syntax is found in Section 8 below. * * See [Regular Expression](https://tools.ietf.org/html/rfc4880#section-5.2.3.14) */ regularExpression(6), /** - * Signature's revocability status. The packet body contains a Boolean - * flag indicating whether the signature is revocable. Signatures that - * are not revocable have any later revocation signatures ignored. They - * represent a commitment by the signer that he cannot revoke his - * signature for the life of his key. If this packet is not present, - * the signature is revocable. + * Signature's revocability status. The packet body contains a Boolean flag indicating whether + * the signature is revocable. Signatures that are not revocable have any later revocation + * signatures ignored. They represent a commitment by the signer that he cannot revoke his + * signature for the life of his key. If this packet is not present, the signature is revocable. * * See [Revocable](https://tools.ietf.org/html/rfc4880#section-5.2.3.12) */ revocable(7), /** - * The validity period of the key. This is the number of seconds after - * the key creation time that the key expires. If this is not present - * or has a value of zero, the key never expires. This is found only on - * a self-signature. + * The validity period of the key. This is the number of seconds after the key creation time + * that the key expires. If this is not present or has a value of zero, the key never expires. + * This is found only on a self-signature. * * See [Key Expiration Time](https://tools.ietf.org/html/rfc4880#section-5.2.3.6) */ keyExpirationTime(9), - /** - * Placeholder for backwards compatibility. - */ + /** Placeholder for backwards compatibility. */ placeholder(10), /** - * Symmetric algorithm numbers that indicate which algorithms the keyholder - * prefers to use. The subpackets body is an ordered list of - * octets with the most preferred listed first. It is assumed that only - * algorithms listed are supported by the recipient's software. - * This is only found on a self-signature. + * Symmetric algorithm numbers that indicate which algorithms the keyholder prefers to use. The + * subpackets body is an ordered list of octets with the most preferred listed first. It is + * assumed that only algorithms listed are supported by the recipient's software. This is only + * found on a self-signature. * * See [Preferred Symmetric Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.7) */ preferredSymmetricAlgorithms(11), /** - * Authorizes the specified key to issue revocation signatures for this - * key. Class octet must have bit 0x80 set. If the bit 0x40 is set, - * then this means that the revocation information is sensitive. Other - * bits are for future expansion to other kinds of authorizations. This - * is found on a self-signature. + * Authorizes the specified key to issue revocation signatures for this key. Class octet must + * have bit 0x80 set. If the bit 0x40 is set, then this means that the revocation information is + * sensitive. Other bits are for future expansion to other kinds of authorizations. This is + * found on a self-signature. * - * If the "sensitive" flag is set, the keyholder feels this subpacket - * contains private trust information that describes a real-world - * sensitive relationship. If this flag is set, implementations SHOULD - * NOT export this signature to other users except in cases where the - * data needs to be available: when the signature is being sent to the - * designated revoker, or when it is accompanied by a revocation - * signature from that revoker. Note that it may be appropriate to - * isolate this subpacket within a separate signature so that it is not - * combined with other subpackets that need to be exported. + * If the "sensitive" flag is set, the keyholder feels this subpacket contains private trust + * information that describes a real-world sensitive relationship. If this flag is set, + * implementations SHOULD NOT export this signature to other users except in cases where the + * data needs to be available: when the signature is being sent to the designated revoker, or + * when it is accompanied by a revocation signature from that revoker. Note that it may be + * appropriate to isolate this subpacket within a separate signature so that it is not combined + * with other subpackets that need to be exported. * * See [Revocation Key](https://tools.ietf.org/html/rfc4880#section-5.2.3.15) */ @@ -134,54 +121,48 @@ enum class SignatureSubpacket(val code: Int) { issuerKeyId(16), /** - * This subpacket describes a "notation" on the signature that the - * issuer wishes to make. The notation has a name and a value, each of - * which are strings of octets. There may be more than one notation in - * a signature. Notations can be used for any extension the issuer of - * the signature cares to make. The "flags" field holds four octets of - * flags. + * This subpacket describes a "notation" on the signature that the issuer wishes to make. The + * notation has a name and a value, each of which are strings of octets. There may be more than + * one notation in a signature. Notations can be used for any extension the issuer of the + * signature cares to make. The "flags" field holds four octets of flags. * * See [Notation Data](https://tools.ietf.org/html/rfc4880#section-5.2.3.16) */ notationData(20), /** - * Message digest algorithm numbers that indicate which algorithms the - * keyholder prefers to receive. Like the preferred symmetric - * algorithms, the list is ordered. - * This is only found on a self-signature. + * Message digest algorithm numbers that indicate which algorithms the keyholder prefers to + * receive. Like the preferred symmetric algorithms, the list is ordered. This is only found on + * a self-signature. * * See [Preferred Hash Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.8) */ preferredHashAlgorithms(21), /** - * Compression algorithm numbers that indicate which algorithms the - * keyholder prefers to use. Like the preferred symmetric algorithms, the - * list is ordered. If this subpacket is not included, ZIP is preferred. - * A zero denotes that uncompressed data is preferred; the keyholder's - * software might have no compression software in that implementation. - * This is only found on a self-signature. + * Compression algorithm numbers that indicate which algorithms the keyholder prefers to use. + * Like the preferred symmetric algorithms, the list is ordered. If this subpacket is not + * included, ZIP is preferred. A zero denotes that uncompressed data is preferred; the + * keyholder's software might have no compression software in that implementation. This is only + * found on a self-signature. * * See [Preferred Compressio Algorithms](https://tools.ietf.org/html/rfc4880#section-5.2.3.9) */ preferredCompressionAlgorithms(22), /** - * This is a list of one-bit flags that indicate preferences that the - * keyholder has about how the key is handled on a key server. All - * undefined flags MUST be zero. - * This is found only on a self-signature. + * This is a list of one-bit flags that indicate preferences that the keyholder has about how + * the key is handled on a key server. All undefined flags MUST be zero. This is found only on a + * self-signature. * * See [Key Server Preferences](https://tools.ietf.org/html/rfc4880#section-5.2.3.17) */ keyServerPreferences(23), /** - * This is a URI of a key server that the keyholder prefers be used for - * updates. Note that keys with multiple User IDs can have a preferred - * key server for each User ID. Note also that since this is a URI, the - * key server can actually be a copy of the key retrieved by ftp, http, + * This is a URI of a key server that the keyholder prefers be used for updates. Note that keys + * with multiple User IDs can have a preferred key server for each User ID. Note also that since + * this is a URI, the key server can actually be a copy of the key retrieved by ftp, http, * finger, etc. * * See [Preferred Key Server](https://tools.ietf.org/html/rfc4880#section-5.2.3.18) @@ -189,63 +170,55 @@ enum class SignatureSubpacket(val code: Int) { preferredKeyServers(24), /** - * This is a flag in a User ID's self-signature that states whether this - * User ID is the main User ID for this key. It is reasonable for an - * implementation to resolve ambiguities in preferences, etc. by - * referring to the primary User ID. If this flag is absent, its value - * is zero. If more than one User ID in a key is marked as primary, the - * implementation may resolve the ambiguity in any way it sees fit, but - * it is RECOMMENDED that priority be given to the User ID with the most - * recent self-signature. + * This is a flag in a User ID's self-signature that states whether this User ID is the main + * User ID for this key. It is reasonable for an implementation to resolve ambiguities in + * preferences, etc. by referring to the primary User ID. If this flag is absent, its value is + * zero. If more than one User ID in a key is marked as primary, the implementation may resolve + * the ambiguity in any way it sees fit, but it is RECOMMENDED that priority be given to the + * User ID with the most recent self-signature. * - * When appearing on a self-signature on a User ID packet, this - * subpacket applies only to User ID packets. When appearing on a - * self-signature on a User Attribute packet, this subpacket applies - * only to User Attribute packets. That is to say, there are two - * different and independent "primaries" -- one for User IDs, and one - * for User Attributes. + * When appearing on a self-signature on a User ID packet, this subpacket applies only to User + * ID packets. When appearing on a self-signature on a User Attribute packet, this subpacket + * applies only to User Attribute packets. That is to say, there are two different and + * independent "primaries" -- one for User IDs, and one for User Attributes. * * See [Primary User-ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.19) */ primaryUserId(25), /** - * This subpacket contains a URI of a document that describes the policy - * under which the signature was issued. + * This subpacket contains a URI of a document that describes the policy under which the + * signature was issued. * * See [Policy URL](https://tools.ietf.org/html/rfc4880#section-5.2.3.20) */ policyUrl(26), /** - * This subpacket contains a list of binary flags that hold information - * about a key. It is a string of octets, and an implementation MUST - * NOT assume a fixed size. This is so it can grow over time. If a - * list is shorter than an implementation expects, the unstated flags - * are considered to be zero. + * This subpacket contains a list of binary flags that hold information about a key. It is a + * string of octets, and an implementation MUST NOT assume a fixed size. This is so it can grow + * over time. If a list is shorter than an implementation expects, the unstated flags are + * considered to be zero. * * See [Key Flags](https://tools.ietf.org/html/rfc4880#section-5.2.3.21) */ keyFlags(27), /** - * This subpacket allows a keyholder to state which User ID is - * responsible for the signing. Many keyholders use a single key for - * different purposes, such as business communications as well as - * personal communications. This subpacket allows such a keyholder to - * state which of their roles is making a signature. + * This subpacket allows a keyholder to state which User ID is responsible for the signing. Many + * keyholders use a single key for different purposes, such as business communications as well + * as personal communications. This subpacket allows such a keyholder to state which of their + * roles is making a signature. * * See [Signer's User ID](https://tools.ietf.org/html/rfc4880#section-5.2.3.22) */ signerUserId(28), /** - * This subpacket is used only in key revocation and certification - * revocation signatures. It describes the reason why the key or - * certificate was revoked. + * This subpacket is used only in key revocation and certification revocation signatures. It + * describes the reason why the key or certificate was revoked. * - * The first octet contains a machine-readable code that denotes the - * reason for the revocation: + * The first octet contains a machine-readable code that denotes the reason for the revocation: * * 0 - No reason specified (key revocations or cert revocations) * 1 - Key is superseded (key revocations) @@ -259,117 +232,105 @@ enum class SignatureSubpacket(val code: Int) { revocationReason(29), /** - * The Features subpacket denotes which advanced OpenPGP features a - * user's implementation supports. This is so that as features are - * added to OpenPGP that cannot be backwards-compatible, a user can - * state that they can use that feature. The flags are single bits that - * indicate that a given feature is supported. + * The Features subpacket denotes which advanced OpenPGP features a user's implementation + * supports. This is so that as features are added to OpenPGP that cannot be + * backwards-compatible, a user can state that they can use that feature. The flags are single + * bits that indicate that a given feature is supported. * - * This subpacket is similar to a preferences subpacket, and only - * appears in a self-signature. + * This subpacket is similar to a preferences subpacket, and only appears in a self-signature. * * See [Features](https://tools.ietf.org/html/rfc4880#section-5.2.3.24) */ features(30), /** - * This subpacket identifies a specific target signature to which a - * signature refers. For revocation signatures, this subpacket - * provides explicit designation of which signature is being revoked. - * For a third-party or timestamp signature, this designates what - * signature is signed. All arguments are an identifier of that target - * signature. + * This subpacket identifies a specific target signature to which a signature refers. For + * revocation signatures, this subpacket provides explicit designation of which signature is + * being revoked. For a third-party or timestamp signature, this designates what signature is + * signed. All arguments are an identifier of that target signature. * - * The N octets of hash data MUST be the size of the hash of the - * signature. For example, a target signature with a SHA-1 hash MUST - * have 20 octets of hash data. + * The N octets of hash data MUST be the size of the hash of the signature. For example, a + * target signature with a SHA-1 hash MUST have 20 octets of hash data. * * See [Signature Target](https://tools.ietf.org/html/rfc4880#section-5.2.3.25) */ signatureTarget(31), /** - * This subpacket contains a complete Signature packet body as - * specified in Section 5.2 above. It is useful when one signature - * needs to refer to, or be incorporated in, another signature. + * This subpacket contains a complete Signature packet body as specified in Section 5.2 above. + * It is useful when one signature needs to refer to, or be incorporated in, another signature. * * See [Embedded Signature](https://tools.ietf.org/html/rfc4880#section-5.2.3.26) */ embeddedSignature(32), /** - * The OpenPGP Key fingerprint of the key issuing the signature. This - * subpacket SHOULD be included in all signatures. If the version of - * the issuing key is 4 and an Issuer subpacket is also included in the - * signature, the key ID of the Issuer subpacket MUST match the low 64 - * bits of the fingerprint. + * The OpenPGP Key fingerprint of the key issuing the signature. This subpacket SHOULD be + * included in all signatures. If the version of the issuing key is 4 and an Issuer subpacket is + * also included in the signature, the key ID of the Issuer subpacket MUST match the low 64 bits + * of the fingerprint. * - * Note that the length N of the fingerprint for a version 4 key is 20 - * octets; for a version 5 key N is 32. + * Note that the length N of the fingerprint for a version 4 key is 20 octets; for a version 5 + * key N is 32. * - * See [Issuer Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28) + * See + * [Issuer Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.28) */ issuerFingerprint(33), /** - * AEAD algorithm numbers that indicate which AEAD algorithms the - * keyholder prefers to use. The subpackets body is an ordered list of - * octets with the most preferred listed first. It is assumed that only - * algorithms listed are supported by the recipient's software. - * This is only found on a self-signature. - * Note that support for the AEAD Encrypted Data packet in the general - * is indicated by a Feature Flag. + * AEAD algorithm numbers that indicate which AEAD algorithms the keyholder prefers to use. The + * subpackets body is an ordered list of octets with the most preferred listed first. It is + * assumed that only algorithms listed are supported by the recipient's software. This is only + * found on a self-signature. Note that support for the AEAD Encrypted Data packet in the + * general is indicated by a Feature Flag. * - * See [Preferred AEAD Algorithms](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8) + * See + * [Preferred AEAD Algorithms](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.8) */ preferredAEADAlgorithms(39), /** - * The OpenPGP Key fingerprint of the intended recipient primary key. - * If one or more subpackets of this type are included in a signature, - * it SHOULD be considered valid only in an encrypted context, where the - * key it was encrypted to is one of the indicated primary keys, or one - * of their subkeys. This can be used to prevent forwarding a signature - * outside its intended, encrypted context. + * The OpenPGP Key fingerprint of the intended recipient primary key. If one or more subpackets + * of this type are included in a signature, it SHOULD be considered valid only in an encrypted + * context, where the key it was encrypted to is one of the indicated primary keys, or one of + * their subkeys. This can be used to prevent forwarding a signature outside its intended, + * encrypted context. * - * Note that the length N of the fingerprint for a version 4 key is 20 - * octets; for a version 5 key N is 32. + * Note that the length N of the fingerprint for a version 4 key is 20 octets; for a version 5 + * key N is 32. * - * See [Intended Recipient Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29) + * See + * [Intended Recipient Fingerprint](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.29) */ intendedRecipientFingerprint(35), /** - * This subpacket MUST only appear as a hashed subpacket of an - * Attestation Key Signature. It has no meaning in any other signature - * type. It is used by the primary key to attest to a set of third- - * party certifications over the associated User ID or User Attribute. - * This enables the holder of an OpenPGP primary key to mark specific - * third-party certifications as re-distributable with the rest of the - * Transferable Public Key (see the "No-modify" flag in "Key Server - * Preferences", above). Implementations MUST include exactly one - * Attested Certification subpacket in any generated Attestation Key - * Signature. + * This subpacket MUST only appear as a hashed subpacket of an Attestation Key Signature. It has + * no meaning in any other signature type. It is used by the primary key to attest to a set of + * third- party certifications over the associated User ID or User Attribute. This enables the + * holder of an OpenPGP primary key to mark specific third-party certifications as + * re-distributable with the rest of the Transferable Public Key (see the "No-modify" flag in + * "Key Server Preferences", above). Implementations MUST include exactly one Attested + * Certification subpacket in any generated Attestation Key Signature. * - * See [Attested Certification](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30) + * See + * [Attested Certification](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10#section-5.2.3.30) */ - attestedCertification(37) - ; - + attestedCertification(37); + companion object { - + /** - * Return the [SignatureSubpacket] that corresponds to the provided id. - * If an unmatched code is presented, return null. + * Return the [SignatureSubpacket] that corresponds to the provided id. If an unmatched code + * is presented, return null. * * @param code id * @return signature subpacket */ @JvmStatic fun fromCode(code: Int): SignatureSubpacket? { - return values().firstOrNull { - it.code == code - } + return values().firstOrNull { it.code == code } } /** @@ -381,21 +342,20 @@ enum class SignatureSubpacket(val code: Int) { */ @JvmStatic fun requireFromCode(code: Int): SignatureSubpacket { - return fromCode(code) ?: - throw NoSuchElementException("No SignatureSubpacket tag found with code $code") + return fromCode(code) + ?: throw NoSuchElementException("No SignatureSubpacket tag found with code $code") } /** - * Convert an array of signature subpacket tags into a list of [SignatureSubpacket SignatureSubpackets]. + * Convert an array of signature subpacket tags into a list of + * [SignatureSubpacket SignatureSubpackets]. * * @param codes array of codes * @return list of subpackets */ @JvmStatic fun fromCodes(vararg codes: Int): List { - return codes.toList().mapNotNull { - fromCode(it) - } + return codes.toList().mapNotNull { fromCode(it) } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt index 1b708a03..865549b8 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SignatureType.kt @@ -7,150 +7,120 @@ package org.pgpainless.algorithm import org.bouncycastle.openpgp.PGPSignature /** - * Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1 - * See [PGPSignature] for comparison. + * Enum that enlists all the Signature Types defined in rfc4880 section 5.2.1 See [PGPSignature] for + * comparison. * * See [rfc4880 §5.2.1. Signature Types](https://tools.ietf.org/html/rfc4880#section-5.11) */ enum class SignatureType(val code: Int) { /** - * Signature of a binary document. - * This means the signer owns it, created it, or certifies that it - * has not been modified. + * Signature of a binary document. This means the signer owns it, created it, or certifies that + * it has not been modified. */ BINARY_DOCUMENT(0x00), /** - * Signature of a canonical text document. - * This means the signer owns it, created it, or certifies that it - * has not been modified. The signature is calculated over the text - * data with its line endings converted to {@code }. + * Signature of a canonical text document. This means the signer owns it, created it, or + * certifies that it has not been modified. The signature is calculated over the text data with + * its line endings converted to {@code }. */ CANONICAL_TEXT_DOCUMENT(0x01), /** - * Standalone signature. - * This signature is a signature of only its own subpacket contents. - * It is calculated identically to a signature over a zero-length - * binary document. Note that it doesn't make sense to have a V3 - * standalone signature. + * Standalone signature. This signature is a signature of only its own subpacket contents. It is + * calculated identically to a signature over a zero-length binary document. Note that it + * doesn't make sense to have a V3 standalone signature. */ STANDALONE(0x02), /** - * Generic certification of a User ID and Public-Key packet. - * The issuer of this certification does not make any particular - * assertion as to how well the certifier has checked that the owner - * of the key is in fact the person described by the User ID. + * Generic certification of a User ID and Public-Key packet. The issuer of this certification + * does not make any particular assertion as to how well the certifier has checked that the + * owner of the key is in fact the person described by the User ID. */ GENERIC_CERTIFICATION(0x10), /** - * Persona certification of a User ID and Public-Key packet. - * The issuer of this certification has not done any verification of - * the claim that the owner of this key is the User ID specified. + * Persona certification of a User ID and Public-Key packet. The issuer of this certification + * has not done any verification of the claim that the owner of this key is the User ID + * specified. */ NO_CERTIFICATION(0x11), /** - * Casual certification of a User ID and Public-Key packet. - * The issuer of this certification has done some casual - * verification of the claim of identity. + * Casual certification of a User ID and Public-Key packet. The issuer of this certification has + * done some casual verification of the claim of identity. */ CASUAL_CERTIFICATION(0x12), /** - * Positive certification of a User ID and Public-Key packet. - * The issuer of this certification has done substantial - * verification of the claim of identity. + * Positive certification of a User ID and Public-Key packet. The issuer of this certification + * has done substantial verification of the claim of identity. */ POSITIVE_CERTIFICATION(0x13), /** - * Subkey Binding Signature. - * This signature is a statement by the top-level signing key that - * indicates that it owns the subkey. This signature is calculated - * directly on the primary key and subkey, and not on any User ID or - * other packets. A signature that binds a signing subkey MUST have - * an Embedded Signature subpacket in this binding signature that - * contains a [#PRIMARYKEY_BINDING] signature made by the - * signing subkey on the primary key and subkey. + * Subkey Binding Signature. This signature is a statement by the top-level signing key that + * indicates that it owns the subkey. This signature is calculated directly on the primary key + * and subkey, and not on any User ID or other packets. A signature that binds a signing subkey + * MUST have an Embedded Signature subpacket in this binding signature that contains a + * [#PRIMARYKEY_BINDING] signature made by the signing subkey on the primary key and subkey. */ SUBKEY_BINDING(0x18), /** - * Primary Key Binding Signature - * This signature is a statement by a signing subkey, indicating - * that it is owned by the primary key and subkey. This signature - * is calculated the same way as a [#SUBKEY_BINDING] signature: - * directly on the primary key and subkey, and not on any User ID or - * other packets. + * Primary Key Binding Signature This signature is a statement by a signing subkey, indicating + * that it is owned by the primary key and subkey. This signature is calculated the same way as + * a [#SUBKEY_BINDING] signature: directly on the primary key and subkey, and not on any User ID + * or other packets. */ PRIMARYKEY_BINDING(0x19), /** - * Signature directly on a key - * This signature is calculated directly on a key. It binds the - * information in the Signature subpackets to the key, and is - * appropriate to be used for subpackets that provide information - * about the key, such as the Revocation Key subpacket. It is also - * appropriate for statements that non-self certifiers want to make - * about the key itself, rather than the binding between a key and a - * name. + * Signature directly on a key This signature is calculated directly on a key. It binds the + * information in the Signature subpackets to the key, and is appropriate to be used for + * subpackets that provide information about the key, such as the Revocation Key subpacket. It + * is also appropriate for statements that non-self certifiers want to make about the key + * itself, rather than the binding between a key and a name. */ DIRECT_KEY(0x1f), /** - * Key revocation signature - * The signature is calculated directly on the key being revoked. A - * revoked key is not to be used. Only revocation signatures by the - * key being revoked, or by an authorized revocation key, should be - * considered valid revocation signatures. + * Key revocation signature The signature is calculated directly on the key being revoked. A + * revoked key is not to be used. Only revocation signatures by the key being revoked, or by an + * authorized revocation key, should be considered valid revocation signatures. */ KEY_REVOCATION(0x20), /** - * Subkey revocation signature - * The signature is calculated directly on the subkey being revoked. - * A revoked subkey is not to be used. Only revocation signatures - * by the top-level signature key that is bound to this subkey, or - * by an authorized revocation key, should be considered valid + * Subkey revocation signature The signature is calculated directly on the subkey being revoked. + * A revoked subkey is not to be used. Only revocation signatures by the top-level signature key + * that is bound to this subkey, or by an authorized revocation key, should be considered valid * revocation signatures. */ SUBKEY_REVOCATION(0x28), /** - * Certification revocation signature - * This signature revokes an earlier User ID certification signature - * (signature class 0x10 through 0x13) or signature [#DIRECT_KEY]. - * It should be issued by the same key that issued the - * revoked signature or an authorized revocation key. The signature - * is computed over the same data as the certificate that it - * revokes, and should have a later creation date than that - * certificate. + * Certification revocation signature This signature revokes an earlier User ID certification + * signature (signature class 0x10 through 0x13) or signature [#DIRECT_KEY]. It should be issued + * by the same key that issued the revoked signature or an authorized revocation key. The + * signature is computed over the same data as the certificate that it revokes, and should have + * a later creation date than that certificate. */ CERTIFICATION_REVOCATION(0x30), - /** - * Timestamp signature. - * This signature is only meaningful for the timestamp contained in - * it. - */ + /** Timestamp signature. This signature is only meaningful for the timestamp contained in it. */ TIMESTAMP(0x40), /** - * Third-Party Confirmation signature. - * This signature is a signature over some other OpenPGP Signature - * packet(s). It is analogous to a notary seal on the signed data. - * A third-party signature SHOULD include Signature Target - * subpacket(s) to give easy identification. Note that we really do - * mean SHOULD. There are plausible uses for this (such as a blind - * party that only sees the signature, not the key or source - * document) that cannot include a target subpacket. + * Third-Party Confirmation signature. This signature is a signature over some other OpenPGP + * Signature packet(s). It is analogous to a notary seal on the signed data. A third-party + * signature SHOULD include Signature Target subpacket(s) to give easy identification. Note that + * we really do mean SHOULD. There are plausible uses for this (such as a blind party that only + * sees the signature, not the key or source document) that cannot include a target subpacket. */ - THIRD_PARTY_CONFIRMATION(0x50) - ; + THIRD_PARTY_CONFIRMATION(0x50); companion object { @@ -162,9 +132,7 @@ enum class SignatureType(val code: Int) { */ @JvmStatic fun fromCode(code: Int): SignatureType? { - return values().firstOrNull { - it.code == code - } + return values().firstOrNull { it.code == code } } /** @@ -176,8 +144,9 @@ enum class SignatureType(val code: Int) { */ @JvmStatic fun requireFromCode(code: Int): SignatureType { - return fromCode(code) ?: - throw NoSuchElementException("Signature type 0x${Integer.toHexString(code)} appears to be invalid.") + return fromCode(code) + ?: throw NoSuchElementException( + "Signature type 0x${Integer.toHexString(code)} appears to be invalid.") } /** @@ -188,8 +157,7 @@ enum class SignatureType(val code: Int) { * @throws IllegalArgumentException in case of an unmatched signature type code */ @JvmStatic - @Deprecated("Deprecated in favor of requireFromCode", - ReplaceWith("requireFromCode")) + @Deprecated("Deprecated in favor of requireFromCode", ReplaceWith("requireFromCode")) fun valueOf(code: Int): SignatureType { try { return requireFromCode(code) @@ -197,12 +165,12 @@ enum class SignatureType(val code: Int) { throw IllegalArgumentException(e.message) } } - + @JvmStatic fun isRevocationSignature(signatureType: Int): Boolean { return isRevocationSignature(valueOf(signatureType)) } - + @JvmStatic fun isRevocationSignature(signatureType: SignatureType): Boolean { return when (signatureType) { @@ -218,11 +186,11 @@ enum class SignatureType(val code: Int) { DIRECT_KEY, TIMESTAMP, THIRD_PARTY_CONFIRMATION -> false - KEY_REVOCATION, - SUBKEY_REVOCATION, + KEY_REVOCATION, + SUBKEY_REVOCATION, CERTIFICATION_REVOCATION -> true else -> throw IllegalArgumentException("Unknown signature type: $signatureType") } } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt index 391797b1..74b2a56b 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/StreamEncoding.kt @@ -11,61 +11,52 @@ package org.pgpainless.algorithm */ enum class StreamEncoding(val code: Char) { - /** - * The Literal packet contains binary data. - */ + /** The Literal packet contains binary data. */ BINARY('b'), /** - * The Literal packet contains text data, and thus may need line ends converted to local form, or other - * text-mode changes. + * The Literal packet contains text data, and thus may need line ends converted to local form, + * or other text-mode changes. */ TEXT('t'), - /** - * Indication that the implementation believes that the literal data contains UTF-8 text. - */ + /** Indication that the implementation believes that the literal data contains UTF-8 text. */ UTF8('u'), /** - * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local conversions. - * RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral one). - * Both of these local modes are deprecated. + * Early versions of PGP also defined a value of 'l' as a 'local' mode for machine-local + * conversions. RFC 1991 [RFC1991] incorrectly stated this local mode flag as '1' (ASCII numeral + * one). Both of these local modes are deprecated. */ - @Deprecated("LOCAL is deprecated.") - LOCAL('l'), + @Deprecated("LOCAL is deprecated.") LOCAL('l'), ; - companion object { /** - * Return the [StreamEncoding] corresponding to the provided code identifier. - * If no matching encoding is found, return null. + * Return the [StreamEncoding] corresponding to the provided code identifier. If no matching + * encoding is found, return null. * * @param code identifier * @return encoding enum */ @JvmStatic fun fromCode(code: Int): StreamEncoding? { - return values().firstOrNull { - it.code == code.toChar() - } ?: if (code == 1) return LOCAL else null + return values().firstOrNull { it.code == code.toChar() } + ?: if (code == 1) return LOCAL else null } /** - * Return the [StreamEncoding] corresponding to the provided code identifier. - * If no matching encoding is found, throw a [NoSuchElementException]. + * Return the [StreamEncoding] corresponding to the provided code identifier. If no matching + * encoding is found, throw a [NoSuchElementException]. * * @param code identifier * @return encoding enum - * * @throws NoSuchElementException in case of an unmatched identifier */ @JvmStatic fun requireFromCode(code: Int): StreamEncoding { - return fromCode(code) ?: - throw NoSuchElementException("No StreamEncoding found for code $code") + return fromCode(code) + ?: throw NoSuchElementException("No StreamEncoding found for code $code") } } - -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt index ae72ff05..bfd32343 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/SymmetricKeyAlgorithm.kt @@ -11,110 +11,79 @@ package org.pgpainless.algorithm */ enum class SymmetricKeyAlgorithm(val algorithmId: Int) { - /** - * Plaintext or unencrypted data. - */ - NULL (0), + /** Plaintext or unencrypted data. */ + NULL(0), /** * IDEA is deprecated. + * * @deprecated use a different algorithm. */ - @Deprecated("IDEA is deprecated.") - IDEA (1), + @Deprecated("IDEA is deprecated.") IDEA(1), - /** - * TripleDES (DES-EDE - 168 bit key derived from 192). - */ - TRIPLE_DES (2), + /** TripleDES (DES-EDE - 168 bit key derived from 192). */ + TRIPLE_DES(2), - /** - * CAST5 (128-bit key, as per RFC2144). - */ - CAST5 (3), + /** CAST5 (128-bit key, as per RFC2144). */ + CAST5(3), - /** - * Blowfish (128-bit key, 16 rounds). - */ - BLOWFISH (4), + /** Blowfish (128-bit key, 16 rounds). */ + BLOWFISH(4), - /** - * Reserved in RFC4880. - * SAFER-SK128 (13 rounds) - */ - SAFER (5), + /** Reserved in RFC4880. SAFER-SK128 (13 rounds) */ + SAFER(5), - /** - * Reserved in RFC4880. - * Reserved for DES/SK - */ - DES (6), + /** Reserved in RFC4880. Reserved for DES/SK */ + DES(6), - /** - * AES with 128-bit key. - */ - AES_128 (7), + /** AES with 128-bit key. */ + AES_128(7), - /** - * AES with 192-bit key. - */ - AES_192 (8), + /** AES with 192-bit key. */ + AES_192(8), - /** - * AES with 256-bit key. - */ - AES_256 (9), + /** AES with 256-bit key. */ + AES_256(9), - /** - * Twofish with 256-bit key. - */ - TWOFISH (10), + /** Twofish with 256-bit key. */ + TWOFISH(10), - /** - * Reserved for Camellia with 128-bit key. - */ - CAMELLIA_128 (11), + /** Reserved for Camellia with 128-bit key. */ + CAMELLIA_128(11), - /** - * Reserved for Camellia with 192-bit key. - */ - CAMELLIA_192 (12), + /** Reserved for Camellia with 192-bit key. */ + CAMELLIA_192(12), - /** - * Reserved for Camellia with 256-bit key. - */ - CAMELLIA_256 (13), + /** Reserved for Camellia with 256-bit key. */ + CAMELLIA_256(13), ; companion object { /** - * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. - * If an invalid id is provided, null is returned. + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. If + * an invalid id is provided, null is returned. * * @param id numeric algorithm id * @return symmetric key algorithm enum */ @JvmStatic fun fromId(id: Int): SymmetricKeyAlgorithm? { - return values().firstOrNull { - it.algorithmId == id - } + return values().firstOrNull { it.algorithmId == id } } /** - * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. - * If an invalid id is provided, throw a [NoSuchElementException]. + * Return the [SymmetricKeyAlgorithm] enum that corresponds to the provided numeric id. If + * an invalid id is provided, throw a [NoSuchElementException]. * * @param id numeric algorithm id * @return symmetric key algorithm enum - * * @throws NoSuchElementException if an unmatched id is provided */ @JvmStatic fun requireFromId(id: Int): SymmetricKeyAlgorithm { - return fromId(id) ?: - throw NoSuchElementException("No SymmetricKeyAlgorithm found for id $id") + return fromId(id) + ?: throw NoSuchElementException("No SymmetricKeyAlgorithm found for id $id") } } -} \ No newline at end of file +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt index 695632a2..5b9f4c40 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/Trustworthiness.kt @@ -5,26 +5,25 @@ package org.pgpainless.algorithm /** - * Facade class for [org.bouncycastle.bcpg.sig.TrustSignature]. - * A trust signature subpacket marks the trustworthiness of a certificate and defines its capabilities to act - * as a trusted introducer. + * Facade class for [org.bouncycastle.bcpg.sig.TrustSignature]. A trust signature subpacket marks + * the trustworthiness of a certificate and defines its capabilities to act as a trusted introducer. */ class Trustworthiness(amount: Int, depth: Int) { val depth = capDepth(depth) val amount = capAmount(amount) /** - * Returns true, if the trust amount is equal to 0. - * This means the key is not trusted. + * Returns true, if the trust amount is equal to 0. This means the key is not trusted. * * Otherwise return false + * * @return true if untrusted */ fun isNotTrusted() = amount == NOT_TRUSTED /** - * Return true if the certificate is at least marginally trusted. - * That is the case, if the trust amount is greater than 0. + * Return true if the certificate is at least marginally trusted. That is the case, if the trust + * amount is greater than 0. * * @return true if the cert is at least marginally trusted */ @@ -46,7 +45,8 @@ class Trustworthiness(amount: Int, depth: Int) { fun isIntroducer() = depth >= 1 /** - * Return true, if the certified cert can introduce certificates with trust depth of