From b9896d37b3e0dd7a08bd02e218f8e5b5109e1ddc Mon Sep 17 00:00:00 2001
From: Paul Schaub
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, + *
saltand
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; + } + +}