Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase iterations for key creation #1251

Open
wants to merge 1 commit into
base: releases/25.1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,15 +316,25 @@ public void testSignatureApiStability() {
}

@Test
public void testDecryptionApiStability() {
public void testDecryptionApiStability_2023() {
final byte[] encrypted = Base64Utility.decode("43aysPcKhTvyzZIWa6d1wwntGobQOXT38VU=");
final byte[] salt = Base64Utility.decode("iPENJpMTU8MxarL8ZMHxXw==");
Assert.assertEquals("myTestData", new String(SecurityUtility.decrypt(encrypted, PASSWORD, salt, 128), ENCODING));
EncryptionKey key = SecurityUtility.createDecryptionKey(new PushbackInputStream(new ByteArrayInputStream(encrypted), 6), PASSWORD, salt, 128, null);
Assert.assertEquals("[1:128-PBKDF2WithHmacSHA256-AES-SunJCE-16-128-3557]", new String(key.getCompatibilityHeader(), StandardCharsets.US_ASCII));
Assert.assertEquals("[2023:v1]", new String(key.getCompatibilityHeader(), StandardCharsets.US_ASCII));
Assert.assertEquals("myTestData", new String(SecurityUtility.decrypt(encrypted, key), ENCODING));
}

@Test
public void testDecryptionApiStability_2024() {
final byte[] encrypted = Base64Utility.decode("WzIwMjQ6djFdU1rrQiSnCSBlPEk7SZayaYngVKYszy7EbjV1RGUq0CsaJyOHtXZwCp+ogg==");
final byte[] salt = "salty".getBytes(ENCODING);
Assert.assertEquals("This is an encrypted string", new String(SecurityUtility.decrypt(encrypted, PASSWORD, salt, 128), ENCODING));
EncryptionKey key = SecurityUtility.createDecryptionKey(new PushbackInputStream(new ByteArrayInputStream(encrypted), 6), PASSWORD, salt, 128, null);
Assert.assertEquals("[2024:v1]", new String(key.getCompatibilityHeader(), StandardCharsets.US_ASCII));
Assert.assertEquals("This is an encrypted string", new String(SecurityUtility.decrypt(encrypted, key), ENCODING));
}

@Test
public void testExtractCompatibilityHeader() {
PushbackInputStream in = new PushbackInputStream(new ByteArrayInputStream(new byte[]{0, 1, 2, 3, 4, 5}), 6);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ public interface ISecurityProvider {
* </pre>
*/
String ENCRYPTION_COMPATIBILITY_HEADER_2023_V1 = "[2023:v1]";
/**
* <pre>
* secretKeyAlgorithm: PBKDF2WithHmacSHA256
* cipherAlgorithm/Provider: AES/SunJCE
* GCM init vector length: 16
* GCM auth tag bit length: 128
* key derivation iteration count: 10000
* </pre>
*/
String ENCRYPTION_COMPATIBILITY_HEADER_2024_V1 = "[2024:v1]";
String ENCRYPTION_COMPATIBILITY_HEADER = ENCRYPTION_COMPATIBILITY_HEADER_2023_V1;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ private SecurityUtility() {
* See {@link ISecurityProvider#encrypt(InputStream, OutputStream, EncryptionKey)}
*/
public static void encrypt(InputStream clearTextData, OutputStream encryptedData, EncryptionKey key) {
byte[] compatibilityHeader = key.getCompatibilityHeader();
if (compatibilityHeader != null) {
try {
encryptedData.write(compatibilityHeader);
}
catch (IOException e) {
throw new ProcessingException("Unable to add compatibility header", e);
}
}
SECURITY_PROVIDER.get().encrypt(clearTextData, encryptedData, key);
}

Expand All @@ -71,7 +80,13 @@ public static void encrypt(InputStream clearTextData, OutputStream encryptedData
*/
public static void decrypt(InputStream encryptedData, OutputStream clearTextData, EncryptionKey key) {
PushbackInputStream input = new PushbackInputStream(encryptedData, 6);
extractCompatibilityHeader(input);
byte[] compatibilityHeader = extractCompatibilityHeader(input);// fast-forward inputStream to skip compatibility header
if (compatibilityHeader != null) {
byte[] keyCompatibilityHeader = key.getCompatibilityHeader();
if (keyCompatibilityHeader != null) {
Assertions.assertTrue(Arrays.equals(compatibilityHeader, keyCompatibilityHeader), "Key compatibility header mismatch.");
}
}
SECURITY_PROVIDER.get().decrypt(input, clearTextData, key);
}

Expand Down Expand Up @@ -176,7 +191,7 @@ public static byte[] encrypt(byte[] clearTextData, EncryptionKey key) {
Assertions.assertNotNull(clearTextData, "no data provided");
ByteArrayInputStream input = new ByteArrayInputStream(clearTextData);
int aesBlockSize = 16;
int expectedOutSize = ((input.available() / aesBlockSize) + 2) * aesBlockSize;
int expectedOutSize = ((input.available() / aesBlockSize) + 2) * aesBlockSize + 9;
ByteArrayOutputStream result = new ByteArrayOutputStream(expectedOutSize);
encrypt(input, result, key);
return result.toByteArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ public EncryptionKey createEncryptionKey(char[] password, byte[] salt, int keyLe
@Override
public EncryptionKey createDecryptionKey(char[] password, byte[] salt, int keyLen, byte[] compatibilityHeader) {
String v = compatibilityHeader != null ? new String(compatibilityHeader, StandardCharsets.US_ASCII) : ENCRYPTION_COMPATIBILITY_HEADER_2021_V1;
if (ENCRYPTION_COMPATIBILITY_HEADER_2021_V1.equals(v)) {
// legacy
if (ENCRYPTION_COMPATIBILITY_HEADER_2021_V1.equals(v) || ENCRYPTION_COMPATIBILITY_HEADER_2023_V1.equals(v)) {
// legacy (also used if no header is set, see above)
return createEncryptionKeyInternal(
password,
salt,
Expand All @@ -122,7 +122,7 @@ public EncryptionKey createDecryptionKey(char[] password, byte[] salt, int keyLe
128,
3557);
}
if (ENCRYPTION_COMPATIBILITY_HEADER_2023_V1.equals(v)) {
if (ENCRYPTION_COMPATIBILITY_HEADER_2024_V1.equals(v)) {
return createEncryptionKeyInternal(
password,
salt,
Expand All @@ -132,7 +132,7 @@ public EncryptionKey createDecryptionKey(char[] password, byte[] salt, int keyLe
"SunJCE",
16,
128,
3557);
10000);
}
if (ENCRYPTION_COMPATIBILITY_HEADER.equals(v)) {
// latest
Expand Down Expand Up @@ -168,15 +168,7 @@ private static EncryptionKey createEncryptionKeyInternal(

SecretKey secretKey = new SecretKeySpec(key, cipherAlgorithm);
GCMParameterSpec parameters = new GCMParameterSpec(gcmAuthTagBitLen, iv);
byte[] compatibilityHeader = ("[1:"
+ keyLen
+ "-" + secretKeyAlgorithm
+ "-" + cipherAlgorithm
+ "-" + cipherAlgorithmProvider
+ "-" + gcmInitVecLen
+ "-" + gcmAuthTagBitLen
+ "-" + keyDerivationIterationCount
+ "]").getBytes(StandardCharsets.US_ASCII);
byte[] compatibilityHeader = generateCompatibilityHeader(keyLen, secretKeyAlgorithm, cipherAlgorithm, cipherAlgorithmProvider, gcmInitVecLen, gcmAuthTagBitLen, keyDerivationIterationCount);
return new EncryptionKey(secretKey, parameters, compatibilityHeader);
}
catch (NoSuchAlgorithmException e) {
Expand All @@ -187,36 +179,57 @@ private static EncryptionKey createEncryptionKeyInternal(
}
}

protected static byte[] generateCompatibilityHeader(int keyLen, String secretKeyAlgorithm, String cipherAlgorithm, String cipherAlgorithmProvider, int gcmInitVecLen, int gcmAuthTagBitLen, int keyDerivationIterationCount) {
String headerStr = "[1:"
+ keyLen
+ "-" + secretKeyAlgorithm
+ "-" + cipherAlgorithm
+ "-" + cipherAlgorithmProvider
+ "-" + gcmInitVecLen
+ "-" + gcmAuthTagBitLen
+ "-" + keyDerivationIterationCount
+ "]";
switch (headerStr) {
case "[1:128-PBKDF2WithHmacSHA256-AES-SunJCE-16-128-3557]":
headerStr = ENCRYPTION_COMPATIBILITY_HEADER_2023_V1;
break;
case "[1:128-PBKDF2WithHmacSHA256-AES-SunJCE-16-128-10000]":
headerStr = ENCRYPTION_COMPATIBILITY_HEADER_2024_V1;
break;
}
return headerStr.getBytes(StandardCharsets.US_ASCII);
}

@Override
public byte[] createPasswordHash(char[] password, byte[] salt) {
return createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS);
}

/**
* @param password
* The password to create the hash for. Must not be {@code null} or empty.
* The password to create the hash for. Must not be {@code null} or empty.
* @param salt
* The salt to use. Use {@link #createSecureRandomBytes(int)} to generate a new random salt for each
* credential. Do not use the same salt for multiple credentials. The salt should be at least 32 bytes long.
* Remember to save the salt with the hashed password! Must not be {@code null} or an empty array.
* The salt to use. Use {@link #createSecureRandomBytes(int)} to generate a new random salt for each
* credential. Do not use the same salt for multiple credentials. The salt should be at least 32 bytes long.
* Remember to save the salt with the hashed password! Must not be {@code null} or an empty array.
* @param iterations
* Specifies how many times the method executes its underlying algorithm. A higher value is safer.<br>
* While there is a minimum number of iterations recommended to ensure data safety, this value changes every
* year as technology improves. As by Aug 2021 at least 120000 iterations are recommended, see
* https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html.<br>
* Experimentation is important. To provide a good security use an iteration count so that the call to this
* method requires one half second to execute (on the production system). Also consider the number of users
* and the number of logins executed to find a value that scales in your environment.
* Specifies how many times the method executes its underlying algorithm. A higher value is safer.<br>
* While there is a minimum number of iterations recommended to ensure data safety, this value changes every
* year as technology improves. As by Aug 2021 at least 120000 iterations are recommended, see
* https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html.<br>
* Experimentation is important. To provide a good security use an iteration count so that the call to this
* method requires one half second to execute (on the production system). Also consider the number of users
* and the number of logins executed to find a value that scales in your environment.
* @return the password hash
* @throws AssertionException
* If one of the following conditions is {@code true}:<br>
* <ul>
* <li>The password is {@code null} or an empty array</li>
* <li>The salt is {@code null} or an empty array</li>
* <li>The number of iterations is too small.</li>
* </ul>
* If one of the following conditions is {@code true}:<br>
* <ul>
* <li>The password is {@code null} or an empty array</li>
* <li>The salt is {@code null} or an empty array</li>
* <li>The number of iterations is too small.</li>
* </ul>
* @throws ProcessingException
* If there is an error creating the hash. <br>
* If there is an error creating the hash. <br>
*/
public byte[] createPasswordHash(char[] password, byte[] salt, int iterations) {
assertGreater(assertNotNull(password, "password must not be null.").length, 0, "empty password is not allowed.");
Expand Down Expand Up @@ -408,7 +421,7 @@ public byte[] createSignature(byte[] privateKey, InputStream data) {
}
catch (NoSuchProviderException | NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | SignatureException | IOException e) {
throw new ProcessingException("Unable to create signature. If the curve is not supported (see cause below), consider creating a new key-pair"
+ " by running '{}' on the command line and configure the properties (e.g. 'scout.auth.publicKey' and 'scout.auth.privateKey') with the new values.", SecurityUtility.class.getName(), e);
+ " by running '{}' on the command line and configure the properties (e.g. 'scout.auth.publicKey' and 'scout.auth.privateKey') with the new values.", SecurityUtility.class.getName(), e);
}
}

Expand Down Expand Up @@ -487,17 +500,21 @@ protected String getMacAlgorithmProvider() {
}

/**
* @return Iteration count for key derivation. <a href="https://www.baeldung.com/java-secure-aes-key">AES Keys</a>
* <p>
* 2023/05: at least 1000
* <p>
* Do not confuse this parameter with {@link #MIN_PASSWORD_HASH_ITERATIONS}. This parameter is used to derive
* a PBEKey for {@link #encrypt(InputStream, OutputStream, EncryptionKey)} whereas
* {@link #MIN_PASSWORD_HASH_ITERATIONS} is used to hash single passwords in a table that is potentially
* exposed to a rainbow attack.
* @return Iteration count for key derivation (current version, see {@link #ENCRYPTION_COMPATIBILITY_HEADER}).
* <p>
* RFC 8018 recommends to use at least 1000 iterations, OpenSSL currently uses 10000 iterations by default.
* <p>
* Do not confuse this parameter with {@link #MIN_PASSWORD_HASH_ITERATIONS}. This parameter is used to derive
* a PBEKey for {@link #encrypt(InputStream, OutputStream, EncryptionKey)} whereas
* {@link #MIN_PASSWORD_HASH_ITERATIONS} is used to hash single passwords in a table that is potentially
* exposed to a rainbow attack.
* @see <a href="https://www.baeldung.com/java-secure-aes-key">AES Keys</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8018#section-4.2">RFC 8018</a>
* @see <a href="https://github.com/openssl/openssl/blob/dc43f080c5d60ef76df4087c1cf53a4bbaad93bd/apps/enc.c#L33">OpenSSL key derivation iteration count constant (permalink, check for updates on master branch)</a>
* @see <a href="https://docs.openssl.org/3.4/man1/openssl-enc/#options">openssl-enc documentation (see -iter and -pbkdf2 option for iteration count, check for updates on master branch)</a>
*/
protected int getKeyDerivationIterationCount() {
return 3557;
return 10000;
}

/**
Expand Down Expand Up @@ -558,7 +575,13 @@ protected String getDigestAlgorithmProvider() {

/**
* @return The key-derivation algorithm (algorithm to create a key based on a password) to use for the
* encryption/decryption.
* encryption/decryption (current version, see {@link #ENCRYPTION_COMPATIBILITY_HEADER}).
* <p>
* Do not confuse this parameter with {@link #getPasswordHashSecretKeyAlgorithm()}. This parameter is used to derive
* a PBEKey for {@link #encrypt(InputStream, OutputStream, EncryptionKey)} whereas
* {@link #getPasswordHashSecretKeyAlgorithm()} is used to hash single passwords in a table that is potentially
* exposed to a rainbow attack.
* @see <a href="https://docs.openssl.org/3.4/man1/openssl-enc/#options">openssl-enc documentation (see -md option, check for updates on master branch)</a>
*/
protected String getSecretKeyAlgorithm() {
// Password-based key-derivation algorithm (<a href="http://tools.ietf.org/search/rfc2898">PKCS #5 2.0</a>)
Expand Down Expand Up @@ -668,21 +691,21 @@ public String keyStoreToHumanReadableText(InputStream keyStoreInput, String stor
@Override
public String toString() {
return "Implementor: " + getClass().getName() + "\n"
+ "MinPasswordHashIterations: " + MIN_PASSWORD_HASH_ITERATIONS + "\n"
+ "MacAlgorithm: " + getMacAlgorithm() + "\n"
+ "MacAlgorithmProvider: " + getMacAlgorithmProvider() + "\n"
+ "KeyDerivationIterationCount (PBE): " + getKeyDerivationIterationCount() + "\n"
+ "SignatureAlgorithm: " + getSignatureAlgorithm() + "\n"
+ "SignatureProvider: " + getSignatureProvider() + "\n"
+ "KeyPairGenerationAlgorithm: " + getKeyPairGenerationAlgorithm() + "\n"
+ "EllipticCurveName: " + getEllipticCurveName() + "\n"
+ "DigestAlgorithm: " + getDigestAlgorithm() + "\n"
+ "DigestAlgorithmProvider: " + getDigestAlgorithmProvider() + "\n"
+ "SecretKeyAlgorithm: " + getSecretKeyAlgorithm() + "\n"
+ "PasswordHashSecretKeyAlgorithm: " + getPasswordHashSecretKeyAlgorithm() + "\n"
+ "CipherAlgorithm: " + getCipherAlgorithm() + "\n"
+ "CipherAlgorithmProvider: " + getCipherAlgorithmProvider() + "\n"
+ "CipherAlgorithmMode: " + getCipherAlgorithmMode() + "\n"
+ "CipherAlgorithmPadding: " + getCipherAlgorithmPadding() + "\n";
+ "MinPasswordHashIterations: " + MIN_PASSWORD_HASH_ITERATIONS + "\n"
+ "MacAlgorithm: " + getMacAlgorithm() + "\n"
+ "MacAlgorithmProvider: " + getMacAlgorithmProvider() + "\n"
+ "KeyDerivationIterationCount (PBE): " + getKeyDerivationIterationCount() + "\n"
+ "SignatureAlgorithm: " + getSignatureAlgorithm() + "\n"
+ "SignatureProvider: " + getSignatureProvider() + "\n"
+ "KeyPairGenerationAlgorithm: " + getKeyPairGenerationAlgorithm() + "\n"
+ "EllipticCurveName: " + getEllipticCurveName() + "\n"
+ "DigestAlgorithm: " + getDigestAlgorithm() + "\n"
+ "DigestAlgorithmProvider: " + getDigestAlgorithmProvider() + "\n"
+ "SecretKeyAlgorithm: " + getSecretKeyAlgorithm() + "\n"
+ "PasswordHashSecretKeyAlgorithm: " + getPasswordHashSecretKeyAlgorithm() + "\n"
+ "CipherAlgorithm: " + getCipherAlgorithm() + "\n"
+ "CipherAlgorithmProvider: " + getCipherAlgorithmProvider() + "\n"
+ "CipherAlgorithmMode: " + getCipherAlgorithmMode() + "\n"
+ "CipherAlgorithmPadding: " + getCipherAlgorithmPadding() + "\n";
}
}