diff --git a/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/security/SecurityUtilityTest.java b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/security/SecurityUtilityTest.java index e18389834d2..a4516bde550 100644 --- a/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/security/SecurityUtilityTest.java +++ b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/security/SecurityUtilityTest.java @@ -321,7 +321,7 @@ public void testDecryptionApiStability() { 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)); } diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/ISecurityProvider.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/ISecurityProvider.java index d77205f725b..baf73780374 100644 --- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/ISecurityProvider.java +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/ISecurityProvider.java @@ -59,6 +59,16 @@ public interface ISecurityProvider { * */ String ENCRYPTION_COMPATIBILITY_HEADER_2023_V1 = "[2023:v1]"; + /** + *
+   * secretKeyAlgorithm: PBKDF2WithHmacSHA256
+   * cipherAlgorithm/Provider: AES/SunJCE
+   * GCM init vector length: 16
+   * GCM auth tag bit length: 128
+   * key derivation iteration count: 10000
+   * 
+ */ + String ENCRYPTION_COMPATIBILITY_HEADER_2024_V1 = "[2024:v1]"; String ENCRYPTION_COMPATIBILITY_HEADER = ENCRYPTION_COMPATIBILITY_HEADER_2023_V1; /** diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SecurityUtility.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SecurityUtility.java index 3bce0d1047c..a8f115b9e4f 100644 --- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SecurityUtility.java +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SecurityUtility.java @@ -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); } @@ -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); } @@ -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 + 6; ByteArrayOutputStream result = new ByteArrayOutputStream(expectedOutSize); encrypt(input, result, key); return result.toByteArray(); diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SunSecurityProvider.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SunSecurityProvider.java index 080e18382ad..ec6329e02b4 100644 --- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SunSecurityProvider.java +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/security/SunSecurityProvider.java @@ -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, @@ -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, @@ -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 @@ -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) { @@ -187,6 +179,27 @@ 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); @@ -194,29 +207,29 @@ public byte[] createPasswordHash(char[] password, byte[] salt) { /** * @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.
- * 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.
- * 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.
+ * 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.
+ * 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}:
- * + * If one of the following conditions is {@code true}:
+ * * @throws ProcessingException - * If there is an error creating the hash.
+ * If there is an error creating the hash.
*/ public byte[] createPasswordHash(char[] password, byte[] salt, int iterations) { assertGreater(assertNotNull(password, "password must not be null.").length, 0, "empty password is not allowed."); @@ -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); } } @@ -487,17 +500,21 @@ protected String getMacAlgorithmProvider() { } /** - * @return Iteration count for key derivation. AES Keys - *

- * 2023/05: at least 1000 - *

- * 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}). + *

+ * RFC 8018 recommends to use at least 1000 iterations, OpenSSL currently uses 10000 iterations by default. + *

+ * 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 AES Keys + * @see RFC 8018 + * @see OpenSSL key derivation iteration count constant (permalink, check for updates on master branch) + * @see openssl-enc documentation (see -iter and -pbkdf2 option for iteration count, check for updates on master branch) */ protected int getKeyDerivationIterationCount() { - return 3557; + return 10000; } /** @@ -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}). + *

+ * 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 openssl-enc documentation (see -md option, check for updates on master branch) */ protected String getSecretKeyAlgorithm() { // Password-based key-derivation algorithm (PKCS #5 2.0) @@ -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"; } }