From b9896d37b3e0dd7a08bd02e218f8e5b5109e1ddc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 14:45:25 +0100 Subject: [PATCH 1/8] Add padding packet tag --- pg/src/main/java/org/bouncycastle/bcpg/PacketTags.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/PacketTags.java b/pg/src/main/java/org/bouncycastle/bcpg/PacketTags.java index 3fc163cb7d..9fe97dabe2 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/PacketTags.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/PacketTags.java @@ -23,6 +23,7 @@ public interface PacketTags public static final int USER_ATTRIBUTE = 17; // User attribute public static final int SYM_ENC_INTEGRITY_PRO = 18; // Symmetric encrypted, integrity protected public static final int MOD_DETECTION_CODE = 19; // Modification detection code + public static final int PADDING = 21; // Padding Packet public static final int EXPERIMENTAL_1 = 60; // Private or Experimental Values public static final int EXPERIMENTAL_2 = 61; From b9c9ca64bdb4ff62fea94a46a4f0aa29c394c088 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 14:53:13 +0100 Subject: [PATCH 2/8] Add third-party confirmation signature type tag --- pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java index 70063f5644..bda2b16d97 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java @@ -47,6 +47,7 @@ public class PGPSignature public static final int SUBKEY_REVOCATION = 0x28; public static final int CERTIFICATION_REVOCATION = 0x30; public static final int TIMESTAMP = 0x40; + public static final int THIRD_PARTY_CONFIRMATION = 0x50; private SignaturePacket sigPck; private int signatureType; From cd6ee5ee739e2b55dd6cf5e2d731ba0e5be7cd56 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 15:06:10 +0100 Subject: [PATCH 3/8] Update SignatureSubpacketTags PreferredAEADAlgorithms was changed from 34 to 39 Key Block was added --- .../java/org/bouncycastle/bcpg/SignatureSubpacketTags.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SignatureSubpacketTags.java b/pg/src/main/java/org/bouncycastle/bcpg/SignatureSubpacketTags.java index edcb6bdf9e..1c9a263048 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/SignatureSubpacketTags.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/SignatureSubpacketTags.java @@ -30,7 +30,9 @@ public interface SignatureSubpacketTags public static final int SIGNATURE_TARGET = 31; // signature target public static final int EMBEDDED_SIGNATURE = 32; // embedded signature public static final int ISSUER_FINGERPRINT = 33; // issuer key fingerprint - public static final int PREFERRED_AEAD_ALGORITHMS = 34; // preferred AEAD algorithms +// public static final int PREFERRED_AEAD_ALGORITHMS = 34; // RESERVED since crypto-refresh-05 public static final int INTENDED_RECIPIENT_FINGERPRINT = 35; // intended recipient fingerprint - public static final int ATTESTED_CERTIFICATIONS = 37; // attested certifications + public static final int ATTESTED_CERTIFICATIONS = 37; // attested certifications (RESERVED) + public static final int KEY_BLOCK = 38; // Key Block (RESERVED) + public static final int PREFERRED_AEAD_ALGORITHMS = 39; // preferred AEAD algorithms } From 32a51fcf3236aa89ec90bbf398a27baeb9673b9b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 16:10:56 +0100 Subject: [PATCH 4/8] Add AEADAlgorithmTags --- .../main/java/org/bouncycastle/bcpg/AEADAlgorithmTags.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pg/src/main/java/org/bouncycastle/bcpg/AEADAlgorithmTags.java diff --git a/pg/src/main/java/org/bouncycastle/bcpg/AEADAlgorithmTags.java b/pg/src/main/java/org/bouncycastle/bcpg/AEADAlgorithmTags.java new file mode 100644 index 0000000000..17f0daf8a2 --- /dev/null +++ b/pg/src/main/java/org/bouncycastle/bcpg/AEADAlgorithmTags.java @@ -0,0 +1,7 @@ +package org.bouncycastle.bcpg; + +public interface AEADAlgorithmTags { + int EAX = 1; // EAX (IV len: 16 octets, Tag len: 16 octets) + int OCB = 2; // OCB (IV len: 15 octets, Tag len: 16 octets) + int GCM = 3; // GCM (IV len: 12 octets, Tag len: 16 octets) +} From 960bbb8817fc6b583a99b60b6ee39308dde2b36e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 16:11:49 +0100 Subject: [PATCH 5/8] Update HashAlgorithmTags Note: SHA3-256 and SHA3-512 values have changed --- .../main/java/org/bouncycastle/bcpg/HashAlgorithmTags.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/HashAlgorithmTags.java b/pg/src/main/java/org/bouncycastle/bcpg/HashAlgorithmTags.java index b3e30343aa..e98fcac904 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/HashAlgorithmTags.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/HashAlgorithmTags.java @@ -1,7 +1,5 @@ package org.bouncycastle.bcpg; -import org.bouncycastle.util.Strings; - /** * basic tags for hash algorithms */ @@ -19,12 +17,12 @@ public interface HashAlgorithmTags public static final int SHA384 = 9; // SHA-384 public static final int SHA512 = 10; // SHA-512 public static final int SHA224 = 11; // SHA-224 + public static final int SHA3_256 = 12; // SHA3-256 + public static final int SHA3_512 = 14; // SHA3-512 public static final int MD4 = 301; public static final int SHA3_224 = 312; // SHA3-224 - public static final int SHA3_256 = 313; //SHA3-256 public static final int SHA3_384 = 314; // SHA3-384 - public static final int SHA3_512 = 315; // SHA3-512 public static final int SM3 = 326; // SHA3-512 From d6df05d1d88e855f04c34eac9279f36e03d49d8e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 16:13:00 +0100 Subject: [PATCH 6/8] SecretKeyPacket: Add AEAD usage tag --- pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java | 1 + 1 file changed, 1 insertion(+) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java index f92360718f..40b226b9f5 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java @@ -12,6 +12,7 @@ public class SecretKeyPacket public static final int USAGE_NONE = 0x00; public static final int USAGE_CHECKSUM = 0xff; public static final int USAGE_SHA1 = 0xfe; + public static final int USAGE_AEAD = 0xfd; private PublicKeyPacket pubKeyPacket; private byte[] secKeyData; From b0aef34bdaa0be3866f51e9dc09b30ab1579be41 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 16:13:22 +0100 Subject: [PATCH 7/8] S2K: Add Argon2 algorithm tag --- pg/src/main/java/org/bouncycastle/bcpg/S2K.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java index ee8eb12db5..e3fd548127 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java @@ -32,6 +32,10 @@ public class S2K * Salted and iterated key generation. Multiple iterations of a hash function, with a salt */ public static final int SALTED_AND_ITERATED = 3; + /** + * Memory-hard, salted key generation using Argon2 hash algorithm. + */ + public static final int ARGON_2 = 4; public static final int GNU_DUMMY_S2K = 101; From a7bfbfc3197e3df81a2c6507fd59cf38ced3a5dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 23 Mar 2022 17:05:49 +0100 Subject: [PATCH 8/8] S2K: Add support for Argon2 (encoding, decoding only) --- .../main/java/org/bouncycastle/bcpg/S2K.java | 448 ++++++++++++++++-- .../PBEKeyEncryptionMethodGenerator.java | 16 +- .../openpgp/operator/PGPUtil.java | 22 +- .../bc/BcPBEKeyEncryptionMethodGenerator.java | 9 + .../openpgp/test/Argon2S2KTest.java | 186 ++++++++ 5 files changed, 629 insertions(+), 52 deletions(-) create mode 100644 pg/src/test/java/org/bouncycastle/openpgp/test/Argon2S2KTest.java diff --git a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java index e3fd548127..3dab7b62f4 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/S2K.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/S2K.java @@ -3,6 +3,7 @@ import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; +import java.security.SecureRandom; /** @@ -16,7 +17,7 @@ *

