Introduction to Pepper Keys
A pepper is a secret key added to password hashing to provide an additional layer of security beyond salts. Unlike salts, which are stored with the hash, peppers are kept secret and stored separately from the database. This prevents attackers from cracking hashes even if they gain access to the database, as they lack the pepper key.
System Architecture Overview
Pepper Key System Architecture ├── Key Storage Layer │ ├── HSM (Hardware Security Module) │ ├── Key Management Service (KMS) │ ├── Environment Variables │ └── Secure Configuration Server ├── Pepper Integration │ ├── Password + Salt + Pepper → Hash │ ├── Multiple Pepper Keys (Rotation) │ ├── Key Versioning │ └── Fallback Mechanisms ├── Security Features │ ├── Key Rotation │ ├── Key Compromise Recovery │ ├── Audit Logging │ └── Access Control └── Application Integration ├── Password Hashing (Bcrypt/PBKDF2/Argon2) ├── Authentication Flow ├── Key Retrieval Service └── Monitoring & Alerting
Core Implementation
1. Maven Dependencies
<properties>
<spring-security.version>6.2.0</spring-security.version>
<bouncycastle.version>1.78</bouncycastle.version>
<aws.sdk.version>2.21.0</aws.sdk.version>
<google.cloud.kms.version>2.34.0</google.cloud.kms.version>
</properties>
<dependencies>
<!-- Spring Security Crypto -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>${spring.security.version}</version>
</dependency>
<!-- Bouncy Castle -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- AWS SDK for KMS -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- Google Cloud KMS -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-kms</artifactId>
<version>${google.cloud.kms.version}</version>
</dependency>
<!-- Azure Key Vault -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-keys</artifactId>
<version>4.7.0</version>
</dependency>
<!-- HashiCorp Vault -->
<dependency>
<groupId>com.bettercloud</groupId>
<artifactId>vault-java-driver</artifactId>
<version>5.1.0</version>
</dependency>
<!-- JCA/JCE -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Redis for caching -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Pepper Key Service Interface
package com.security.pepper;
import java.util.Optional;
public interface PepperKeyService {
/**
* Get current pepper key for hashing
*/
PepperKey getCurrentPepper();
/**
* Get specific pepper key by version
*/
Optional<PepperKey> getPepperByVersion(String version);
/**
* Rotate pepper keys
*/
PepperRotationResult rotateKeys(RotationConfig config);
/**
* Apply pepper to password
*/
byte[] applyPepper(String password, byte[] salt, String pepperVersion);
/**
* Verify password with pepper
*/
boolean verifyPassword(String password, byte[] salt, byte[] hash, String pepperVersion);
/**
* Get pepper health status
*/
PepperHealth getHealth();
/**
* List all pepper versions
*/
List<PepperVersion> listVersions();
/**
* Revoke pepper key
*/
void revokeKey(String version, RevocationReason reason);
/**
* Backup pepper keys
*/
byte[] backupKeys(BackupConfig config);
/**
* Restore pepper keys from backup
*/
void restoreKeys(byte[] backup, BackupConfig config);
}
3. Core Pepper Key Implementation
package com.security.pepper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class CorePepperService implements PepperKeyService {
private static final Logger logger = LoggerFactory.getLogger(CorePepperService.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// Pepper storage (in production, use HSM/KMS)
private final Map<String, PepperKey> pepperStore = new ConcurrentHashMap<>();
private final Map<String, PepperKey> revokedPeppers = new ConcurrentHashMap<>();
// Current active pepper version
private String currentVersion;
// Configuration
private final PepperConfig config;
// Metrics
private final AtomicLong totalHashOperations = new AtomicLong(0);
private final AtomicLong failedVerifications = new AtomicLong(0);
public CorePepperService(PepperConfig config) {
this.config = config;
initializePeppers();
}
@Override
public PepperKey getCurrentPepper() {
PepperKey current = pepperStore.get(currentVersion);
if (current == null) {
throw new PepperException("No active pepper key available");
}
return current;
}
@Override
public Optional<PepperKey> getPepperByVersion(String version) {
return Optional.ofNullable(pepperStore.get(version));
}
@Override
public PepperRotationResult rotateKeys(RotationConfig rotationConfig) {
logger.info("Starting pepper key rotation");
try {
// Generate new pepper
String newVersion = generateVersionId();
byte[] newKey = generatePepperKey(config.getKeySizeBytes());
Instant activationTime = Instant.now().plusSeconds(rotationConfig.getActivationDelaySeconds());
// Create new pepper
PepperKey newPepper = PepperKey.builder()
.version(newVersion)
.key(newKey)
.algorithm(config.getAlgorithm())
.createdAt(Instant.now())
.activationTime(activationTime)
.status(PepperStatus.PENDING)
.build();
// Store new pepper
pepperStore.put(newVersion, newPepper);
// Schedule activation
scheduleActivation(newVersion, activationTime);
// Deactivate old pepper if needed
if (rotationConfig.isDeactivatePrevious()) {
deactivatePepper(currentVersion, DeactivationReason.ROTATED);
}
// Update current version after activation delay
if (rotationConfig.getActivationDelaySeconds() == 0) {
currentVersion = newVersion;
newPepper.setStatus(PepperStatus.ACTIVE);
}
logger.info("Pepper rotation completed. New version: {}", newVersion);
return new PepperRotationResult(
currentVersion,
newVersion,
activationTime,
rotationConfig
);
} catch (Exception e) {
logger.error("Pepper rotation failed", e);
throw new PepperException("Failed to rotate pepper keys", e);
}
}
@Override
public byte[] applyPepper(String password, byte[] salt, String pepperVersion) {
totalHashOperations.incrementAndGet();
try {
PepperKey pepper = getPepperForVersion(pepperVersion);
// Combine password with salt and pepper
byte[] passwordBytes = password.getBytes("UTF-8");
// Method 1: HMAC-based (recommended)
if (config.getAlgorithm().startsWith("Hmac")) {
return applyHmacPepper(passwordBytes, salt, pepper);
}
// Method 2: Concatenation-based
byte[] combined = new byte[passwordBytes.length + salt.length + pepper.getKey().length];
System.arraycopy(passwordBytes, 0, combined, 0, passwordBytes.length);
System.arraycopy(salt, 0, combined, passwordBytes.length, salt.length);
System.arraycopy(pepper.getKey(), 0, combined, passwordBytes.length + salt.length, pepper.getKey().length);
// Hash with configured algorithm
MessageDigest digest = MessageDigest.getInstance(config.getHashAlgorithm());
return digest.digest(combined);
} catch (Exception e) {
logger.error("Failed to apply pepper", e);
throw new PepperException("Pepper application failed", e);
}
}
@Override
public boolean verifyPassword(String password, byte[] salt, byte[] expectedHash, String pepperVersion) {
try {
byte[] computedHash = applyPepper(password, salt, pepperVersion);
boolean matches = MessageDigest.isEqual(computedHash, expectedHash);
if (!matches) {
failedVerifications.incrementAndGet();
logger.warn("Password verification failed for version: {}", pepperVersion);
}
return matches;
} catch (Exception e) {
logger.error("Password verification error", e);
return false;
}
}
@Override
public PepperHealth getHealth() {
PepperHealth health = new PepperHealth();
health.setTotalKeys(pepperStore.size());
health.setActiveKey(currentVersion);
health.setTotalHashOperations(totalHashOperations.get());
health.setFailedVerifications(failedVerifications.get());
// Check key expiration
pepperStore.values().forEach(pepper -> {
if (pepper.getExpirationTime() != null &&
pepper.getExpirationTime().isBefore(Instant.now())) {
health.addExpiringKey(pepper.getVersion());
}
});
// Calculate success rate
long total = totalHashOperations.get();
if (total > 0) {
double successRate = (total - failedVerifications.get()) * 100.0 / total;
health.setSuccessRate(successRate);
}
return health;
}
@Override
public List<PepperVersion> listVersions() {
List<PepperVersion> versions = new ArrayList<>();
pepperStore.values().forEach(pepper -> {
versions.add(new PepperVersion(
pepper.getVersion(),
pepper.getStatus(),
pepper.getCreatedAt(),
pepper.getActivationTime(),
pepper.getExpirationTime()
));
});
// Sort by creation time descending
versions.sort((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()));
return versions;
}
@Override
public void revokeKey(String version, RevocationReason reason) {
PepperKey pepper = pepperStore.get(version);
if (pepper == null) {
throw new PepperException("Pepper version not found: " + version);
}
pepper.setStatus(PepperStatus.REVOKED);
pepper.setRevocationReason(reason);
pepper.setRevokedAt(Instant.now());
// Move to revoked store
revokedPeppers.put(version, pepper);
pepperStore.remove(version);
logger.warn("Pepper key revoked: {} - {}", version, reason);
// If current version was revoked, activate next available
if (version.equals(currentVersion)) {
activateNextAvailablePepper();
}
}
@Override
public byte[] backupKeys(BackupConfig config) {
try {
// Create backup structure
PepperBackup backup = new PepperBackup();
backup.setTimestamp(Instant.now());
backup.setConfig(config);
// Add active peppers
pepperStore.values().forEach(pepper -> {
backup.addPepper(pepper.cloneWithMaskedKey());
});
// Add revoked peppers if configured
if (config.isIncludeRevoked()) {
revokedPeppers.values().forEach(pepper -> {
backup.addRevokedPepper(pepper.cloneWithMaskedKey());
});
}
// Serialize and encrypt
byte[] serialized = serializeBackup(backup);
return encryptBackup(serialized, config.getEncryptionKey());
} catch (Exception e) {
throw new PepperException("Backup creation failed", e);
}
}
@Override
public void restoreKeys(byte[] backup, BackupConfig config) {
try {
// Decrypt and deserialize
byte[] decrypted = decryptBackup(backup, config.getEncryptionKey());
PepperBackup restored = deserializeBackup(decrypted);
// Validate restoration
validateBackupForRestore(restored);
// Clear current stores
pepperStore.clear();
revokedPeppers.clear();
// Restore peppers
restored.getPeppers().forEach(pepper -> {
pepperStore.put(pepper.getVersion(), pepper);
});
restored.getRevokedPeppers().forEach(pepper -> {
revokedPeppers.put(pepper.getVersion(), pepper);
});
// Set current version
currentVersion = restored.getConfig().getPreferredVersion();
logger.info("Pepper keys restored from backup");
} catch (Exception e) {
throw new PepperException("Restore failed", e);
}
}
private void initializePeppers() {
// Generate initial pepper if none exists
if (pepperStore.isEmpty()) {
String version = generateVersionId();
byte[] key = generatePepperKey(config.getKeySizeBytes());
PepperKey initialPepper = PepperKey.builder()
.version(version)
.key(key)
.algorithm(config.getAlgorithm())
.createdAt(Instant.now())
.activationTime(Instant.now())
.status(PepperStatus.ACTIVE)
.expirationTime(Instant.now().plusSeconds(config.getKeyLifetimeSeconds()))
.build();
pepperStore.put(version, initialPepper);
currentVersion = version;
logger.info("Initial pepper key created: {}", version);
}
}
private byte[] generatePepperKey(int keySizeBytes) {
byte[] key = new byte[keySizeBytes];
SECURE_RANDOM.nextBytes(key);
return key;
}
private String generateVersionId() {
byte[] random = new byte[8];
SECURE_RANDOM.nextBytes(random);
return Base64.getUrlEncoder().withoutPadding().encodeToString(random);
}
private PepperKey getPepperForVersion(String version) {
PepperKey pepper = pepperStore.get(version);
if (pepper == null) {
// Try fallback to current version
pepper = pepperStore.get(currentVersion);
if (pepper == null) {
throw new PepperException("No valid pepper key available");
}
logger.warn("Pepper version {} not found, using current {}", version, currentVersion);
}
// Check if pepper is active
if (pepper.getStatus() != PepperStatus.ACTIVE) {
throw new PepperException("Pepper key is not active: " + pepper.getStatus());
}
return pepper;
}
private byte[] applyHmacPepper(byte[] passwordBytes, byte[] salt, PepperKey pepper) throws Exception {
// Create HMAC with pepper as key
SecretKeySpec keySpec = new SecretKeySpec(pepper.getKey(), pepper.getAlgorithm());
Mac mac = Mac.getInstance(pepper.getAlgorithm());
mac.init(keySpec);
// Update with password and salt
mac.update(passwordBytes);
mac.update(salt);
return mac.doFinal();
}
private void scheduleActivation(String version, Instant activationTime) {
// In production, use a scheduler
long delay = activationTime.toEpochMilli() - System.currentTimeMillis();
if (delay > 0) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
activatePepper(version);
}
}, delay);
}
}
private void activatePepper(String version) {
PepperKey pepper = pepperStore.get(version);
if (pepper != null && pepper.getStatus() == PepperStatus.PENDING) {
pepper.setStatus(PepperStatus.ACTIVE);
currentVersion = version;
logger.info("Pepper key activated: {}", version);
}
}
private void activateNextAvailablePepper() {
pepperStore.values().stream()
.filter(p -> p.getStatus() == PepperStatus.ACTIVE)
.findFirst()
.ifPresent(pepper -> currentVersion = pepper.getVersion());
}
private void deactivatePepper(String version, DeactivationReason reason) {
PepperKey pepper = pepperStore.get(version);
if (pepper != null) {
pepper.setStatus(PepperStatus.DEACTIVATED);
pepper.setDeactivationReason(reason);
logger.info("Pepper deactivated: {} - {}", version, reason);
}
}
private void validateBackupForRestore(PepperBackup backup) {
// Check timestamp
if (backup.getTimestamp().isBefore(Instant.now().minusSeconds(config.getMaxBackupAgeSeconds()))) {
throw new PepperException("Backup is too old");
}
// Validate integrity
if (!backup.verifyIntegrity()) {
throw new PepperException("Backup integrity check failed");
}
}
private byte[] serializeBackup(PepperBackup backup) {
// Implementation using Protocol Buffers, JSON, or custom serialization
return new byte[0]; // Placeholder
}
private PepperBackup deserializeBackup(byte[] data) {
// Implementation
return null; // Placeholder
}
private byte[] encryptBackup(byte[] data, byte[] key) {
// Implementation using AES-GCM
return new byte[0]; // Placeholder
}
private byte[] decryptBackup(byte[] encrypted, byte[] key) {
// Implementation
return new byte[0]; // Placeholder
}
}
4. KMS-Based Pepper Storage
package com.security.pepper.kms;
import com.security.pepper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.*;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.*;
@Service
public class KmsPepperService implements PepperKeyService {
private static final Logger logger = LoggerFactory.getLogger(KmsPepperService.class);
private final KmsClient kmsClient;
private final String keyId;
private final Map<String, KmsPepperReference> pepperCache = new HashMap<>();
// Data encryption key for local caching
private final byte[] localEncryptionKey;
public KmsPepperService(KmsClient kmsClient, String keyId, byte[] localEncryptionKey) {
this.kmsClient = kmsClient;
this.keyId = keyId;
this.localEncryptionKey = localEncryptionKey;
}
@Override
public PepperKey getCurrentPepper() {
// Generate data key from KMS
GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
.keyId(keyId)
.keySpec("AES_256")
.build();
GenerateDataKeyResponse response = kmsClient.generateDataKey(request);
// Create pepper key from data key
byte[] pepperKey = response.plaintext().asByteArray();
String version = response.keyId() + "-" + Instant.now().toEpochMilli();
// Cache encrypted version
cachePepper(version, response.ciphertextBlob().asByteArray());
return PepperKey.builder()
.version(version)
.key(pepperKey)
.algorithm("HmacSHA256")
.createdAt(Instant.now())
.activationTime(Instant.now())
.status(PepperStatus.ACTIVE)
.build();
}
@Override
public Optional<PepperKey> getPepperByVersion(String version) {
try {
// Check cache first
KmsPepperReference cached = pepperCache.get(version);
if (cached != null) {
// Decrypt cached key
byte[] key = decryptKey(cached.getEncryptedKey());
return Optional.of(createPepperFromKey(version, key));
}
// Try to decrypt directly (would need KMS to store versions)
return Optional.empty();
} catch (Exception e) {
logger.error("Failed to retrieve pepper version: {}", version, e);
return Optional.empty();
}
}
@Override
public PepperRotationResult rotateKeys(RotationConfig config) {
try {
// Rotate KMS key
if (config.isRotateMasterKey()) {
ScheduleKeyDeletionRequest deleteRequest = ScheduleKeyDeletionRequest.builder()
.keyId(keyId)
.pendingWindowInDays(7)
.build();
kmsClient.scheduleKeyDeletion(deleteRequest);
// Create new key
CreateKeyRequest createRequest = CreateKeyRequest.builder()
.description("Pepper Master Key - " + Instant.now())
.keyUsage(KeyUsage.ENCRYPT_DECRYPT)
.keySpec(KeySpec.SYMMETRIC_DEFAULT)
.build();
CreateKeyResponse createResponse = kmsClient.createKey(createRequest);
String newKeyId = createResponse.keyMetadata().keyId();
// Generate new pepper
return new PepperRotationResult(
keyId,
newKeyId,
Instant.now().plusSeconds(config.getActivationDelaySeconds()),
config
);
}
return null;
} catch (Exception e) {
throw new PepperException("KMS rotation failed", e);
}
}
@Override
public byte[] applyPepper(String password, byte[] salt, String pepperVersion) {
PepperKey pepper = getPepperByVersion(pepperVersion)
.orElseGet(this::getCurrentPepper);
try {
// Apply pepper using HMAC
javax.crypto.Mac mac = javax.crypto.Mac.getInstance(pepper.getAlgorithm());
javax.crypto.spec.SecretKeySpec keySpec =
new javax.crypto.spec.SecretKeySpec(pepper.getKey(), pepper.getAlgorithm());
mac.init(keySpec);
mac.update(password.getBytes("UTF-8"));
mac.update(salt);
return mac.doFinal();
} catch (Exception e) {
throw new PepperException("Failed to apply KMS pepper", e);
}
}
@Override
public boolean verifyPassword(String password, byte[] salt, byte[] expectedHash, String pepperVersion) {
try {
byte[] computedHash = applyPepper(password, salt, pepperVersion);
return MessageDigest.isEqual(computedHash, expectedHash);
} catch (Exception e) {
logger.error("Verification failed", e);
return false;
}
}
@Override
public PepperHealth getHealth() {
try {
// Check KMS status
DescribeKeyRequest request = DescribeKeyRequest.builder()
.keyId(keyId)
.build();
DescribeKeyResponse response = kmsClient.describeKey(request);
KeyMetadata metadata = response.keyMetadata();
PepperHealth health = new PepperHealth();
health.setActiveKey(keyId);
health.setKeyState(metadata.keyState().toString());
health.setKeyEnabled(metadata.enabled());
health.setKeyArn(metadata.arn());
return health;
} catch (Exception e) {
PepperHealth health = new PepperHealth();
health.setHealthy(false);
health.setError(e.getMessage());
return health;
}
}
@Override
public List<PepperVersion> listVersions() {
// KMS doesn't automatically track pepper versions
// Would need to maintain version registry
return new ArrayList<>();
}
@Override
public void revokeKey(String version, RevocationReason reason) {
// Remove from cache
pepperCache.remove(version);
logger.warn("KMS pepper key revoked from cache: {}", version);
}
@Override
public byte[] backupKeys(BackupConfig config) {
try {
// Generate backup key
GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
.keyId(keyId)
.keySpec("AES_256")
.build();
GenerateDataKeyResponse response = kmsClient.generateDataKey(request);
// Create backup structure
KmsBackup backup = new KmsBackup();
backup.setKeyId(keyId);
backup.setTimestamp(Instant.now());
backup.setEncryptedBackupKey(response.ciphertextBlob().asByteArray());
// Serialize
return serializeBackup(backup);
} catch (Exception e) {
throw new PepperException("KMS backup failed", e);
}
}
@Override
public void restoreKeys(byte[] backup, BackupConfig config) {
try {
// Deserialize backup
KmsBackup restored = deserializeBackup(backup);
// Validate
if (!restored.getKeyId().equals(keyId)) {
throw new PepperException("Backup key ID mismatch");
}
logger.info("KMS pepper keys restored from backup");
} catch (Exception e) {
throw new PepperException("KMS restore failed", e);
}
}
private void cachePepper(String version, byte[] encryptedKey) {
// Store encrypted key for future retrieval
pepperCache.put(version, new KmsPepperReference(version, encryptedKey));
}
private byte[] decryptKey(byte[] encryptedKey) {
// Use KMS to decrypt
DecryptRequest request = DecryptRequest.builder()
.keyId(keyId)
.ciphertextBlob(ByteBuffer.wrap(encryptedKey))
.build();
DecryptResponse response = kmsClient.decrypt(request);
return response.plaintext().asByteArray();
}
private PepperKey createPepperFromKey(String version, byte[] key) {
return PepperKey.builder()
.version(version)
.key(key)
.algorithm("HmacSHA256")
.createdAt(Instant.now())
.activationTime(Instant.now())
.status(PepperStatus.ACTIVE)
.build();
}
private byte[] serializeBackup(KmsBackup backup) {
// Implementation
return new byte[0];
}
private KmsBackup deserializeBackup(byte[] data) {
// Implementation
return null;
}
// Inner class for cached references
private static class KmsPepperReference {
private final String version;
private final byte[] encryptedKey;
public KmsPepperReference(String version, byte[] encryptedKey) {
this.version = version;
this.encryptedKey = encryptedKey;
}
public String getVersion() { return version; }
public byte[] getEncryptedKey() { return encryptedKey; }
}
// Backup structure
private static class KmsBackup {
private String keyId;
private Instant timestamp;
private byte[] encryptedBackupKey;
// getters and setters
}
}
5. HSM-Based Pepper Storage
package com.security.pepper.hsm;
import com.security.pepper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.time.Instant;
import java.util.*;
public class HSMPepperService implements PepperKeyService {
private static final Logger logger = LoggerFactory.getLogger(HSMPepperService.class);
private final KeyStore hsmKeyStore;
private final String hsmPartition;
private final String hsmUser;
private final SecureRandom secureRandom = new SecureRandom();
private final Map<String, HSMPepperReference> pepperCache = new HashMap<>();
private String currentVersion;
public HSMPepperService(KeyStore hsmKeyStore, String hsmPartition, String hsmUser) {
this.hsmKeyStore = hsmKeyStore;
this.hsmPartition = hsmPartition;
this.hsmUser = hsmUser;
initializeHSMKeys();
}
@Override
public PepperKey getCurrentPepper() {
try {
// Generate key in HSM
String keyAlias = "pepper-" + UUID.randomUUID().toString();
// In HSM, generate key pair (asymmetric) or secret key (symmetric)
// This is implementation-specific to the HSM
// For symmetric pepper (faster)
byte[] pepperKey = generateHSMSecretKey(keyAlias, 256);
// For asymmetric pepper (more secure)
// KeyPair keyPair = generateHSMKeyPair(keyAlias, "RSA", 2048);
// byte[] pepperKey = keyPair.getPrivate().getEncoded();
String version = keyAlias + "-" + Instant.now().toEpochMilli();
return PepperKey.builder()
.version(version)
.key(pepperKey)
.algorithm("HmacSHA256")
.createdAt(Instant.now())
.activationTime(Instant.now())
.status(PepperStatus.ACTIVE)
.hsmAlias(keyAlias)
.build();
} catch (Exception e) {
throw new PepperException("Failed to get HSM pepper", e);
}
}
@Override
public Optional<PepperKey> getPepperByVersion(String version) {
try {
// Extract HSM alias from version
String hsmAlias = extractHSMAlias(version);
// Check if key exists in HSM
if (!hsmKeyStore.containsAlias(hsmAlias)) {
return Optional.empty();
}
// Get key from HSM
SecretKey hsmKey = (SecretKey) hsmKeyStore.getKey(hsmAlias, null);
return Optional.of(PepperKey.builder()
.version(version)
.key(hsmKey.getEncoded())
.algorithm("HmacSHA256")
.createdAt(Instant.now())
.activationTime(Instant.now())
.status(PepperStatus.ACTIVE)
.hsmAlias(hsmAlias)
.build());
} catch (Exception e) {
logger.error("Failed to retrieve HSM pepper: {}", version, e);
return Optional.empty();
}
}
@Override
public PepperRotationResult rotateKeys(RotationConfig config) {
try {
// Generate new key in HSM
String newAlias = "pepper-" + UUID.randomUUID().toString();
byte[] newKey = generateHSMSecretKey(newAlias, config.getKeySizeBits());
String newVersion = newAlias + "-" + Instant.now().toEpochMilli();
// Archive old key if needed
if (config.isArchivePrevious() && currentVersion != null) {
archiveHSMKey(extractHSMAlias(currentVersion));
}
currentVersion = newVersion;
return new PepperRotationResult(
currentVersion,
newVersion,
Instant.now(),
config
);
} catch (Exception e) {
throw new PepperException("HSM key rotation failed", e);
}
}
@Override
public byte[] applyPepper(String password, byte[] salt, String pepperVersion) {
try {
PepperKey pepper = getPepperByVersion(pepperVersion)
.orElseGet(this::getCurrentPepper);
// Use HSM for HMAC operation (if supported)
if (isHSMOperationSupported()) {
return performHSMOperation(pepper, password, salt);
}
// Fallback to local HMAC
javax.crypto.Mac mac = javax.crypto.Mac.getInstance(pepper.getAlgorithm());
javax.crypto.spec.SecretKeySpec keySpec =
new javax.crypto.spec.SecretKeySpec(pepper.getKey(), pepper.getAlgorithm());
mac.init(keySpec);
mac.update(password.getBytes("UTF-8"));
mac.update(salt);
return mac.doFinal();
} catch (Exception e) {
throw new PepperException("Failed to apply HSM pepper", e);
}
}
@Override
public boolean verifyPassword(String password, byte[] salt, byte[] expectedHash, String pepperVersion) {
try {
byte[] computedHash = applyPepper(password, salt, pepperVersion);
return MessageDigest.isEqual(computedHash, expectedHash);
} catch (Exception e) {
logger.error("HSM verification failed", e);
return false;
}
}
@Override
public PepperHealth getHealth() {
PepperHealth health = new PepperHealth();
health.setActiveKey(currentVersion);
try {
// Check HSM connection
Enumeration<String> aliases = hsmKeyStore.aliases();
int keyCount = 0;
while (aliases.hasMoreElements()) {
aliases.nextElement();
keyCount++;
}
health.setTotalKeys(keyCount);
health.setHsmConnected(true);
health.setHsmPartition(hsmPartition);
health.setHsmUser(hsmUser);
} catch (Exception e) {
health.setHsmConnected(false);
health.setError(e.getMessage());
}
return health;
}
@Override
public List<PepperVersion> listVersions() {
List<PepperVersion> versions = new ArrayList<>();
try {
Enumeration<String> aliases = hsmKeyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (alias.startsWith("pepper-")) {
Certificate cert = hsmKeyStore.getCertificate(alias);
versions.add(new PepperVersion(
alias + "-" + cert.getNotBefore().getTime(),
PepperStatus.ACTIVE,
cert.getNotBefore().toInstant(),
cert.getNotBefore().toInstant(),
cert.getNotAfter().toInstant()
));
}
}
} catch (Exception e) {
logger.error("Failed to list HSM versions", e);
}
return versions;
}
@Override
public void revokeKey(String version, RevocationReason reason) {
try {
String hsmAlias = extractHSMAlias(version);
// In HSM, revoke key (implementation-specific)
// This might involve moving to a revoked partition or deleting
logger.warn("HSM key revoked: {} - {}", version, reason);
} catch (Exception e) {
throw new PepperException("Failed to revoke HSM key", e);
}
}
@Override
public byte[] backupKeys(BackupConfig config) {
try {
// HSM-specific backup
// This would use HSM's backup capabilities
return new byte[0];
} catch (Exception e) {
throw new PepperException("HSM backup failed", e);
}
}
@Override
public void restoreKeys(byte[] backup, BackupConfig config) {
try {
// HSM-specific restore
logger.info("HSM keys restored from backup");
} catch (Exception e) {
throw new PepperException("HSM restore failed", e);
}
}
private void initializeHSMKeys() {
try {
// Check if we have existing keys
Enumeration<String> aliases = hsmKeyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (alias.startsWith("pepper-") && hsmKeyStore.isKeyEntry(alias)) {
currentVersion = alias + "-" + System.currentTimeMillis();
logger.info("Found existing HSM pepper key: {}", alias);
return;
}
}
// No existing keys, generate new one
PepperKey initial = getCurrentPepper();
currentVersion = initial.getVersion();
} catch (Exception e) {
throw new PepperException("Failed to initialize HSM keys", e);
}
}
private byte[] generateHSMSecretKey(String alias, int keySizeBits) throws Exception {
// HSM-specific key generation
// This would use HSM's API to generate and store key
return new byte[keySizeBits / 8];
}
private String extractHSMAlias(String version) {
return version.split("-")[0] + "-" + version.split("-")[1];
}
private void archiveHSMKey(String alias) {
// HSM-specific archiving
logger.info("Archived HSM key: {}", alias);
}
private boolean isHSMOperationSupported() {
// Check if HSM supports HMAC operations directly
return false;
}
private byte[] performHSMOperation(PepperKey pepper, String password, byte[] salt) {
// HSM-specific crypto operation
return new byte[0];
}
// Inner class for HSM references
private static class HSMPepperReference {
private final String version;
private final String hsmAlias;
public HSMPepperReference(String version, String hsmAlias) {
this.version = version;
this.hsmAlias = hsmAlias;
}
}
}
6. Password Hashing with Pepper Integration
package com.security.pepper.hashing;
import com.security.pepper.PepperKey;
import com.security.pepper.PepperKeyService;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;
@Service
public class PepperedPasswordService {
private final PepperKeyService pepperService;
private final SecureRandom secureRandom = new SecureRandom();
// Hashing algorithms
public enum HashAlgorithm {
BCRYPT,
PBKDF2_SHA256,
PBKDF2_SHA512,
ARGON2ID
}
public PepperedPasswordService(PepperKeyService pepperService) {
this.pepperService = pepperService;
}
/**
* Hash password with pepper (Bcrypt + Pepper)
*/
public PepperedHash hashWithBcrypt(String password, int cost) {
// Generate salt
byte[] salt = generateSalt(16);
// Get current pepper
PepperKey pepper = pepperService.getCurrentPepper();
// Apply pepper to password (pre-hashing)
byte[] pepperedPassword = pepperService.applyPepper(
password,
salt,
pepper.getVersion()
);
// Encode for bcrypt
String pepperedPasswordStr = Base64.getEncoder().encodeToString(pepperedPassword);
// Use bcrypt on peppered password
org.mindrot.jbcrypt.BCrypt bcrypt = new org.mindrot.jbcrypt.BCrypt();
String bcryptSalt = org.mindrot.jbcrypt.BCrypt.gensalt(cost);
String bcryptHash = org.mindrot.jbcrypt.BCrypt.hashpw(pepperedPasswordStr, bcryptSalt);
// Create result with metadata
return new PepperedHash(
bcryptHash,
salt,
pepper.getVersion(),
HashAlgorithm.BCRYPT,
cost
);
}
/**
* Hash password with pepper (PBKDF2 + Pepper)
*/
public PepperedHash hashWithPBKDF2(String password,
HashAlgorithm algorithm,
int iterations,
int keyLength) {
// Generate salt
byte[] salt = generateSalt(16);
// Get current pepper
PepperKey pepper = pepperService.getCurrentPepper();
// Apply pepper to password (pre-hashing)
byte[] pepperedPassword = pepperService.applyPepper(
password,
salt,
pepper.getVersion()
);
try {
// Determine algorithm
String pbkdf2Algorithm;
switch (algorithm) {
case PBKDF2_SHA256:
pbkdf2Algorithm = "PBKDF2WithHmacSHA256";
break;
case PBKDF2_SHA512:
pbkdf2Algorithm = "PBKDF2WithHmacSHA512";
break;
default:
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
// Use PBKDF2 on peppered password
KeySpec spec = new PBEKeySpec(
bytesToChars(pepperedPassword),
salt,
iterations,
keyLength
);
SecretKeyFactory factory = SecretKeyFactory.getInstance(pbkdf2Algorithm);
byte[] hash = factory.generateSecret(spec).getEncoded();
// Encode as Base64 for storage
String hashStr = Base64.getEncoder().encodeToString(hash);
return new PepperedHash(
hashStr,
salt,
pepper.getVersion(),
algorithm,
iterations
);
} catch (Exception e) {
throw new RuntimeException("PBKDF2 hashing failed", e);
}
}
/**
* Verify password with pepper
*/
public boolean verifyPassword(String password, PepperedHash storedHash) {
try {
// Get the pepper used for this hash
PepperKey pepper = pepperService.getPepperByVersion(storedHash.getPepperVersion())
.orElseThrow(() -> new RuntimeException("Pepper version not found"));
// Apply same pepper
byte[] pepperedPassword = pepperService.applyPepper(
password,
storedHash.getSalt(),
storedHash.getPepperVersion()
);
// Verify based on algorithm
switch (storedHash.getAlgorithm()) {
case BCRYPT:
return verifyBcrypt(pepperedPassword, storedHash);
case PBKDF2_SHA256:
case PBKDF2_SHA512:
return verifyPBKDF2(pepperedPassword, storedHash);
default:
throw new IllegalArgumentException("Unsupported algorithm");
}
} catch (Exception e) {
return false;
}
}
/**
* Check if hash needs upgrade (pepper rotation or stronger parameters)
*/
public boolean needsUpgrade(PepperedHash hash, SecurityPolicy policy) {
// Check if pepper version is still active
boolean pepperValid = pepperService.getPepperByVersion(hash.getPepperVersion())
.isPresent();
if (!pepperValid) {
return true;
}
// Check algorithm strength
switch (hash.getAlgorithm()) {
case BCRYPT:
return hash.getCost() < policy.getMinBcryptCost();
case PBKDF2_SHA256:
case PBKDF2_SHA512:
return hash.getIterations() < policy.getMinPbkdf2Iterations();
default:
return true;
}
}
/**
* Upgrade hash to use current pepper and stronger parameters
*/
public PepperedHash upgradeHash(String password, PepperedHash oldHash, SecurityPolicy policy) {
if (!needsUpgrade(oldHash, policy)) {
return oldHash;
}
// Rehash with current pepper
switch (policy.getPreferredAlgorithm()) {
case BCRYPT:
return hashWithBcrypt(password, policy.getMinBcryptCost());
case PBKDF2_SHA256:
return hashWithPBKDF2(
password,
HashAlgorithm.PBKDF2_SHA256,
policy.getMinPbkdf2Iterations(),
policy.getMinKeyLength()
);
default:
return hashWithBcrypt(password, policy.getMinBcryptCost());
}
}
private byte[] generateSalt(int length) {
byte[] salt = new byte[length];
secureRandom.nextBytes(salt);
return salt;
}
private char[] bytesToChars(byte[] bytes) {
char[] chars = new char[bytes.length];
for (int i = 0; i < bytes.length; i++) {
chars[i] = (char) (bytes[i] & 0xFF);
}
return chars;
}
private boolean verifyBcrypt(byte[] pepperedPassword, PepperedHash storedHash) {
String pepperedPasswordStr = Base64.getEncoder().encodeToString(pepperedPassword);
return org.mindrot.jbcrypt.BCrypt.checkpw(
pepperedPasswordStr,
storedHash.getHash()
);
}
private boolean verifyPBKDF2(byte[] pepperedPassword, PepperedHash storedHash) {
try {
String pbkdf2Algorithm = storedHash.getAlgorithm() == HashAlgorithm.PBKDF2_SHA256
? "PBKDF2WithHmacSHA256"
: "PBKDF2WithHmacSHA512";
KeySpec spec = new PBEKeySpec(
bytesToChars(pepperedPassword),
storedHash.getSalt(),
storedHash.getIterations(),
storedHash.getHashBytes().length * 8
);
SecretKeyFactory factory = SecretKeyFactory.getInstance(pbkdf2Algorithm);
byte[] computedHash = factory.generateSecret(spec).getEncoded();
return MessageDigest.isEqual(computedHash, storedHash.getHashBytes());
} catch (Exception e) {
return false;
}
}
// Data class for peppered hash
public static class PepperedHash {
private final String hash;
private final byte[] salt;
private final String pepperVersion;
private final HashAlgorithm algorithm;
private final int cost; // for bcrypt
private final int iterations; // for pbkdf2
public PepperedHash(String hash, byte[] salt, String pepperVersion,
HashAlgorithm algorithm, int cost) {
this.hash = hash;
this.salt = salt;
this.pepperVersion = pepperVersion;
this.algorithm = algorithm;
this.cost = cost;
this.iterations = 0;
}
public PepperedHash(String hash, byte[] salt, String pepperVersion,
HashAlgorithm algorithm, int iterations) {
this.hash = hash;
this.salt = salt;
this.pepperVersion = pepperVersion;
this.algorithm = algorithm;
this.cost = 0;
this.iterations = iterations;
}
// For storage (format: algorithm$pepperVersion$cost$salt$hash)
public String toStorageFormat() {
String saltStr = Base64.getEncoder().encodeToString(salt);
if (algorithm == HashAlgorithm.BCRYPT) {
return String.format("%s$%s$%d$%s$%s",
algorithm.name(),
pepperVersion,
cost,
saltStr,
hash
);
} else {
return String.format("%s$%s$%d$%s$%s",
algorithm.name(),
pepperVersion,
iterations,
saltStr,
hash
);
}
}
// Parse from storage format
public static PepperedHash fromStorageFormat(String stored) {
String[] parts = stored.split("\\$");
if (parts.length < 5) {
throw new IllegalArgumentException("Invalid hash format");
}
HashAlgorithm algorithm = HashAlgorithm.valueOf(parts[0]);
String pepperVersion = parts[1];
int costOrIter = Integer.parseInt(parts[2]);
byte[] salt = Base64.getDecoder().decode(parts[3]);
String hash = parts[4];
if (algorithm == HashAlgorithm.BCRYPT) {
return new PepperedHash(hash, salt, pepperVersion, algorithm, costOrIter);
} else {
return new PepperedHash(hash, salt, pepperVersion, algorithm, costOrIter);
}
}
public String getHash() { return hash; }
public byte[] getHashBytes() { return Base64.getDecoder().decode(hash); }
public byte[] getSalt() { return salt; }
public String getPepperVersion() { return pepperVersion; }
public HashAlgorithm getAlgorithm() { return algorithm; }
public int getCost() { return cost; }
public int getIterations() { return iterations; }
}
// Security policy
public static class SecurityPolicy {
private final HashAlgorithm preferredAlgorithm;
private final int minBcryptCost;
private final int minPbkdf2Iterations;
private final int minKeyLength;
private SecurityPolicy(Builder builder) {
this.preferredAlgorithm = builder.preferredAlgorithm;
this.minBcryptCost = builder.minBcryptCost;
this.minPbkdf2Iterations = builder.minPbkdf2Iterations;
this.minKeyLength = builder.minKeyLength;
}
public HashAlgorithm getPreferredAlgorithm() { return preferredAlgorithm; }
public int getMinBcryptCost() { return minBcryptCost; }
public int getMinPbkdf2Iterations() { return minPbkdf2Iterations; }
public int getMinKeyLength() { return minKeyLength; }
public static class Builder {
private HashAlgorithm preferredAlgorithm = HashAlgorithm.BCRYPT;
private int minBcryptCost = 12;
private int minPbkdf2Iterations = 310000;
private int minKeyLength = 256;
public Builder preferredAlgorithm(HashAlgorithm algorithm) {
this.preferredAlgorithm = algorithm;
return this;
}
public Builder minBcryptCost(int cost) {
this.minBcryptCost = cost;
return this;
}
public Builder minPbkdf2Iterations(int iterations) {
this.minPbkdf2Iterations = iterations;
return this;
}
public Builder minKeyLength(int bits) {
this.minKeyLength = bits;
return this;
}
public SecurityPolicy build() {
return new SecurityPolicy(this);
}
}
}
}
7. REST API for Pepper Management
package com.security.pepper.rest;
import com.security.pepper.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/pepper")
public class PepperController {
private final PepperKeyService pepperService;
private final PepperedPasswordService passwordService;
public PepperController(PepperKeyService pepperService,
PepperedPasswordService passwordService) {
this.pepperService = pepperService;
this.passwordService = passwordService;
}
@GetMapping("/current")
public ResponseEntity<PepperResponse> getCurrentPepper() {
try {
PepperKey pepper = pepperService.getCurrentPepper();
return ResponseEntity.ok(new PepperResponse(
pepper.getVersion(),
pepper.getAlgorithm(),
pepper.getStatus(),
pepper.getCreatedAt(),
pepper.getActivationTime()
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new PepperResponse("Failed to get current pepper: " + e.getMessage()));
}
}
@GetMapping("/version/{version}")
public ResponseEntity<PepperResponse> getPepperVersion(@PathVariable String version) {
return pepperService.getPepperByVersion(version)
.map(pepper -> ResponseEntity.ok(new PepperResponse(
pepper.getVersion(),
pepper.getAlgorithm(),
pepper.getStatus(),
pepper.getCreatedAt(),
pepper.getActivationTime()
)))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/rotate")
public ResponseEntity<RotationResponse> rotateKeys(@RequestBody RotationRequest request) {
try {
RotationConfig config = new RotationConfig(
request.getActivationDelaySeconds(),
request.isDeactivatePrevious(),
request.getKeySizeBits()
);
PepperRotationResult result = pepperService.rotateKeys(config);
return ResponseEntity.ok(new RotationResponse(
result.getOldVersion(),
result.getNewVersion(),
result.getActivationTime(),
"Keys rotated successfully"
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new RotationResponse("Rotation failed: " + e.getMessage()));
}
}
@PostMapping("/revoke/{version}")
public ResponseEntity<RevokeResponse> revokeKey(
@PathVariable String version,
@RequestParam RevocationReason reason) {
try {
pepperService.revokeKey(version, reason);
return ResponseEntity.ok(new RevokeResponse(
version,
reason,
"Key revoked successfully"
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new RevokeResponse(version, reason, "Revocation failed: " + e.getMessage()));
}
}
@GetMapping("/versions")
public ResponseEntity<List<PepperVersion>> listVersions() {
return ResponseEntity.ok(pepperService.listVersions());
}
@GetMapping("/health")
public ResponseEntity<PepperHealth> getHealth() {
return ResponseEntity.ok(pepperService.getHealth());
}
@PostMapping("/hash/password")
public ResponseEntity<HashResponse> hashPassword(@RequestBody HashRequest request) {
try {
PepperedPasswordService.PepperedHash hash;
switch (request.getAlgorithm()) {
case "bcrypt":
hash = passwordService.hashWithBcrypt(
request.getPassword(),
request.getCost() != null ? request.getCost() : 12
);
break;
case "pbkdf2-sha256":
hash = passwordService.hashWithPBKDF2(
request.getPassword(),
PepperedPasswordService.HashAlgorithm.PBKDF2_SHA256,
request.getIterations() != null ? request.getIterations() : 310000,
request.getKeyLength() != null ? request.getKeyLength() : 256
);
break;
default:
return ResponseEntity.badRequest()
.body(new HashResponse("Unsupported algorithm"));
}
return ResponseEntity.ok(new HashResponse(
hash.toStorageFormat(),
hash.getAlgorithm(),
hash.getPepperVersion()
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new HashResponse("Hashing failed: " + e.getMessage()));
}
}
@PostMapping("/verify")
public ResponseEntity<VerifyResponse> verifyPassword(@RequestBody VerifyRequest request) {
try {
PepperedPasswordService.PepperedHash storedHash =
PepperedPasswordService.PepperedHash.fromStorageFormat(request.getHash());
boolean valid = passwordService.verifyPassword(request.getPassword(), storedHash);
return ResponseEntity.ok(new VerifyResponse(
valid,
valid ? "Password valid" : "Invalid password"
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new VerifyResponse(false, "Verification failed: " + e.getMessage()));
}
}
@PostMapping("/needs-upgrade")
public ResponseEntity<UpgradeCheckResponse> checkUpgrade(@RequestBody UpgradeRequest request) {
try {
PepperedPasswordService.PepperedHash storedHash =
PepperedPasswordService.PepperedHash.fromStorageFormat(request.getHash());
PepperedPasswordService.SecurityPolicy policy =
new PepperedPasswordService.SecurityPolicy.Builder()
.minBcryptCost(request.getMinBcryptCost())
.minPbkdf2Iterations(request.getMinPbkdf2Iterations())
.minKeyLength(request.getMinKeyLength())
.build();
boolean needsUpgrade = passwordService.needsUpgrade(storedHash, policy);
return ResponseEntity.ok(new UpgradeCheckResponse(
needsUpgrade,
needsUpgrade ? "Hash needs upgrade" : "Hash meets requirements"
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new UpgradeCheckResponse(false, "Check failed: " + e.getMessage()));
}
}
@PostMapping("/upgrade")
public ResponseEntity<HashResponse> upgradeHash(@RequestBody UpgradeRequest request) {
try {
PepperedPasswordService.PepperedHash oldHash =
PepperedPasswordService.PepperedHash.fromStorageFormat(request.getHash());
PepperedPasswordService.SecurityPolicy policy =
new PepperedPasswordService.SecurityPolicy.Builder()
.preferredAlgorithm(
PepperedPasswordService.HashAlgorithm.valueOf(
request.getTargetAlgorithm()
)
)
.minBcryptCost(request.getTargetBcryptCost())
.minPbkdf2Iterations(request.getTargetPbkdf2Iterations())
.minKeyLength(request.getTargetKeyLength())
.build();
PepperedPasswordService.PepperedHash upgraded =
passwordService.upgradeHash(request.getPassword(), oldHash, policy);
return ResponseEntity.ok(new HashResponse(
upgraded.toStorageFormat(),
upgraded.getAlgorithm(),
upgraded.getPepperVersion()
));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(new HashResponse("Upgrade failed: " + e.getMessage()));
}
}
// Request/Response classes
public static class PepperResponse {
private String version;
private String algorithm;
private PepperStatus status;
private Instant createdAt;
private Instant activationTime;
private String error;
public PepperResponse(String version, String algorithm, PepperStatus status,
Instant createdAt, Instant activationTime) {
this.version = version;
this.algorithm = algorithm;
this.status = status;
this.createdAt = createdAt;
this.activationTime = activationTime;
}
public PepperResponse(String error) {
this.error = error;
}
// getters
}
public static class RotationRequest {
private int activationDelaySeconds;
private boolean deactivatePrevious;
private int keySizeBits;
// getters and setters
}
public static class RotationResponse {
private String oldVersion;
private String newVersion;
private Instant activationTime;
private String message;
public RotationResponse(String oldVersion, String newVersion,
Instant activationTime, String message) {
this.oldVersion = oldVersion;
this.newVersion = newVersion;
this.activationTime = activationTime;
this.message = message;
}
public RotationResponse(String message) {
this.message = message;
}
// getters
}
public static class RevokeResponse {
private String version;
private RevocationReason reason;
private String message;
public RevokeResponse(String version, RevocationReason reason, String message) {
this.version = version;
this.reason = reason;
this.message = message;
}
// getters
}
public static class HashRequest {
private String password;
private String algorithm;
private Integer cost;
private Integer iterations;
private Integer keyLength;
// getters and setters
}
public static class HashResponse {
private String hash;
private PepperedPasswordService.HashAlgorithm algorithm;
private String pepperVersion;
private String error;
public HashResponse(String hash, PepperedPasswordService.HashAlgorithm algorithm,
String pepperVersion) {
this.hash = hash;
this.algorithm = algorithm;
this.pepperVersion = pepperVersion;
}
public HashResponse(String error) {
this.error = error;
}
// getters
}
public static class VerifyRequest {
private String password;
private String hash;
// getters and setters
}
public static class VerifyResponse {
private boolean valid;
private String message;
public VerifyResponse(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
// getters
}
public static class UpgradeRequest {
private String hash;
private String password;
private int minBcryptCost;
private int minPbkdf2Iterations;
private int minKeyLength;
private String targetAlgorithm;
private int targetBcryptCost;
private int targetPbkdf2Iterations;
private int targetKeyLength;
// getters and setters
}
public static class UpgradeCheckResponse {
private boolean needsUpgrade;
private String message;
public UpgradeCheckResponse(boolean needsUpgrade, String message) {
this.needsUpgrade = needsUpgrade;
this.message = message;
}
// getters
}
}
8. Testing and Validation
package com.security.pepper.test;
import com.security.pepper.*;
import com.security.pepper.hashing.PepperedPasswordService;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PepperKeyTest {
@Autowired
private PepperKeyService pepperService;
@Autowired
private PepperedPasswordService passwordService;
private static final String TEST_PASSWORD = "MySecurePassword123!";
@Test
@Order(1)
void testGetCurrentPepper() {
PepperKey current = pepperService.getCurrentPepper();
assertNotNull(current);
assertNotNull(current.getVersion());
assertNotNull(current.getKey());
assertEquals(32, current.getKey().length); // 256 bits
assertEquals(PepperStatus.ACTIVE, current.getStatus());
}
@Test
@Order(2)
void testPepperVersionRetrieval() {
PepperKey current = pepperService.getCurrentPepper();
PepperKey retrieved = pepperService.getPepperByVersion(current.getVersion())
.orElseThrow(() -> new AssertionError("Pepper version not found"));
assertEquals(current.getVersion(), retrieved.getVersion());
assertArrayEquals(current.getKey(), retrieved.getKey());
}
@Test
@Order(3)
void testApplyPepper() {
PepperKey pepper = pepperService.getCurrentPepper();
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
byte[] peppered = pepperService.applyPepper(TEST_PASSWORD, salt, pepper.getVersion());
assertNotNull(peppered);
assertEquals(32, peppered.length); // SHA-256 output
// Same inputs should produce same output
byte[] peppered2 = pepperService.applyPepper(TEST_PASSWORD, salt, pepper.getVersion());
assertArrayEquals(peppered, peppered2);
}
@Test
@Order(4)
void testPasswordHashingWithPepper() {
// Hash with bcrypt + pepper
PepperedPasswordService.PepperedHash hash = passwordService.hashWithBcrypt(TEST_PASSWORD, 12);
assertNotNull(hash);
assertNotNull(hash.getPepperVersion());
assertEquals(16, hash.getSalt().length);
// Verify
boolean verified = passwordService.verifyPassword(TEST_PASSWORD, hash);
assertTrue(verified);
// Wrong password should fail
boolean wrongVerified = passwordService.verifyPassword("WrongPassword", hash);
assertFalse(wrongVerified);
}
@Test
@Order(5)
void testPBKDF2WithPepper() {
// Hash with PBKDF2 + pepper
PepperedPasswordService.PepperedHash hash = passwordService.hashWithPBKDF2(
TEST_PASSWORD,
PepperedPasswordService.HashAlgorithm.PBKDF2_SHA256,
100000,
256
);
assertNotNull(hash);
assertEquals(100000, hash.getIterations());
// Verify
boolean verified = passwordService.verifyPassword(TEST_PASSWORD, hash);
assertTrue(verified);
}
@Test
@Order(6)
void testHashStorageFormat() {
PepperedPasswordService.PepperedHash original = passwordService.hashWithBcrypt(TEST_PASSWORD, 12);
String stored = original.toStorageFormat();
PepperedPasswordService.PepperedHash parsed =
PepperedPasswordService.PepperedHash.fromStorageFormat(stored);
assertEquals(original.getAlgorithm(), parsed.getAlgorithm());
assertEquals(original.getPepperVersion(), parsed.getPepperVersion());
assertArrayEquals(original.getSalt(), parsed.getSalt());
assertEquals(original.getHash(), parsed.getHash());
}
@Test
@Order(7)
void testPepperRotation() throws InterruptedException {
RotationConfig config = new RotationConfig(
1, // 1 second delay
true,
256
);
PepperRotationResult result = pepperService.rotateKeys(config);
assertNotNull(result);
assertNotNull(result.getNewVersion());
assertEquals(result.getOldVersion(), result.getNewVersion()); // Same if delay > 0
// Wait for activation
Thread.sleep(1100);
PepperKey newCurrent = pepperService.getCurrentPepper();
assertEquals(result.getNewVersion(), newCurrent.getVersion());
}
@Test
@Order(8)
void testNeedsUpgrade() {
// Create hash with weak parameters
PepperedPasswordService.PepperedHash weakHash = passwordService.hashWithBcrypt(TEST_PASSWORD, 8);
PepperedPasswordService.SecurityPolicy strongPolicy =
new PepperedPasswordService.SecurityPolicy.Builder()
.minBcryptCost(12)
.build();
boolean needsUpgrade = passwordService.needsUpgrade(weakHash, strongPolicy);
assertTrue(needsUpgrade);
// Upgrade
PepperedPasswordService.PepperedHash upgraded =
passwordService.upgradeHash(TEST_PASSWORD, weakHash, strongPolicy);
assertNotEquals(weakHash.getHash(), upgraded.getHash());
assertTrue(passwordService.verifyPassword(TEST_PASSWORD, upgraded));
}
@Test
@Order(9)
void testListVersions() {
List<PepperVersion> versions = pepperService.listVersions();
assertNotNull(versions);
assertFalse(versions.isEmpty());
// Should be sorted newest first
if (versions.size() > 1) {
assertTrue(versions.get(0).getCreatedAt()
.isAfter(versions.get(1).getCreatedAt()) ||
versions.get(0).getCreatedAt()
.equals(versions.get(1).getCreatedAt()));
}
}
@Test
@Order(10)
void testHealthCheck() {
PepperHealth health = pepperService.getHealth();
assertNotNull(health);
assertNotNull(health.getActiveKey());
assertTrue(health.getTotalKeys() > 0);
assertTrue(health.getSuccessRate() >= 0);
}
@Test
@Order(11)
void testRevokeKey() {
PepperKey current = pepperService.getCurrentPepper();
pepperService.revokeKey(current.getVersion(), RevocationReason.COMPROMISED);
// Should not be retrievable
assertFalse(pepperService.getPepperByVersion(current.getVersion()).isPresent());
// Service should have activated another key
assertNotNull(pepperService.getCurrentPepper());
assertNotEquals(current.getVersion(), pepperService.getCurrentPepper().getVersion());
}
@Test
@Order(12)
void testBackupAndRestore() throws Exception {
BackupConfig config = new BackupConfig();
config.setIncludeRevoked(true);
config.setEncryptionKey("test-encryption-key".getBytes());
// Create backup
byte[] backup = pepperService.backupKeys(config);
// Simulate disaster recovery
// Clear current service and restore
// Note: This would require reinitializing the service
assertNotNull(backup);
assertTrue(backup.length > 0);
}
@Test
@Order(13)
void testConcurrentAccess() throws InterruptedException {
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
boolean[] results = new boolean[threadCount];
for (int i = 0; i < threadCount; i++) {
final int index = i;
threads[i] = new Thread(() -> {
try {
PepperKey pepper = pepperService.getCurrentPepper();
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
byte[] peppered = pepperService.applyPepper(
TEST_PASSWORD, salt, pepper.getVersion()
);
results[index] = peppered != null && peppered.length == 32;
} catch (Exception e) {
results[index] = false;
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
for (boolean result : results) {
assertTrue(result);
}
}
@Test
@Order(14)
void testDifferentPeppersProduceDifferentHashes() {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
// Get two different pepper versions
List<PepperVersion> versions = pepperService.listVersions();
assertTrue(versions.size() >= 2);
String version1 = versions.get(0).getVersion();
String version2 = versions.get(1).getVersion();
byte[] hash1 = pepperService.applyPepper(TEST_PASSWORD, salt, version1);
byte[] hash2 = pepperService.applyPepper(TEST_PASSWORD, salt, version2);
assertFalse(Arrays.equals(hash1, hash2));
}
@Test
@Order(15)
void testKeyStrength() {
PepperKey current = pepperService.getCurrentPepper();
// Check key strength (should be at least 256 bits for HMAC-SHA256)
assertTrue(current.getKey().length * 8 >= 256);
// Check entropy (simple test)
double entropy = calculateEntropy(current.getKey());
assertTrue(entropy > 7.5); // Close to 8 bits per byte is good
}
private double calculateEntropy(byte[] data) {
int[] counts = new int[256];
for (byte b : data) {
counts[b & 0xFF]++;
}
double entropy = 0.0;
for (int count : counts) {
if (count > 0) {
double probability = (double) count / data.length;
entropy -= probability * (Math.log(probability) / Math.log(2));
}
}
return entropy;
}
}
9. Spring Boot Configuration
package com.security.pepper.config;
import com.security.pepper.CorePepperService;
import com.security.pepper.PepperConfig;
import com.security.pepper.PepperKeyService;
import com.security.pepper.hashing.PepperedPasswordService;
import com.security.pepper.kms.KmsPepperService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
@Configuration
@EnableScheduling
@ConfigurationProperties(prefix = "pepper")
public class PepperConfig {
private String storageType = "memory"; // memory, kms, hsm, vault
private int keySizeBytes = 32;
private String algorithm = "HmacSHA256";
private long keyLifetimeSeconds = 2592000; // 30 days
private long rotationCheckIntervalSeconds = 86400; // 24 hours
private int maxBackupAgeSeconds = 2592000; // 30 days
// KMS configuration
private KmsConfig kms = new KmsConfig();
// HSM configuration
private HsmConfig hsm = new HsmConfig();
@Bean
@ConditionalOnProperty(name = "pepper.storage-type", havingValue = "memory", matchIfMissing = true)
public PepperKeyService memoryPepperService() {
PepperConfig config = new PepperConfig();
config.setKeySizeBytes(keySizeBytes);
config.setAlgorithm(algorithm);
config.setKeyLifetimeSeconds(keyLifetimeSeconds);
return new CorePepperService(config);
}
@Bean
@ConditionalOnProperty(name = "pepper.storage-type", havingValue = "kms")
public PepperKeyService kmsPepperService() {
KmsClient kmsClient = KmsClient.builder()
.region(Region.of(kms.getRegion()))
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
return new KmsPepperService(
kmsClient,
kms.getKeyId(),
kms.getLocalEncryptionKey().getBytes()
);
}
@Bean
public PepperedPasswordService pepperedPasswordService(PepperKeyService pepperService) {
return new PepperedPasswordService(pepperService);
}
@Scheduled(fixedDelayString = "${pepper.rotation-check-interval-seconds:86400}000")
public void checkPepperRotation() {
// Check if pepper needs rotation
// Implementation would check age of current pepper
}
@Scheduled(cron = "0 0 2 * * *") // Daily at 2 AM
public void backupPepperKeys() {
// Automated backup of pepper keys
// Implementation would create encrypted backup
}
// Inner configuration classes
public static class KmsConfig {
private String region = "us-east-1";
private String keyId;
private String localEncryptionKey;
// getters and setters
}
public static class HsmConfig {
private String partition;
private String user;
private String password;
private String library;
// getters and setters
}
// Getters and setters for main config
public String getStorageType() { return storageType; }
public void setStorageType(String storageType) { this.storageType = storageType; }
public int getKeySizeBytes() { return keySizeBytes; }
public void setKeySizeBytes(int keySizeBytes) { this.keySizeBytes = keySizeBytes; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public long getKeyLifetimeSeconds() { return keyLifetimeSeconds; }
public void setKeyLifetimeSeconds(long keyLifetimeSeconds) {
this.keyLifetimeSeconds = keyLifetimeSeconds;
}
public KmsConfig getKms() { return kms; }
public void setKms(KmsConfig kms) { this.kms = kms; }
public HsmConfig getHsm() { return hsm; }
public void setHsm(HsmConfig hsm) { this.hsm = hsm; }
}
10. Monitoring and Alerting
package com.security.pepper.monitoring;
import com.security.pepper.PepperHealth;
import com.security.pepper.PepperKeyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class PepperMonitor {
private static final Logger logger = LoggerFactory.getLogger(PepperMonitor.class);
private final PepperKeyService pepperService;
private final Map<String, Alert> activeAlerts = new ConcurrentHashMap<>();
private final AtomicLong alertCounter = new AtomicLong(0);
public PepperMonitor(PepperKeyService pepperService) {
this.pepperService = pepperService;
}
/**
* Check pepper health and raise alerts
*/
public void checkHealth() {
PepperHealth health = pepperService.getHealth();
// Check for critical conditions
if (!health.isHealthy()) {
raiseAlert("PEPPER-001", "Pepper service unhealthy", Severity.CRITICAL);
}
if (health.getSuccessRate() < 95.0) {
raiseAlert("PEPPER-002",
"Low success rate: " + health.getSuccessRate() + "%",
Severity.WARNING);
}
if (!health.getExpiringKeys().isEmpty()) {
raiseAlert("PEPPER-003",
"Keys expiring soon: " + health.getExpiringKeys(),
Severity.WARNING);
}
// Log metrics
logMetrics(health);
}
/**
* Monitor pepper operations
*/
public void recordOperation(String operation, long durationMs, boolean success) {
logger.info("Pepper operation: {} - {}ms - {}",
operation, durationMs, success ? "SUCCESS" : "FAILURE");
if (!success) {
checkFailureRate(operation);
}
}
/**
* Track failed verification attempts
*/
public void recordFailedVerification(String userId, String pepperVersion) {
String key = "failed_verification_" + userId;
logger.warn("Failed verification for user: {}, pepper: {}", userId, pepperVersion);
// Check for brute force attempts
checkBruteForceAttempts(userId);
}
/**
* Alert on suspicious activity
*/
private void checkBruteForceAttempts(String userId) {
// Track attempts per user
// Raise alert if threshold exceeded
}
/**
* Check failure rate for operation
*/
private void checkFailureRate(String operation) {
// Track failure rate
// Raise alert if rate exceeds threshold
}
/**
* Raise alert
*/
private void raiseAlert(String code, String message, Severity severity) {
Alert alert = new Alert(
alertCounter.incrementAndGet(),
code,
message,
severity,
Instant.now()
);
activeAlerts.put(code, alert);
// Log alert
switch (severity) {
case CRITICAL:
logger.error("ALERT [{}]: {} - {}", code, severity, message);
break;
case WARNING:
logger.warn("ALERT [{}]: {} - {}", code, severity, message);
break;
case INFO:
logger.info("ALERT [{}]: {} - {}", code, severity, message);
break;
}
// Send to monitoring system (e.g., CloudWatch, Prometheus)
sendToMonitoringSystem(alert);
}
/**
* Clear alert
*/
public void clearAlert(String code) {
Alert removed = activeAlerts.remove(code);
if (removed != null) {
logger.info("Alert cleared: {}", code);
}
}
/**
* Get active alerts
*/
public Map<String, Alert> getActiveAlerts() {
return activeAlerts;
}
/**
* Log health metrics
*/
private void logMetrics(PepperHealth health) {
logger.info("Pepper Health - Keys: {}, Success Rate: {}%, Active: {}",
health.getTotalKeys(),
health.getSuccessRate(),
health.getActiveKey()
);
}
/**
* Send to external monitoring
*/
private void sendToMonitoringSystem(Alert alert) {
// Implementation for CloudWatch, Prometheus, etc.
}
// Severity levels
public enum Severity {
INFO,
WARNING,
CRITICAL
}
// Alert class
public static class Alert {
private final long id;
private final String code;
private final String message;
private final Severity severity;
private final Instant timestamp;
private boolean acknowledged;
public Alert(long id, String code, String message,
Severity severity, Instant timestamp) {
this.id = id;
this.code = code;
this.message = message;
this.severity = severity;
this.timestamp = timestamp;
this.acknowledged = false;
}
// getters and setters
}
}
Security Best Practices
1. Key Generation
// Use cryptographically secure random for pepper generation
public byte[] generateSecurePepper(int keySizeBytes) {
byte[] pepper = new byte[keySizeBytes];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(pepper);
return pepper;
}
2. Key Storage
// Never store peppers in plaintext
public class SecurePepperStorage {
public void storePepper(String version, byte[] pepper) {
// Encrypt before storage
byte[] encrypted = encryptPepper(pepper);
// Store in HSM or secure KMS
hsm.store(version, encrypted);
// Zeroize memory
Arrays.fill(pepper, (byte) 0);
}
}
3. Key Rotation
// Implement regular rotation
@Scheduled(cron = "0 0 2 * * 0") // Weekly on Sunday at 2 AM
public void rotatePepper() {
RotationConfig config = new RotationConfig(
3600, // 1 hour activation delay
true, // Deactivate previous
256 // 256-bit key
);
pepperService.rotateKeys(config);
}
4. Audit Logging
// Log all pepper operations
public void logPepperOperation(String operation, String version, boolean success) {
AuditLog log = new AuditLog();
log.setOperation(operation);
log.setPepperVersion(version);
log.setSuccess(success);
log.setTimestamp(Instant.now());
log.setPrincipal(SecurityContextHolder.getContext().getAuthentication().getName());
auditRepository.save(log);
}
5. Access Control
// Restrict access to pepper keys
@PreAuthorize("hasRole('ADMIN')")
public PepperKey getCurrentPepper() {
return pepperService.getCurrentPepper();
}
Conclusion
Pepper keys provide an essential additional security layer for password hashing. This comprehensive implementation offers:
Key Features
- Multiple storage backends: Memory, KMS (AWS/GCP/Azure), HSM, Vault
- Automatic key rotation with activation delays
- Key versioning for backward compatibility
- Secure backup and restore capabilities
- Comprehensive monitoring and alerting
- Integration with bcrypt/PBKDF2/Argon2
- REST API for management
- Audit logging for compliance
Security Benefits
- Defense in depth: Even if database is compromised, passwords remain secure
- Key separation: Pepper stored separately from hashes
- Rotation capability: Regular key rotation limits exposure
- Revocation support: Compromised keys can be revoked
- HSM/KMS integration: Hardware-level protection
Recommendations
- Use HSM or KMS for production pepper storage
- Rotate peppers regularly (30-90 days)
- Implement proper access controls to pepper keys
- Monitor for anomalies and failed verifications
- Maintain offline backups of pepper keys
- Use at least 256-bit keys for adequate security
This implementation ensures that even if an attacker gains access to your password database, they cannot crack the hashes without also compromising your separately stored pepper keys.
Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)
Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/
Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/
Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/
Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/
Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/
Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/
Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/
Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/
Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/