Skip to content
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 @@ -131,6 +131,17 @@ public void testRandomBytes() {
Assert.assertTrue(illegalExc);
}

@Test
public void testCreatePasswordHash() {
char[] password = "AwesomePassword".toCharArray();
byte[] salt = SecurityUtility.createRandomBytes();
PasswordHash passwordHash = SecurityUtility.createPasswordHash(password, salt);

// ensure created password hash matches given inputs
Assert.assertArrayEquals(password, passwordHash.getPassword());
Assert.assertArrayEquals(salt, passwordHash.getSalt());
}

@Test
public void testHash() {
final byte[] data = "testdata".getBytes(ENCODING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ default byte[] createHash(byte[] data, byte[] salt) {
/**
* Creates a hash for the given data using the given salt.
* <p>
* <b>Important:</b> For hashing of passwords use {@link ISecurityProvider#createPasswordHash(char[], byte[])}!
* <b>Important:</b> For hashing of passwords use {@link SecurityUtility#hashPassword(char[], byte[])}!
* </p>
* <p><b>Important 2:</b> For "normal" hashing use {@link SecurityUtility#hash(byte[])}</p>
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -137,7 +137,7 @@ public interface ISecurityProvider {
* <code>DigestInputStream.getMessageDigest().digest()</code>.
* </p>
* <p>
* <b>Important:</b> For hashing of passwords use {@link #createPasswordHash(char[], byte[])}!
* <b>Important:</b> For hashing of passwords use {@link SecurityUtility#hashPassword(char[], byte[])}!
* </p>
*
* @param stream
Expand All @@ -155,7 +155,7 @@ public interface ISecurityProvider {
* <code>DigestInputStream.getMessageDigest().digest()</code>.
* </p>
* <p>
* <b>Important:</b> For hashing of passwords use {@link #createPasswordHash(char[], byte[])}!
* <b>Important:</b> For hashing of passwords use {@link SecurityUtility#hashPassword(char[], byte[])}!
* </p>
*
* @param stream
Expand All @@ -167,36 +167,23 @@ public interface ISecurityProvider {
DigestOutputStream toHashingStream(OutputStream stream);

/**
* Creates a hash for the given password.<br>
* Creates a new {@link PasswordHash} used for hashing a password.
*
* @param password
* 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.
* @return the password hash
* @return The {@link PasswordHash} used for hashing a password.
* @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>
* </ul>
* @throws ProcessingException
* If there is an error creating the hash. <br>
*/
byte[] createPasswordHash(char[] password, byte[] salt);

/**
* 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 the
* expected hash.
* @since 11.0
*/
boolean verifyPasswordHash(char[] password, byte[] salt, byte[] expectedHash);
PasswordHash createPasswordHash(char[] password, byte[] salt);

/**
* Encrypts the given data using the given {@link EncryptionKey}.<br>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.scout.rt.platform.security;

import static org.eclipse.scout.rt.platform.util.Assertions.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.IntStream;

import org.eclipse.scout.rt.platform.util.MutablePair;

/**
* This class represents a password hash and can be used to hash a password and to verify it.
* Hash creation and hash verification are separated and take into account all known variations of hash computation.
* <p>
* Use {@link SecurityUtility#createPasswordHash(char[], byte[])} to create a {@link PasswordHash} that hashes a password
* with respect to the current security standard and all known older variations.
*/
public final class PasswordHash {

private final char[] m_password;
private final byte[] m_salt;
private final List<MutablePair<BiFunction<char[], byte[], byte[]>, byte[]>> m_hashGeneratorAndHash;

private PasswordHash(char[] password, byte[] salt, List<BiFunction<char[], byte[], byte[]>> hashGeneratorAndHash) {
m_password = password;
m_salt = salt;
m_hashGeneratorAndHash = hashGeneratorAndHash.stream()
.map(generator -> MutablePair.of(generator, (byte[]) null))
.toList();
}

public char[] getPassword() {
return Arrays.copyOf(m_password, m_password.length);
}

public byte[] getSalt() {
return Arrays.copyOf(m_salt, m_salt.length);
}

/**
* Creates a password hash. The hashing of the password is performed only once. The result will be cached.
*
* @return A {@code byte[]} containing the password hash.
*/
public byte[] get() {
return get(0);
}

private byte[] get(int index) {
MutablePair<BiFunction<char[], byte[], byte[]>, byte[]> hashGeneratorAndHash = m_hashGeneratorAndHash.get(index);
if (hashGeneratorAndHash.getRight() == null) {
BiFunction<char[], byte[], byte[]> hashGenerator = hashGeneratorAndHash.getLeft();
hashGeneratorAndHash.setRight(hashGenerator.apply(m_password, m_salt));
}
byte[] hash = hashGeneratorAndHash.getRight();
return Arrays.copyOf(hash, hash.length);
}

/**
* Verifies that the given expected hash matches this {@link PasswordHash}. Taking into account all known variations
* of hash computation.
*
* @param expectedHash
* The expected hash the given {@link PasswordHash} is tested against.
* @return true if the {@link PasswordHash} matches the given expected hash against one of the known hashing variations.
*/
public boolean verify(byte[] expectedHash) {
if (expectedHash == null) {
return false;
}
return IntStream.range(0, m_hashGeneratorAndHash.size())
.mapToObj(this::get)
.anyMatch(hash -> Arrays.equals(expectedHash, hash));
}

public static final class PasswordHashBuilder {

private final char[] m_password;
private final byte[] m_salt;
private final List<BiFunction<char[], byte[], byte[]>> m_hashGenerators;

/**
* Use this method to get a {@link PasswordHashBuilder} to build a new {@link PasswordHash}.
*
* @param password
* The password to create the hash for. Must not be {@code null} or empty.
* @param salt
* The salt to use. Use {@link SecurityUtility#createRandomBytes(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 defaultHashGenerator
* A {@link BiFunction} that can compute a hash from a password and a salt.
* The default generator must be selected so that the hash is computed in line with the current security standard.
* @return A {@link PasswordHashBuilder} used to build a new {@link PasswordHash}.
*/
public static PasswordHashBuilder of(char[] password, byte[] salt, BiFunction<char[], byte[], byte[]> defaultHashGenerator) {
return new PasswordHashBuilder(password, salt)
.withVariant(defaultHashGenerator);
}

private PasswordHashBuilder(char[] password, byte[] salt) {
assertGreater(assertNotNull(password, "password must not be null.").length, 0, "empty password is not allowed.");
assertGreater(assertNotNull(salt, "salt must not be null.").length, 0, "empty salt is not allowed.");

m_password = password;
m_salt = salt;
m_hashGenerators = new ArrayList<>();
}

/**
* Use this method to add variations of the hash computation. This is necessary when the security standard for hashing
* changes, for example, due to a change in the number of iterations or the algorithm itself.
*
* @param hashGenerator
* A {@link BiFunction} that can compute a hash from a password and a salt.
* @return A {@link PasswordHashBuilder} used to build a new {@link PasswordHash}.
*/
public PasswordHashBuilder withVariant(BiFunction<char[], byte[], byte[]> hashGenerator) {
m_hashGenerators.add(assertNotNull(hashGenerator, "hashGenerator must not be null."));
return this;
}

public PasswordHash build() {
return new PasswordHash(m_password, m_salt, m_hashGenerators);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -325,7 +325,7 @@ public static SecureRandom createSecureRandom() {
* @see ISecurityProvider#createPasswordHash(char[], byte[])
*/
public static byte[] hashPassword(char[] password, byte[] salt) {
return SECURITY_PROVIDER.get().createPasswordHash(password, salt);
return createPasswordHash(password, salt).get();
}

/**
Expand All @@ -338,7 +338,21 @@ public static byte[] hashPassword(char[] password, byte[] salt) {
* @since 11.0
*/
public static boolean verifyPasswordHash(char[] password, byte[] salt, byte[] expectedHash) {
return SECURITY_PROVIDER.get().verifyPasswordHash(password, salt, expectedHash);
return createPasswordHash(password, salt).verify(expectedHash);
}

/**
* @see PasswordHash#verify(byte[])
*/
public static boolean verifyPasswordHash(PasswordHash passwordHash, byte[] expectedHash) {
return passwordHash.verify(expectedHash);
}

/**
* See {@link ISecurityProvider#createPasswordHash(char[], byte[])}
*/
public static PasswordHash createPasswordHash(char[] password, byte[] salt) {
return SECURITY_PROVIDER.get().createPasswordHash(password, salt);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,7 +43,6 @@
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
Expand All @@ -60,6 +59,7 @@

import org.eclipse.scout.rt.platform.Order;
import org.eclipse.scout.rt.platform.exception.ProcessingException;
import org.eclipse.scout.rt.platform.security.PasswordHash.PasswordHashBuilder;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.Base64Utility;

Expand Down Expand Up @@ -201,8 +201,17 @@ protected static byte[] generateCompatibilityHeader(int keyLen, String secretKey
}

@Override
public byte[] createPasswordHash(char[] password, byte[] salt) {
return createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS);
public PasswordHash createPasswordHash(char[] password, byte[] salt) {
return PasswordHashBuilder.of(password, salt, (p, s) -> this.createPasswordHash(p, s, MIN_PASSWORD_HASH_ITERATIONS))
// in case the computation of the hash changes (i.e. the iterations or the implementation itself changes), add the "old" computation as a variant.
// example: the current one is the 2023 implementation, therefore we add all older implementations as a variant (2021, 2019, etc.)
.withVariant((p, s) -> this.createPasswordHash(p, s, MIN_PASSWORD_HASH_ITERATIONS_2021))
.withVariant((p, s) -> this.createPasswordHash(p, s, MIN_PASSWORD_HASH_ITERATIONS_2019))
.withVariant((p, s) -> this.createPasswordHash(p, s, MIN_PASSWORD_HASH_ITERATIONS_2016))
// 2014 variants
.withVariant((p, s) -> createHash(new ByteArrayInputStream(new String(p).getBytes(StandardCharsets.UTF_8)), s, 3557))
.withVariant((p, s) -> createHash(new ByteArrayInputStream(new String(p).getBytes(StandardCharsets.UTF_16)), s, 3557))
.build();
}

/**
Expand Down Expand Up @@ -248,30 +257,6 @@ 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 true;
}
if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2021))) {
return true;
}
if (Arrays.equals(expectedHash, createPasswordHash(password, salt, MIN_PASSWORD_HASH_ITERATIONS_2019))) {
return true;
}
if (Arrays.equals(expectedHash, 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))) {
return true;
}
if (Arrays.equals(expectedHash, createHash(new ByteArrayInputStream(new String(password).getBytes(StandardCharsets.UTF_16)), salt, 3557))) {
return true;
}
return false;
}

@Override
public void encrypt(InputStream clearTextData, OutputStream encryptedData, EncryptionKey key) {
doCrypt(clearTextData, encryptedData, key, Cipher.ENCRYPT_MODE);
Expand Down
Loading