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