diff --git a/powerauth-nextstep-model/src/main/java/io/getlime/security/powerauth/lib/nextstep/model/entity/enumeration/HashAlgorithm.java b/powerauth-nextstep-model/src/main/java/io/getlime/security/powerauth/lib/nextstep/model/entity/enumeration/HashAlgorithm.java
index 6a06a6636..c076fdc30 100644
--- a/powerauth-nextstep-model/src/main/java/io/getlime/security/powerauth/lib/nextstep/model/entity/enumeration/HashAlgorithm.java
+++ b/powerauth-nextstep-model/src/main/java/io/getlime/security/powerauth/lib/nextstep/model/entity/enumeration/HashAlgorithm.java
@@ -17,11 +17,14 @@
*/
package io.getlime.security.powerauth.lib.nextstep.model.entity.enumeration;
+import lombok.Getter;
+
/**
* Enumeration representing hashing algorithms.
*
* @author Roman Strobl, roman.strobl@wultra.com
*/
+@Getter
public enum HashAlgorithm {
/**
@@ -37,35 +40,30 @@ public enum HashAlgorithm {
/**
* Algorithm argon2id.
*/
- ARGON_2ID("argon2id", 2);
+ ARGON_2ID("argon2id", 2),
+
+ BCRYPT("bcrypt");
private final String name;
- private final int id;
+ private final Integer id;
/**
* Hash algorithm constructor.
- * @param name Algorithm name for Modular Crypt Format.
- * @param id Algorithm ID in Bouncy Castle library.
+ * @param name Algorithm name.
*/
- HashAlgorithm(String name, int id) {
+ HashAlgorithm(String name) {
this.name = name;
- this.id = id;
+ this.id = null;
}
/**
- * Get algorithm name for Modular Crypt Format.
- * @return Algorithm name.
- */
- public String getName() {
- return name;
- }
-
- /**
- * Get algorithm ID in Bouncy Castle library.
- * @return Algorithm ID.
+ * Hash algorithm constructor for Argon family of algorithms.
+ * @param name Algorithm name for Modular Crypt Format.
+ * @param id Algorithm ID in Bouncy Castle library (Argon algorithms only).
*/
- public int getId() {
- return id;
+ HashAlgorithm(String name, int id) {
+ this.name = name;
+ this.id = id;
}
}
diff --git a/powerauth-nextstep/src/main/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionService.java b/powerauth-nextstep/src/main/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionService.java
index 52b82e1f6..55ba2ff9c 100644
--- a/powerauth-nextstep/src/main/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionService.java
+++ b/powerauth-nextstep/src/main/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionService.java
@@ -37,6 +37,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -100,6 +101,10 @@ public CredentialValue protectCredential(String credentialValue, CredentialEntit
final String hashedValue = argon2Hash.toString();
return credentialValueConverter.toDBValue(hashedValue, userId, credentialDefinition);
}
+ case BCRYPT -> {
+ String hashedValue = BCrypt.hashpw(credentialValue, BCrypt.gensalt());
+ return credentialValueConverter.toDBValue(hashedValue, userId, credentialDefinition);
+ }
default -> throw new InvalidConfigurationException("Unsupported hashing algorithm: " + algorithm);
}
}
@@ -132,6 +137,13 @@ public boolean verifyCredential(String credentialValue, CredentialEntity credent
}
return succeeded;
}
+ case BCRYPT -> {
+ boolean succeeded = BCrypt.checkpw(credentialValue, decryptedCredentialValue);
+ if (succeeded) {
+ updateStoredCredentialValueIfRequired(credentialValue, credential);
+ }
+ return succeeded;
+ }
default -> throw new InvalidConfigurationException("Unsupported hashing algorithm: " + algorithm);
}
}
@@ -154,6 +166,7 @@ public boolean verifyCredentialHistory(String credentialValue, CredentialHistory
final HashAlgorithm algorithm = hashingConfig.getAlgorithm();
return switch (algorithm) {
case ARGON_2I, ARGON_2D, ARGON_2ID -> verifyCredentialUsingArgon2(credentialValue, algorithm, decryptedCredentialValue);
+ case BCRYPT -> BCrypt.checkpw(credentialValue, decryptedCredentialValue);
};
}
@@ -335,7 +348,7 @@ private void updateStoredCredentialValueIfRequired(String credentialValue, Crede
updateRequired = true;
} else {
// Check actual argon2 parameters from the hash in the database and compare them with credential definition
- updateRequired = updateRequired || !argon2ParamMatch(extractCredentialValue(credential), credentialDefinition.getHashingConfig().getAlgorithm(), credential.getHashingConfig().getParameters());
+ updateRequired = updateRequired || (credentialDefinition.getHashingConfig().getAlgorithm() != HashAlgorithm.BCRYPT && !argon2ParamMatch(extractCredentialValue(credential), credentialDefinition.getHashingConfig().getAlgorithm(), credential.getHashingConfig().getParameters()));
}
}
diff --git a/powerauth-nextstep/src/test/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionServiceTest.java b/powerauth-nextstep/src/test/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionServiceTest.java
new file mode 100644
index 000000000..32fda9718
--- /dev/null
+++ b/powerauth-nextstep/src/test/java/io/getlime/security/powerauth/app/nextstep/service/CredentialProtectionServiceTest.java
@@ -0,0 +1,97 @@
+/*
+ * PowerAuth Web Flow and related software components
+ * Copyright (C) 2024 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.nextstep.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.getlime.security.powerauth.app.nextstep.NextStepTest;
+import io.getlime.security.powerauth.app.nextstep.repository.model.entity.CredentialDefinitionEntity;
+import io.getlime.security.powerauth.app.nextstep.repository.model.entity.CredentialEntity;
+import io.getlime.security.powerauth.app.nextstep.repository.model.entity.HashConfigEntity;
+import io.getlime.security.powerauth.app.nextstep.repository.model.entity.UserIdentityEntity;
+import io.getlime.security.powerauth.lib.nextstep.model.entity.CredentialValue;
+import io.getlime.security.powerauth.lib.nextstep.model.entity.enumeration.EncryptionAlgorithm;
+import io.getlime.security.powerauth.lib.nextstep.model.entity.enumeration.HashAlgorithm;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for password hashing.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+class CredentialProtectionServiceTest extends NextStepTest {
+
+ @Autowired
+ private CredentialProtectionService credentialProtectionService;
+
+ @Test
+ void testBcrypt() throws Exception {
+ final CredentialEntity credentialEntity = new CredentialEntity();
+ final UserIdentityEntity user = new UserIdentityEntity();
+ final HashConfigEntity hashConfig = new HashConfigEntity();
+ final CredentialDefinitionEntity credentialDefinition = new CredentialDefinitionEntity();
+ credentialDefinition.setEncryptionAlgorithm(EncryptionAlgorithm.NO_ENCRYPTION);
+ credentialDefinition.setHashingConfig(hashConfig);
+ hashConfig.setAlgorithm(HashAlgorithm.BCRYPT);
+ hashConfig.setParameters("{}");
+ user.setUserId("test");
+ credentialEntity.setUser(user);
+ credentialEntity.setCredentialDefinition(credentialDefinition);
+ credentialEntity.setHashingConfig(hashConfig);
+ final CredentialValue hashed = credentialProtectionService.protectCredential("test", credentialEntity);
+ credentialEntity.setValue(hashed.getValue());
+ assertEquals(EncryptionAlgorithm.NO_ENCRYPTION, hashed.getEncryptionAlgorithm());
+ assertNotEquals("test", hashed.getValue());
+ assertTrue(hashed.getValue().startsWith("$2a$10$"));
+ assertTrue(credentialProtectionService.verifyCredential("test", credentialEntity));
+ }
+
+ @Test
+ void testArgon2() throws Exception {
+ final CredentialEntity credentialEntity = new CredentialEntity();
+ final UserIdentityEntity user = new UserIdentityEntity();
+ final HashConfigEntity hashConfig = new HashConfigEntity();
+ final CredentialDefinitionEntity credentialDefinition = new CredentialDefinitionEntity();
+ credentialDefinition.setEncryptionAlgorithm(EncryptionAlgorithm.NO_ENCRYPTION);
+ credentialDefinition.setHashingConfig(hashConfig);
+ hashConfig.setAlgorithm(HashAlgorithm.ARGON_2I);
+ final Map params = Map.of(
+ "version", "19",
+ "iterations", "4",
+ "memory", "16",
+ "parallelism", "2",
+ "outputLength", "32"
+ );
+ hashConfig.setParameters(new ObjectMapper().writeValueAsString(params));
+ user.setUserId("test");
+ credentialEntity.setUser(user);
+ credentialEntity.setCredentialDefinition(credentialDefinition);
+ credentialEntity.setHashingConfig(hashConfig);
+ final CredentialValue hashed = credentialProtectionService.protectCredential("test", credentialEntity);
+ credentialEntity.setValue(hashed.getValue());
+ assertEquals(EncryptionAlgorithm.NO_ENCRYPTION, hashed.getEncryptionAlgorithm());
+ assertNotEquals("test", hashed.getValue());
+ assertTrue(hashed.getValue().startsWith("$argon2i$v=19$m=65536,t=4,p=2$"));
+ assertTrue(credentialProtectionService.verifyCredential("test", credentialEntity));
+ }
+
+}
\ No newline at end of file