Skip to content
Merged
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
7 changes: 7 additions & 0 deletions vaultonapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
<mapstruct.version>1.6.3</mapstruct.version>
</properties>
<dependencies>
<!-- Source: https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import java.util.UUID;

public interface CryptoService {
SecureBuffer computeStoredVerifier(SecureBuffer verifierRaw, SecureBuffer salt);
SecureBuffer computeStoredVerifier(
SecureBuffer verifierRaw, SecureBuffer salt, int iterations, byte[] pepper);

SecureBuffer generateRandomBytes(int length);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ void wipe() {
}
}

private final int pbkdf2Iterations;
private final byte[] verifierPepper;
private final CryptoService cryptoService;
private final UserRepository userRepository;

Expand Down Expand Up @@ -97,7 +99,8 @@ public UserCreationResult createUser(UserCreationInput input) {
private SaltedVerifier compute(SecureBuffer raw) {
SecureBuffer salt = cryptoService.generateRandomBytes(CryptoConstants.SALT_LEN);
try {
SecureBuffer stored = cryptoService.computeStoredVerifier(raw, salt);
SecureBuffer stored =
cryptoService.computeStoredVerifier(raw, salt, pbkdf2Iterations, verifierPepper);
return new SaltedVerifier(stored, salt);
} catch (Exception e) {
salt.wipe();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package dev.vaulton.vaultonapi.infrastructure.config;

import dev.vaulton.vaultonapi.domain.repository.UserRepository;
import dev.vaulton.vaultonapi.domain.service.shared.CryptoService;
import dev.vaulton.vaultonapi.domain.service.usercreation.UserCreationService;
import dev.vaulton.vaultonapi.domain.service.usercreation.UserCreationServiceImpl;
import dev.vaulton.vaultonapi.infrastructure.crypto.BouncyCryptoAdapter;
import java.security.SecureRandom;
import java.util.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CryptoConfig {

@Bean
public CryptoService cryptoService(
@Value("${vaulton.auth.fake-salt-secret}") String fakeSaltB64) {
byte[] secret = Base64.getDecoder().decode(fakeSaltB64);

return new BouncyCryptoAdapter(new SecureRandom(), secret);
}

@Bean
public UserCreationService userCreationService(
CryptoService cryptoService,
UserRepository userRepository,
@Value("${vaulton.auth.pepper}") String pepperB64,
@Value("${vaulton.auth.iterations}") int iterations) {

byte[] pepper = Base64.getDecoder().decode(pepperB64);

return new UserCreationServiceImpl(iterations, pepper, cryptoService, userRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package dev.vaulton.vaultonapi.infrastructure.crypto;

import dev.vaulton.vaultonapi.domain.crypto.CryptoConstants;
import dev.vaulton.vaultonapi.domain.crypto.SecureBuffer;
import dev.vaulton.vaultonapi.domain.service.shared.CryptoService;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import lombok.RequiredArgsConstructor;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;

@RequiredArgsConstructor
public class BouncyCryptoAdapter implements CryptoService {
Comment thread
Treszyk marked this conversation as resolved.
private final SecureRandom randGen;
private final byte[] fakeSaltSecret;

void zeroizeBuffer(byte[] buffer) {
if (buffer != null) java.util.Arrays.fill(buffer, (byte) 0x00);
}

@Override
public SecureBuffer computeStoredVerifier(
SecureBuffer verifierRaw, SecureBuffer salt, int iterations, byte[] pepper) {
PKCS5S2ParametersGenerator gen = null;
byte[] verifierBytes = null;
byte[] saltBytes = null;
byte[] pepperedVerifier = null;
byte[] hash = null;

try {
gen = new PKCS5S2ParametersGenerator(new SHA256Digest());

verifierBytes = verifierRaw.bytes();
saltBytes = salt.bytes();
pepperedVerifier = new byte[verifierBytes.length + pepper.length];
System.arraycopy(verifierBytes, 0, pepperedVerifier, 0, verifierBytes.length);
System.arraycopy(pepper, 0, pepperedVerifier, verifierBytes.length, pepper.length);

gen.init(pepperedVerifier, saltBytes, iterations);
KeyParameter keyParameter = (KeyParameter) gen.generateDerivedParameters(256);
hash = keyParameter.getKey();

return new SecureBuffer(hash);
} finally {
zeroizeBuffer(pepperedVerifier);
zeroizeBuffer(verifierBytes);
zeroizeBuffer(saltBytes);
zeroizeBuffer(hash);
}
}

@Override
public SecureBuffer generateRandomBytes(int length) {
byte[] randBytes = null;
try {
randBytes = new byte[length];
randGen.nextBytes(randBytes);

return new SecureBuffer(randBytes);
} finally {
zeroizeBuffer(randBytes);
}
}

@Override
public SecureBuffer computeFakeSalt(UUID accountId) {
byte[] idBytes = null;
byte[] contextBytes = null;
byte[] combined = null;
byte[] hmacResult = null;
byte[] truncatedResult = null;

ByteBuffer idBuffer = null;
Mac mac = null;
SecretKeySpec spec = new SecretKeySpec(fakeSaltSecret, "HmacSHA256");

try {
contextBytes = "Vaulton.FakeSalt.v1".getBytes(StandardCharsets.UTF_8);

idBuffer = ByteBuffer.allocate(CryptoConstants.SALT_LEN);
idBuffer.putLong(accountId.getMostSignificantBits());
idBuffer.putLong(accountId.getLeastSignificantBits());
idBytes = idBuffer.array();

combined = new byte[idBytes.length + contextBytes.length];
System.arraycopy(contextBytes, 0, combined, 0, contextBytes.length);
System.arraycopy(idBytes, 0, combined, contextBytes.length, idBytes.length);

mac = Mac.getInstance("HmacSHA256");
mac.init(spec);
hmacResult = mac.doFinal(combined);

truncatedResult = new byte[CryptoConstants.SALT_LEN];
System.arraycopy(hmacResult, 0, truncatedResult, 0, truncatedResult.length);

return new SecureBuffer(truncatedResult);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} finally {
zeroizeBuffer(idBytes);
if (idBuffer != null) idBuffer.clear();
if (mac != null) mac.reset();
zeroizeBuffer(contextBytes);
zeroizeBuffer(combined);
zeroizeBuffer(hmacResult);
zeroizeBuffer(truncatedResult);
}
}
}
6 changes: 6 additions & 0 deletions vaultonapi/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ spring:

server:
port: 8080

vaulton:
auth:
pepper: ${Auth__VerifierPepper}
fake-salt-secret: ${Auth__FakeSaltSecret}
iterations: ${Auth__VerifierPbkdf2Iterations:600000}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ class UserCreationServiceTest {

@Mock private UserRepository userRepository;

private UserCreationService registrationService;
private UserCreationService userCreationService;

@BeforeEach
void setUp() {
registrationService = new UserCreationServiceImpl(cryptoService, userRepository);
userCreationService =
new UserCreationServiceImpl(1000, new byte[32], cryptoService, userRepository);
}

@Test
void shouldGenerateUniqueIdWhenPreRegisterIsCalled() {
when(userRepository.existsById(any())).thenReturn(false);

UUID generatedId = registrationService.preRegister();
UUID generatedId = userCreationService.preRegister();

assertNotNull(generatedId);
verify(userRepository, atLeastOnce()).existsById(any());
Expand All @@ -50,7 +51,7 @@ void shouldGenerateUniqueIdWhenPreRegisterIsCalled() {
void shouldReturnUnsupportedSchemaWhenVersionIsInvalid() {
UserCreationInput input = createValidInput(UUID.randomUUID(), 999);

UserCreationResult result = registrationService.createUser(input);
UserCreationResult result = userCreationService.createUser(input);

assertInstanceOf(UserCreationResult.Failure.class, result);
assertEquals(
Expand All @@ -64,10 +65,10 @@ void shouldReturnSuccessWhenInputIsValid() {

when(userRepository.existsById(accountId)).thenReturn(false);
when(cryptoService.generateRandomBytes(anyInt())).thenReturn(new SecureBuffer(new byte[16]));
when(cryptoService.computeStoredVerifier(any(), any()))
when(cryptoService.computeStoredVerifier(any(), any(), anyInt(), any()))
.thenReturn(new SecureBuffer(new byte[32]));

UserCreationResult result = registrationService.createUser(input);
UserCreationResult result = userCreationService.createUser(input);

assertInstanceOf(UserCreationResult.Success.class, result);
User user = ((UserCreationResult.Success) result).user();
Expand All @@ -82,10 +83,10 @@ void shouldReturnFailureWhenAccountAlreadyExists() {

when(userRepository.existsById(accountId)).thenReturn(true);
when(cryptoService.generateRandomBytes(anyInt())).thenReturn(new SecureBuffer(new byte[16]));
when(cryptoService.computeStoredVerifier(any(), any()))
when(cryptoService.computeStoredVerifier(any(), any(), anyInt(), any()))
.thenReturn(new SecureBuffer(new byte[32]));

UserCreationResult result = registrationService.createUser(input);
UserCreationResult result = userCreationService.createUser(input);

assertInstanceOf(UserCreationResult.Failure.class, result);
assertEquals(UserCreationError.ACCOUNT_EXISTS, ((UserCreationResult.Failure) result).error());
Expand All @@ -98,12 +99,12 @@ void shouldWipeSecretsOnFailure() {

when(userRepository.existsById(accountId)).thenReturn(true);
when(cryptoService.generateRandomBytes(anyInt())).thenReturn(new SecureBuffer(new byte[16]));
when(cryptoService.computeStoredVerifier(any(), any()))
when(cryptoService.computeStoredVerifier(any(), any(), anyInt(), any()))
.thenReturn(new SecureBuffer(new byte[32]));

registrationService.createUser(input);
userCreationService.createUser(input);

verify(cryptoService, times(3)).computeStoredVerifier(any(), any());
verify(cryptoService, times(3)).computeStoredVerifier(any(), any(), anyInt(), any());
verify(userRepository, atLeastOnce()).existsById(any());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package dev.vaulton.vaultonapi.infrastructure.crypto;

import static org.junit.jupiter.api.Assertions.*;

import dev.vaulton.vaultonapi.domain.crypto.CryptoConstants;
import dev.vaulton.vaultonapi.domain.crypto.SecureBuffer;
import java.security.SecureRandom;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

@SuppressWarnings("resource")
class BouncyCryptoAdapterTest {

private BouncyCryptoAdapter adapter;
private final byte[] fakeSaltSecret = new byte[32];

@BeforeEach
void setUp() {
adapter = new BouncyCryptoAdapter(new SecureRandom(), fakeSaltSecret);
}

@Test
void shouldGenerateRandomBytesOfRequestedLength() {

SecureBuffer testBuffer = adapter.generateRandomBytes(32);
SecureBuffer secondTestBuffer = adapter.generateRandomBytes(32);

assertEquals(32, testBuffer.length());
assertEquals(32, secondTestBuffer.length());

assertNotEquals(testBuffer, secondTestBuffer);
}

@Test
void shouldComputeConsistentVerifier() {
SecureBuffer verifierRaw = adapter.generateRandomBytes(CryptoConstants.VERIFIER_LEN);
SecureBuffer salt = adapter.generateRandomBytes(CryptoConstants.SALT_LEN);
int iterations = 1000;
byte[] pepper = adapter.generateRandomBytes(CryptoConstants.PEPPER_LEN).bytes();

SecureBuffer firstCompute =
adapter.computeStoredVerifier(verifierRaw, salt, iterations, pepper);
SecureBuffer secondCompute =
adapter.computeStoredVerifier(verifierRaw, salt, iterations, pepper);
SecureBuffer diffPepperCompute =
adapter.computeStoredVerifier(
verifierRaw,
salt,
iterations,
adapter.generateRandomBytes(CryptoConstants.PEPPER_LEN).bytes());

assertEquals(CryptoConstants.VERIFIER_LEN, firstCompute.length());
assertEquals(CryptoConstants.VERIFIER_LEN, secondCompute.length());
assertEquals(CryptoConstants.VERIFIER_LEN, diffPepperCompute.length());
assertEquals(firstCompute, secondCompute);

assertNotEquals(firstCompute, diffPepperCompute);
}

@Test
void shouldComputeDeterministicFakeSalt() {
UUID firstUUID = UUID.randomUUID();
UUID secondUUID = UUID.randomUUID();

SecureBuffer firstSalt = adapter.computeFakeSalt(firstUUID);
SecureBuffer secondSalt = adapter.computeFakeSalt(firstUUID);
SecureBuffer diffIdSalt = adapter.computeFakeSalt(secondUUID);

assertEquals(CryptoConstants.SALT_LEN, firstSalt.length());
assertEquals(CryptoConstants.SALT_LEN, secondSalt.length());
assertEquals(CryptoConstants.SALT_LEN, diffIdSalt.length());

assertEquals(firstSalt, secondSalt);
assertNotEquals(firstSalt, diffIdSalt);
}
}
15 changes: 5 additions & 10 deletions vaultonapi/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
vaulton:
auth:
pepper: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
fake-salt-secret: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=
iterations: 1000 # small amount of iters to make tests faster
Loading