*/ public class S2K - extends BCPGObject + extends BCPGObject { private static final int EXPBIAS = 6; @@ -46,39 +47,54 @@ public class S2K int algorithm; byte[] iv; int itCount = -1; + int passes = -1; int protectionMode = -1; + int parallelism; + int memorySizeExponent; S2K( - InputStream in) - throws IOException + InputStream in) + throws IOException { DataInputStream dIn = new DataInputStream(in); type = dIn.read(); - algorithm = dIn.read(); - // - // if this happens we have a dummy-S2K packet. - // - if (type != GNU_DUMMY_S2K) - { - if (type != 0) - { + switch (type) { + case SIMPLE: + algorithm = dIn.read(); + break; + + case SALTED: + algorithm = dIn.read(); iv = new byte[8]; dIn.readFully(iv, 0, iv.length); + break; - if (type == 3) - { - itCount = dIn.read(); - } - } - } - else - { - dIn.read(); // G - dIn.read(); // N - dIn.read(); // U - protectionMode = dIn.read(); // protection mode + case SALTED_AND_ITERATED: + algorithm = dIn.read(); + iv = new byte[8]; + dIn.readFully(iv, 0, iv.length); + itCount = dIn.read(); + break; + + case ARGON_2: + iv = new byte[16]; + dIn.readFully(iv); + passes = dIn.read(); + parallelism = dIn.read(); + memorySizeExponent = dIn.read(); + break; + + case GNU_DUMMY_S2K: + dIn.read(); // G + dIn.read(); // N + dIn.read(); // U + protectionMode = dIn.read(); // protection mode + break; + + default: + throw new IllegalStateException("Invalid S2K type: " + type); } } @@ -88,7 +104,7 @@ public class S2K * @param algorithm the {@link HashAlgorithmTags digest algorithm} to use. */ public S2K( - int algorithm) + int algorithm) { this.type = 0; this.algorithm = algorithm; @@ -101,8 +117,8 @@ public S2K( * @param iv the salt to apply to input to the key generation. */ public S2K( - int algorithm, - byte[] iv) + int algorithm, + byte[] iv) { this.type = 1; this.algorithm = algorithm; @@ -117,9 +133,9 @@ public S2K( * @param itCount the single byte iteration count specifier. */ public S2K( - int algorithm, - byte[] iv, - int itCount) + int algorithm, + byte[] iv, + int itCount) { this.type = 3; this.algorithm = algorithm; @@ -132,6 +148,85 @@ public S2K( this.itCount = itCount; } + /** + * Constructs a specifier for an {@link #ARGON_2 S2K method using Argon2}. + * + * @param argon2Params argon2 parameters + */ + public S2K(Argon2Params argon2Params) + { + this.type = ARGON_2; + this.iv = argon2Params.getSalt(); + this.passes = argon2Params.getPasses(); + this.parallelism = argon2Params.getParallelism(); + this.memorySizeExponent = argon2Params.getMemSizeExp(); + } + + /** + * Construct a specifier for an S2K using the {@link #GNU_DUMMY_S2K} method. + * + * @param gnuDummyParams GNU_DUMMY_S2K parameters + */ + public S2K(GNUDummyParams gnuDummyParams) { + this.type = GNU_DUMMY_S2K; + this.protectionMode = gnuDummyParams.getProtectionMode(); + } + + /** + * Return a new S2K instance using the {@link #SIMPLE} method, using the given hash
algorithm
. + * + * @param algorithm hash algorithm tag + * @return S2K + */ + public static S2K simpleS2K(int algorithm) { + return new S2K(algorithm); + } + + /** + * Return a new S2K instance using the {@link #SALTED} method, using the given hash
algorithm
+ * and
salt
. + * + * @param algorithm hash algorithm tag + * @param salt salt + * @return S2K + */ + public static S2K saltedS2K(int algorithm, byte[] salt) { + return new S2K(algorithm, salt); + } + + /** + * Return a new S2K instance using the {@link #SALTED_AND_ITERATED} method, using the given hash
algorithm
, + *
salt
and
iterationCount
. + * + * @param algorithm hash algorithm tag + * @param salt salt + * @param iterationCount number of iterations + * @return S2K + */ + public static S2K saltedAndIteratedS2K(int algorithm, byte[] salt, int iterationCount) { + return new S2K(algorithm, salt, iterationCount); + } + + /** + * Return a new S2K instance using the {@link #ARGON_2} method, using the given argon2
parameters
. + * + * @param parameters argon2 parameters + * @return S2K + */ + public static S2K argon2S2K(Argon2Params parameters) { + return new S2K(parameters); + } + + /** + * Return a new S2K instance using the {@link #GNU_DUMMY_S2K} method, using the given GNU Dummy S2K
parameters
. + * + * @param parameters GNU Dummy S2K parameters + * @return S2K + */ + public static S2K gnuDummyS2K(GNUDummyParams parameters) { + return new S2K(parameters); + } + /** * Gets the {@link HashAlgorithmTags digest algorithm} specified. */ @@ -168,6 +263,15 @@ public long getIterationCount() return (16 + (itCount & 15)) << ((itCount >> 4) + EXPBIAS); } + /** + * Return the number of passes - only Argon2 + * @return number of passes + */ + public int getPasses() + { + return passes; + } + /** * Gets the protection mode - only if GNU_DUMMY_S2K */ @@ -176,37 +280,285 @@ public int getProtectionMode() return protectionMode; } + /** + * Gets the degree of parallelism - only if ARGON_2 + * @return parallelism + */ + public int getParallelism() + { + return parallelism; + } + + /** + * Gets the memory size exponent - only if ARGON_2 + * @return memory size exponent + */ + public int getMemorySizeExponent() + { + return memorySizeExponent; + } + public void encode( - BCPGOutputStream out) - throws IOException + BCPGOutputStream out) + throws IOException { - out.write(type); - out.write(algorithm); + switch (type) + { + case SIMPLE: + out.write(type); + out.write(algorithm); + break; + + case SALTED: + out.write(type); + out.write(algorithm); + out.write(iv); + break; + + case SALTED_AND_ITERATED: + out.write(type); + out.write(algorithm); + out.write(iv); + writeOneOctetOrThrow(out, itCount, "Iteration count"); + break; - if (type != GNU_DUMMY_S2K) + case ARGON_2: + out.write(type); + out.write(iv); + writeOneOctetOrThrow(out, passes, "Passes"); + writeOneOctetOrThrow(out, parallelism, "Parallelism"); + writeOneOctetOrThrow(out, memorySizeExponent, "Memory size exponent"); + break; + + case GNU_DUMMY_S2K: + out.write(type); + out.write('G'); + out.write('N'); + out.write('U'); + out.write(protectionMode); + break; + + default: + throw new IllegalStateException("Unknown S2K type " + type); + } + } + + /** + * Throw an {@link IllegalArgumentException} if the value cannot be encoded, + * otherwise write the value to the output stream. + * + * @param out output stream + * @param val value + * @param valName name of the value for the error message + * + * @throws IllegalArgumentException if the value cannot be encoded + * @throws IOException potentially thrown by {@link BCPGOutputStream#write(int)} + */ + private void writeOneOctetOrThrow(BCPGOutputStream out, int val, String valName) + throws IOException + { + if (val >= 256) { - if (type != 0) + throw new IllegalStateException(valName + " not encodable"); + } + out.write(val); + } + + /** + * Parameters for Argon2 S2K. + */ + public static class Argon2Params + { + private final byte[] salt; + private final int passes; + private final int parallelism; + private final int memSizeExp; + + /** + * Uniformly safe and recommended parameters not tailored to any hardware. + * Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + * + * @see RFC 9106: §4. Parameter Choice + */ + public Argon2Params() + { + this(new SecureRandom()); + } + + /** + * Uniformly safe and recommended parameters not tailored to any hardware. + * Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + * + * @see RFC 9106: §4. Parameter Choice + */ + public Argon2Params(SecureRandom secureRandom) + { + this(1, 4, 21, secureRandom); + } + + /** + * Create customized Argon2 S2K parameters. + * + * @param passes number of iterations, must be greater than 0 + * @param parallelism number of lanes, must be greater 0 + * @param memSizeExp exponent for memory consumption, must be between 3+ceil(log_2(p)) and 31 + * @param secureRandom secure random generator to initialize the salt vector + */ + public Argon2Params(int passes, int parallelism, int memSizeExp, SecureRandom secureRandom) + { + this(mineSalt(secureRandom), passes, parallelism, memSizeExp); + } + + /** + * Create customized Argon2 S2K parameters. + * + * @param salt 16 bytes of random salt + * @param passes number of iterations, must be greater than 0 + * @param parallelism number of lanes, must be greater 0 + * @param memSizeExp exponent for memory consumption, must be between 3+ceil(log_2(p)) and 31 + */ + public Argon2Params(byte[] salt, int passes, int parallelism, int memSizeExp) + { + if (salt.length != 16) { - out.write(iv); + throw new IllegalArgumentException("Argon2 uses 16 bytes of salt"); } + this.salt = salt; - if (type == 3) + if (passes < 1) { - if (itCount >= 256) - { - // TODO check C code for encoding. - throw new IllegalStateException("not encodable"); - } + throw new IllegalArgumentException("Number of passes MUST be positive, non-zero"); + } + this.passes = passes; + + if (parallelism < 1) + { + throw new IllegalArgumentException("Parallelism MUST be positive, non-zero."); + } + this.parallelism = parallelism; - out.write(itCount); + // log_2(p) = log_e(p) / log_e(2) + double log2_p = Math.log(parallelism) / Math.log(2); + // see https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-05.html#section-3.7.1.4-5 + if (memSizeExp < (3 + Math.ceil(log2_p)) || memSizeExp > 31) + { + throw new IllegalArgumentException("Memory size exponent MUST be between 3+ceil(log_2(parallelism)) and 31"); } + this.memSizeExp = memSizeExp; } - else + + /** + * Uniformly safe and recommended parameters not tailored to any hardware. + * Uses Argon2id, 1 pass, 4 parallelism, 2 GiB RAM. + * + * @see RFC 9106: §4. Parameter Choice + */ + public static Argon2Params universallyRecommendedParameters() + { + return new Argon2Params(1, 4, 21, new SecureRandom()); + } + + /** + * Recommended parameters for memory constrained environments (64MiB RAM). + * Uses Argon2id with 3 passes, 4 lanes and 64 MiB RAM. + * + * @see RFC9106: §4. Parameter Choice + * @return safe parameters for memory constrained environments + */ + public static Argon2Params memoryConstrainedParameters() + { + return new Argon2Params(3, 4, 16, new SecureRandom()); + } + + /** + * Generate 16 bytes of random salt. + * @param secureRandom random number generator instance + * @return salt + */ + private static byte[] mineSalt(SecureRandom secureRandom) + { + byte[] salt = new byte[16]; + secureRandom.nextBytes(salt); + return salt; + } + + /** + * Return a 16-byte byte array containing the salt
S
. + * + * @return salt + */ + public byte[] getSalt() + { + return salt; + } + + /** + * Return the number of passes
t
. + * + * @return number of passes + */ + public int getPasses() + { + return passes; + } + + /** + * Return the factor of parallelism
p
. + * + * @return parallelism + */ + public int getParallelism() { - out.write('G'); - out.write('N'); - out.write('U'); - out.write(protectionMode); + return parallelism; + } + + /** + * Return the exponent indicating the memory size
m
. + * + * @return memory size exponent + */ + public int getMemSizeExp() + { + return memSizeExp; + } + } + + /** + * Parameters for the {@link #GNU_DUMMY_S2K} method. + */ + public static class GNUDummyParams { + + private final int protectionMode; + + private GNUDummyParams(int protectionMode) { + this.protectionMode = protectionMode; + } + + /** + * Factory method for a GNU Dummy S2K indicating a missing private key. + * + * @return params + */ + public static GNUDummyParams noPrivateKey() { + return new GNUDummyParams(GNU_PROTECTION_MODE_NO_PRIVATE_KEY); + } + + /** + * Factory method for a GNU Dummy S2K indicating a private key located on a smart card. + * + * @return params + */ + public static GNUDummyParams divertToCard() { + return new GNUDummyParams(GNU_PROTECTION_MODE_DIVERT_TO_CARD); + } + + /** + * Return the GNU Dummy S2K protection method. + * + * @return protection method + */ + public int getProtectionMode() { + return protectionMode; } } } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PBEKeyEncryptionMethodGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBEKeyEncryptionMethodGenerator.java index b9190bb514..bcac5f1195 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/PBEKeyEncryptionMethodGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PBEKeyEncryptionMethodGenerator.java @@ -1,13 +1,13 @@ package org.bouncycastle.openpgp.operator; -import java.security.SecureRandom; - import org.bouncycastle.bcpg.ContainedPacket; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; import org.bouncycastle.openpgp.PGPException; +import java.security.SecureRandom; + /** * PGP style PBE encryption method. *

@@ -43,6 +43,18 @@ protected PBEKeyEncryptionMethodGenerator( this(passPhrase, s2kDigestCalculator, 0x60); } + /** + * Construct a PBE key generator using Argon2 as S2K mechanism. + * + * @param passPhrase passphrase + * @param params argon2 parameters + */ + protected PBEKeyEncryptionMethodGenerator( + char[] passPhrase, S2K.Argon2Params params) { + this.passPhrase = passPhrase; + this.s2k = new S2K(params); + } + /** * Construct a PBE key generator using a specific iteration level. * diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPUtil.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPUtil.java index 1a5d2e7aa7..4dc8400ed0 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPUtil.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/PGPUtil.java @@ -6,6 +6,8 @@ import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.util.Strings; @@ -92,7 +94,23 @@ static byte[] makeKeyFromPassPhrase( if (s2k != null) { - if (s2k.getHashAlgorithm() != digestCalculator.getAlgorithm()) + if (s2k.getType() == S2K.ARGON_2) + { + Argon2Parameters.Builder builder = new Argon2Parameters + .Builder(Argon2Parameters.ARGON2_id) + .withSalt(s2k.getIV()) + .withIterations(s2k.getPasses()) + .withParallelism(s2k.getParallelism()) + .withMemoryPowOfTwo(s2k.getMemorySizeExponent()) + .withVersion(Argon2Parameters.ARGON2_VERSION_13); + + Argon2BytesGenerator argon2 = new Argon2BytesGenerator(); + argon2.init(builder.build()); + argon2.generateBytes(passPhrase, keyBytes); + + return keyBytes; + } + else if (s2k.getHashAlgorithm() != digestCalculator.getAlgorithm()) { throw new PGPException("s2k/digestCalculator mismatch"); } @@ -215,7 +233,7 @@ public static byte[] makeKeyFromPassPhrase( { PGPDigestCalculator digestCalculator; - if (s2k != null) + if (s2k != null && s2k.getType() != S2K.ARGON_2) { digestCalculator = digCalcProvider.get(s2k.getHashAlgorithm()); } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPBEKeyEncryptionMethodGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPBEKeyEncryptionMethodGenerator.java index 17aa28cc24..d3fc54b0e1 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPBEKeyEncryptionMethodGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcPBEKeyEncryptionMethodGenerator.java @@ -39,6 +39,15 @@ public BcPBEKeyEncryptionMethodGenerator(char[] passPhrase) this(passPhrase, new SHA1PGPDigestCalculator()); } + /** + * Create a PBE encryption method generator using Argon2 for S2K key generation. + * @param passPhrase passphrase + * @param argon2Params parameters for argon2 + */ + public BcPBEKeyEncryptionMethodGenerator(char[] passPhrase, S2K.Argon2Params argon2Params) { + super(passPhrase, argon2Params); + } + /** * Create a PBE encryption method generator using the provided calculator and S2K count for key * generation. diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/Argon2S2KTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/Argon2S2KTest.java new file mode 100644 index 0000000000..988b9361af --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/Argon2S2KTest.java @@ -0,0 +1,186 @@ +package org.bouncycastle.openpgp.test; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPBEKeyEncryptionMethodGenerator; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.util.io.Streams; +import org.bouncycastle.util.test.SimpleTest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Date; + +public class Argon2S2KTest + extends SimpleTest { + + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final String TEST_MSG_PASSWORD = "password"; + + // Test message from the crypto-refresh-05 document + private static final String TEST_MSG_AES128 = "-----BEGIN PGP MESSAGE-----\n" + + "Comment: Encrypted using AES with 128-bit key\n" + + "Comment: Session key: 01FE16BBACFD1E7B78EF3B865187374F\n" + + "\n" + + "wycEBwScUvg8J/leUNU1RA7N/zE2AQQVnlL8rSLPP5VlQsunlO+ECxHSPgGYGKY+\n" + + "YJz4u6F+DDlDBOr5NRQXt/KJIf4m4mOlKyC/uqLbpnLJZMnTq3o79GxBTdIdOzhH\n" + + "XfA3pqV4mTzF\n" + + "=uIks\n" + + "-----END PGP MESSAGE-----"; + + // Test message from the crypto-refresh-05 document + private static final String TEST_MSG_AES192 = "-----BEGIN PGP MESSAGE-----\n" + + "Comment: Encrypted using AES with 192-bit key\n" + + "Comment: Session key: 27006DAE68E509022CE45A14E569E91001C2955AF8DFE194\n" + + "\n" + + "wy8ECAThTKxHFTRZGKli3KNH4UP4AQQVhzLJ2va3FG8/pmpIPd/H/mdoVS5VBLLw\n" + + "F9I+AdJ1Sw56PRYiKZjCvHg+2bnq02s33AJJoyBexBI4QKATFRkyez2gldJldRys\n" + + "LVg77Mwwfgl2n/d572WciAM=\n" + + "=n8Ma\n" + + "-----END PGP MESSAGE-----"; + + // Test message from the crypto-refresh-05 document + private static final String TEST_MSG_AES256 = "-----BEGIN PGP MESSAGE-----\n" + + "Comment: Encrypted using AES with 192-bit key\n" + + "Comment: Session key: 27006DAE68E509022CE45A14E569E91001C2955AF8DFE194\n" + + "\n" + + "wy8ECAThTKxHFTRZGKli3KNH4UP4AQQVhzLJ2va3FG8/pmpIPd/H/mdoVS5VBLLw\n" + + "F9I+AdJ1Sw56PRYiKZjCvHg+2bnq02s33AJJoyBexBI4QKATFRkyez2gldJldRys\n" + + "LVg77Mwwfgl2n/d572WciAM=\n" + + "=n8Ma\n" + + "-----END PGP MESSAGE-----"; + + private static final String TEST_MSG_PLAIN = "Hello, world!"; + + public static void main(String[] args) { + runTest(new Argon2S2KTest()); + } + + @Override + public String getName() { + return Argon2S2KTest.class.getSimpleName(); + } + + @Override + public void performTest() throws Exception { + // S2K parameter serialization + encodingTest(); + // Test vectors + testDecryptAES128Message(); + testDecryptAES192Message(); + testDecryptAES256Message(); + // dynamic round-trip + testEncryptAndDecryptMessageWithArgon2(); + } + + public void encodingTest() throws IOException { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + + S2K.Argon2Params params = new S2K.Argon2Params(salt, 1, 4, 21); + S2K argon2 = S2K.argon2S2K(params); + + isEquals(S2K.ARGON_2, argon2.getType()); + isEquals(1, argon2.getPasses()); + isEquals(4, argon2.getParallelism()); + isEquals(21, argon2.getMemorySizeExponent()); + isEquals(16, argon2.getIV().length); + + // Test actual encoding + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + BCPGOutputStream out = new BCPGOutputStream(bytes); + argon2.encode(out); + byte[] encoding = bytes.toByteArray(); + + isEquals(20, encoding.length); + isEquals(0x04, encoding[0]); // Type is Argon2 + isEquals(0x01, encoding[17]); // 1 pass + isEquals(0x04, encoding[18]); // 4 parallelism + isEquals(0x15, encoding[19]); // 0x15 = 21 mem exp + } + + public void testDecryptAES128Message() throws IOException, PGPException { + String plaintext = decryptSymmetricallyEncryptedMessage(TEST_MSG_AES128, TEST_MSG_PASSWORD); + isEquals(TEST_MSG_PLAIN, plaintext); + } + + public void testDecryptAES192Message() throws IOException, PGPException { + String plaintext = decryptSymmetricallyEncryptedMessage(TEST_MSG_AES192, TEST_MSG_PASSWORD); + isEquals(TEST_MSG_PLAIN, plaintext); + } + + public void testDecryptAES256Message() throws IOException, PGPException { + String plaintext = decryptSymmetricallyEncryptedMessage(TEST_MSG_AES256, TEST_MSG_PASSWORD); + isEquals(TEST_MSG_PLAIN, plaintext); + } + + public void testEncryptAndDecryptMessageWithArgon2() throws PGPException, IOException { + String encrypted = encryptMessageSymmetricallyWithArgon2(TEST_MSG_PLAIN, TEST_MSG_PASSWORD); + String plaintext = decryptSymmetricallyEncryptedMessage(encrypted, TEST_MSG_PASSWORD); + isEquals(TEST_MSG_PLAIN, plaintext); + } + + private String decryptSymmetricallyEncryptedMessage(String message, String password) throws IOException, PGPException { + char[] pass = password.toCharArray(); + BcPBEDataDecryptorFactory factory = new BcPBEDataDecryptorFactory(pass, new BcPGPDigestCalculatorProvider()); + ByteArrayInputStream msgIn = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = new ArmoredInputStream(msgIn); + + PGPObjectFactory objectFactory = new BcPGPObjectFactory(armorIn); + PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) objectFactory.nextObject(); + PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) encryptedDataList.get(0); + + // decrypt + InputStream inputStream = encryptedData.getDataStream(factory); + objectFactory = new BcPGPObjectFactory(inputStream); + PGPLiteralData literalData = (PGPLiteralData) objectFactory.nextObject(); + InputStream decryptedIn = literalData.getDataStream(); + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptedIn, decryptedOut); + + String decryptedString = decryptedOut.toString(); + return decryptedString; + } + + public String encryptMessageSymmetricallyWithArgon2(String plaintext, String password) throws PGPException, IOException { + + PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator( + new BcPGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256)); + encGen.addMethod(new BcPBEKeyEncryptionMethodGenerator(password.toCharArray(), S2K.Argon2Params.universallyRecommendedParameters())); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + OutputStream encOut = encGen.open(armorOut, new byte[4096]); + OutputStream litOut = litGen.open(encOut, PGPLiteralData.UTF8, "", new Date(), new byte[4096]); + + ByteArrayInputStream plainIn = new ByteArrayInputStream(plaintext.getBytes(StandardCharsets.UTF_8)); + Streams.pipeAll(plainIn, litOut); + litOut.close(); + encOut.close(); + armorOut.close(); + + String encrypted = out.toString(); + return encrypted; + } + +}