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 4e9fa448a4f..d316e244af1 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -21,6 +21,7 @@ import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.util.Arrays; +import java.util.Set; import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.util.Assertions.AssertionException; @@ -253,6 +254,32 @@ public void testHashPassword() { Assert.assertTrue(ok); } + @Test + public void testVerifyPassword() { + char[] password1 = "password-one".toCharArray(); + char[] password2 = "password-two".toCharArray(); + char[] password3 = "password-three".toCharArray(); + char[] password4 = "password-four".toCharArray(); + + byte[] salt = SecurityUtility.createRandomBytes(); + byte[] salt2 = SecurityUtility.createRandomBytes(); + + byte[] hash1 = SecurityUtility.hashPassword(password1, salt); + byte[] hash2 = SecurityUtility.hashPassword(password2, salt); + byte[] hash3 = SecurityUtility.hashPassword(password3, salt2); + Set expectedHashes = Set.of(hash1, hash2, hash3); + + Assert.assertTrue(SecurityUtility.verifyPasswordHash(password1, salt, expectedHashes)); + Assert.assertTrue(SecurityUtility.verifyPasswordHash(password2, salt, expectedHashes)); + Assert.assertFalse(SecurityUtility.verifyPasswordHash(password3, salt, expectedHashes)); + Assert.assertFalse(SecurityUtility.verifyPasswordHash(password4, salt, expectedHashes)); + + Assert.assertFalse(SecurityUtility.verifyPasswordHash(password1, salt2, expectedHashes)); + Assert.assertFalse(SecurityUtility.verifyPasswordHash(password2, salt2, expectedHashes)); + Assert.assertTrue(SecurityUtility.verifyPasswordHash(password3, salt2, expectedHashes)); + Assert.assertFalse(SecurityUtility.verifyPasswordHash(password4, salt2, expectedHashes)); + } + @Test public void testCreateMac() { byte[] data = "testdata".getBytes(); 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 f25aafa8940..f1e829b73f5 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -14,6 +14,7 @@ import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.SecureRandom; +import java.util.Collection; import org.eclipse.scout.rt.platform.ApplicationScoped; import org.eclipse.scout.rt.platform.exception.ProcessingException; @@ -198,6 +199,17 @@ public interface ISecurityProvider { */ boolean verifyPasswordHash(char[] password, byte[] salt, byte[] expectedHash); + /** + * This method is recommended in combination with {@link #createPasswordHash(char[], byte[])} where the iteration + * count is omitted. This has the advantage that the check of the password hash is independent of the creation of the + * hash. In case the iteration count is increased yearly, this method checks if the hash is valid + * + * @return true if calculated password hash created with {@link #createPasswordHash(char[], byte[])} matches one of + * the expected hashes. + * @since 25.2 + */ + boolean verifyPasswordHash(char[] password, byte[] salt, Collection expectedHashes); + /** * Encrypts the given data using the given {@link EncryptionKey}.
* Use {@link #decrypt(InputStream, OutputStream, EncryptionKey)} to decrypt the data again using the same key. 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 599a10b3142..47a64805e31 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -22,6 +22,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -341,6 +342,19 @@ public static boolean verifyPasswordHash(char[] password, byte[] salt, byte[] ex return SECURITY_PROVIDER.get().verifyPasswordHash(password, salt, expectedHash); } + /** + * This method is recommended in combination with {@link #hashPassword(char[], byte[])} where the iteration count is + * omitted. This has the advantage that the check of the password hash is independent of the creation of the hash. In + * case the iteration count is increased yearly, this method checks if the hash is valid + * + * @return true if calculated password hash created with {@link #hashPassword(char[], byte[])} matches one of the + * expected hashes. + * @since 25.2 + */ + public static boolean verifyPasswordHash(char[] password, byte[] salt, Collection expectedHashes) { + return SECURITY_PROVIDER.get().verifyPasswordHash(password, salt, expectedHashes); + } + /** * Creates a hash for the given data using the given salt. *

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 8a0cf7921b2..ac21ed7373a 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2025 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -44,8 +44,10 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.crypto.Cipher; @@ -62,6 +64,7 @@ import org.eclipse.scout.rt.platform.exception.ProcessingException; import org.eclipse.scout.rt.platform.util.Assertions; import org.eclipse.scout.rt.platform.util.Base64Utility; +import org.eclipse.scout.rt.platform.util.CollectionUtility; /** * Utility class for encryption/decryption, hashing, random number generation and digital signatures.
@@ -250,23 +253,32 @@ public byte[] createPasswordHash(char[] password, byte[] salt, int iterations) { @Override public boolean verifyPasswordHash(char[] password, byte[] salt, byte[] expectedHash) { - if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS))) { + return verifyPasswordHash(password, salt, CollectionUtility.hashSet(expectedHash)); + } + + @Override + public boolean verifyPasswordHash(char[] password, byte[] salt, Collection expectedHashes) { + if (CollectionUtility.isEmpty(expectedHashes)) { + return false; + } + Predicate acceptHash = hash -> expectedHashes.stream().anyMatch(expectedHash -> Arrays.equals(expectedHash, hash)); + if (acceptHash.test(createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS))) { return true; } - if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2021))) { + if (acceptHash.test(createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2021))) { return true; } - if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2019))) { + if (acceptHash.test(createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2019))) { return true; } - if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2016))) { + if (acceptHash.test(createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2016))) { return true; } //2014 variants - if (Arrays.equals(expectedHash, createHash(new ByteArrayInputStream(new String(password).getBytes(StandardCharsets.UTF_8)), salt, 3557))) { + if (acceptHash.test(createHash(new ByteArrayInputStream(new String(password).getBytes(StandardCharsets.UTF_8)), salt, 3557))) { return true; } - if (Arrays.equals(expectedHash, createHash(new ByteArrayInputStream(new String(password).getBytes(StandardCharsets.UTF_16)), salt, 3557))) { + if (acceptHash.test(createHash(new ByteArrayInputStream(new String(password).getBytes(StandardCharsets.UTF_16)), salt, 3557))) { return true; } return false;