Build a robust, production-ready tokenization service to protect sensitive data while maintaining usability in your applications.
What is Tokenization?
Tokenization replaces sensitive data with non-sensitive equivalents (tokens) that have no extrinsic or exploitable meaning. Unlike encryption, tokenization is not mathematically reversible without accessing the token vault.
Use Cases
- Payment card data (PCI DSS compliance)
- Personally Identifiable Information (PII)
- Healthcare data (HIPAA compliance)
- API keys and credentials
- Social Security Numbers
Architecture Overview
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │ Application │───▶│ Tokenization │───▶│ Token │ │ Layer │ │ Service │ │ Vault │ └─────────────┘ └──────────────────┘ └─────────────┘ │ ┌─────────────┐ │ Key │ │ Management │ └─────────────┘
Core Dependencies
Maven Configuration
<properties>
<junit.version>5.9.2</junit.version>
<guava.version>32.1.2-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot Starter (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Data Persistence -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.220</version>
<scope>runtime</scope>
</dependency>
<!-- Security & Cryptography -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Core Tokenization Service
1. Token Generator Interface & Implementations
public interface TokenGenerator {
String generateToken(String sensitiveData);
String generateToken(String sensitiveData, String tokenType);
}
/**
* Secure random token generator
*/
@Component
public class SecureRandomTokenGenerator implements TokenGenerator {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final int DEFAULT_TOKEN_LENGTH = 32;
private static final String CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@Override
public String generateToken(String sensitiveData) {
return generateToken(sensitiveData, "DEFAULT");
}
@Override
public String generateToken(String sensitiveData, String tokenType) {
int tokenLength = getTokenLengthForType(tokenType);
StringBuilder token = new StringBuilder(tokenLength);
// Add prefix based on token type
token.append(getTokenPrefix(tokenType));
// Generate random part
for (int i = 0; i < tokenLength - token.length(); i++) {
int index = SECURE_RANDOM.nextInt(CHARACTERS.length());
token.append(CHARACTERS.charAt(index));
}
return token.toString();
}
private String getTokenPrefix(String tokenType) {
return switch (tokenType.toUpperCase()) {
case "CREDIT_CARD" -> "CC_";
case "SSN" -> "SSN_";
case "EMAIL" -> "EML_";
case "PHONE" -> "PHN_";
default -> "TK_";
};
}
private int getTokenLengthForType(String tokenType) {
return switch (tokenType.toUpperCase()) {
case "CREDIT_CARD" -> 20;
case "SSN" -> 16;
case "EMAIL" -> 24;
case "PHONE" -> 18;
default -> DEFAULT_TOKEN_LENGTH;
};
}
}
/**
* Format-preserving token generator
*/
@Component
public class FormatPreservingTokenGenerator implements TokenGenerator {
private static final SecureRandom RANDOM = new SecureRandom();
@Override
public String generateToken(String sensitiveData) {
return generateToken(sensitiveData, "DEFAULT");
}
@Override
public String generateToken(String sensitiveData, String tokenType) {
if (sensitiveData == null || sensitiveData.trim().isEmpty()) {
throw new IllegalArgumentException("Sensitive data cannot be null or empty");
}
return switch (tokenType.toUpperCase()) {
case "CREDIT_CARD" -> generateCreditCardToken(sensitiveData);
case "SSN" -> generateSSNToken(sensitiveData);
case "PHONE" -> generatePhoneToken(sensitiveData);
case "EMAIL" -> generateEmailToken(sensitiveData);
default -> generateDefaultToken(sensitiveData);
};
}
private String generateCreditCardToken(String creditCard) {
String cleaned = creditCard.replaceAll("[^0-9]", "");
// Keep first 6 (BIN) and last 4 digits, tokenize the middle
if (cleaned.length() >= 16) {
String bin = cleaned.substring(0, 6);
String lastFour = cleaned.substring(cleaned.length() - 4);
String middle = generateNumericToken(cleaned.length() - 10);
return bin + middle + lastFour;
}
return generateNumericToken(cleaned.length());
}
private String generateSSNToken(String ssn) {
String cleaned = ssn.replaceAll("[^0-9]", "");
if (cleaned.length() == 9) {
return "XXX-XX-" + cleaned.substring(5);
}
return generateNumericToken(cleaned.length());
}
private String generatePhoneToken(String phone) {
String cleaned = phone.replaceAll("[^0-9]", "");
if (cleaned.length() == 10) {
return "XXX-XXX-" + cleaned.substring(6);
}
return generateNumericToken(cleaned.length());
}
private String generateEmailToken(String email) {
String[] parts = email.split("@");
if (parts.length == 2) {
String localPart = parts[0];
String domain = parts[1];
// Keep first and last character of local part
if (localPart.length() >= 2) {
String maskedLocal = localPart.charAt(0) +
"***" +
localPart.charAt(localPart.length() - 1);
return maskedLocal + "@" + domain;
}
}
return "***@" + (email.contains("@") ? email.split("@")[1] : "example.com");
}
private String generateDefaultToken(String data) {
// For generic data, maintain similar length but replace with random chars
char[] token = new char[data.length()];
for (int i = 0; i < data.length(); i++) {
if (Character.isDigit(data.charAt(i))) {
token[i] = (char) ('0' + RANDOM.nextInt(10));
} else if (Character.isLetter(data.charAt(i))) {
token[i] = (char) ('A' + RANDOM.nextInt(26));
} else {
token[i] = data.charAt(i); // Keep separators
}
}
return new String(token);
}
private String generateNumericToken(int length) {
char[] token = new char[length];
for (int i = 0; i < length; i++) {
token[i] = (char) ('0' + RANDOM.nextInt(10));
}
return new String(token);
}
}
2. Data Encryption Service
public interface EncryptionService {
String encrypt(String data);
String decrypt(String encryptedData);
}
@Component
public class AESEncryptionService implements EncryptionService {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private final SecretKey secretKey;
private final Cipher cipher;
public AESEncryptionService(@Value("${encryption.secret.key}") String base64Key)
throws GeneralSecurityException {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
this.secretKey = new SecretKeySpec(keyBytes, "AES");
this.cipher = Cipher.getInstance(ALGORITHM);
}
@Override
public String encrypt(String data) {
try {
byte[] iv = new byte[IV_LENGTH_BYTE];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
byte[] encrypted = new byte[iv.length + cipherText.length];
System.arraycopy(iv, 0, encrypted, 0, iv.length);
System.arraycopy(cipherText, 0, encrypted, iv.length, cipherText.length);
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("Encryption failed", e);
}
}
@Override
public String decrypt(String encryptedData) {
try {
byte[] decoded = Base64.getDecoder().decode(encryptedData);
byte[] iv = Arrays.copyOfRange(decoded, 0, IV_LENGTH_BYTE);
byte[] cipherText = Arrays.copyOfRange(decoded, IV_LENGTH_BYTE, decoded.length);
GCMParameterSpec parameterSpec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}
3. Token Entity and Repository
@Entity
@Table(name = "tokens", indexes = {
@Index(name = "idx_token_value", columnList = "tokenValue"),
@Index(name = "idx_token_type", columnList = "tokenType"),
@Index(name = "idx_created_at", columnList = "createdAt")
})
public class Token {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String tokenValue;
@Column(nullable = false, length = 1000)
private String encryptedData;
@Column(nullable = false)
private String tokenType;
@Column
private String metadata;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column
private LocalDateTime expiresAt;
@Column(nullable = false)
private boolean active = true;
// Constructors
public Token() {}
public Token(String tokenValue, String encryptedData, String tokenType) {
this.tokenValue = tokenValue;
this.encryptedData = encryptedData;
this.tokenType = tokenType;
this.createdAt = LocalDateTime.now();
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTokenValue() { return tokenValue; }
public void setTokenValue(String tokenValue) { this.tokenValue = tokenValue; }
public String getEncryptedData() { return encryptedData; }
public void setEncryptedData(String encryptedData) { this.encryptedData = encryptedData; }
public String getTokenType() { return tokenType; }
public void setTokenType(String tokenType) { this.tokenType = tokenType; }
public String getMetadata() { return metadata; }
public void setMetadata(String metadata) { this.metadata = metadata; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
@Repository
public interface TokenRepository extends JpaRepository<Token, Long> {
Optional<Token> findByTokenValue(String tokenValue);
Optional<Token> findByTokenValueAndActiveTrue(String tokenValue);
List<Token> findByTokenType(String tokenType);
List<Token> findByExpiresAtBeforeAndActiveTrue(LocalDateTime dateTime);
boolean existsByTokenValue(String tokenValue);
@Query("SELECT COUNT(t) FROM Token t WHERE t.createdAt >= :startDate AND t.createdAt < :endDate")
Long countTokensCreatedBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
@Modifying
@Query("UPDATE Token t SET t.active = false WHERE t.expiresAt < :now")
int deactivateExpiredTokens(@Param("now") LocalDateTime now);
}
4. Main Tokenization Service
public interface TokenizationService {
TokenizationResponse tokenize(TokenizationRequest request);
DetokenizationResponse detokenize(DetokenizationRequest request);
boolean validateToken(String token);
boolean deleteToken(String token);
TokenMetadata getTokenMetadata(String token);
}
@Service
@Transactional
public class TokenizationServiceImpl implements TokenizationService {
private final TokenRepository tokenRepository;
private final EncryptionService encryptionService;
private final TokenGenerator secureTokenGenerator;
private final TokenGenerator formatPreservingGenerator;
private final Map<String, TokenGenerator> tokenGenerators;
public TokenizationServiceImpl(TokenRepository tokenRepository,
EncryptionService encryptionService,
SecureRandomTokenGenerator secureTokenGenerator,
FormatPreservingTokenGenerator formatPreservingGenerator) {
this.tokenRepository = tokenRepository;
this.encryptionService = encryptionService;
this.secureTokenGenerator = secureTokenGenerator;
this.formatPreservingGenerator = formatPreservingGenerator;
this.tokenGenerators = Map.of(
"SECURE_RANDOM", secureTokenGenerator,
"FORMAT_PRESERVING", formatPreservingGenerator
);
}
@Override
public TokenizationResponse tokenize(TokenizationRequest request) {
try {
// Validate input
validateTokenizationRequest(request);
// Select appropriate token generator
TokenGenerator generator = selectTokenGenerator(request);
// Generate unique token
String token = generateUniqueToken(generator, request.getData(), request.getTokenType());
// Encrypt sensitive data
String encryptedData = encryptionService.encrypt(request.getData());
// Create and save token entity
Token tokenEntity = new Token(token, encryptedData, request.getTokenType());
// Set expiration if provided
if (request.getExpiryMinutes() != null) {
tokenEntity.setExpiresAt(LocalDateTime.now().plusMinutes(request.getExpiryMinutes()));
}
// Set metadata
if (request.getMetadata() != null) {
tokenEntity.setMetadata(request.getMetadata());
}
tokenRepository.save(tokenEntity);
return TokenizationResponse.builder()
.success(true)
.token(token)
.tokenType(request.getTokenType())
.createdAt(tokenEntity.getCreatedAt())
.expiresAt(tokenEntity.getExpiresAt())
.build();
} catch (Exception e) {
return TokenizationResponse.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
}
@Override
public DetokenizationResponse detokenize(DetokenizationRequest request) {
try {
// Find active token
Token token = tokenRepository.findByTokenValueAndActiveTrue(request.getToken())
.orElseThrow(() -> new IllegalArgumentException("Invalid or inactive token"));
// Check expiration
if (token.getExpiresAt() != null && token.getExpiresAt().isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("Token has expired");
}
// Decrypt data
String decryptedData = encryptionService.decrypt(token.getEncryptedData());
return DetokenizationResponse.builder()
.success(true)
.originalData(decryptedData)
.tokenType(token.getTokenType())
.build();
} catch (Exception e) {
return DetokenizationResponse.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
}
@Override
public boolean validateToken(String token) {
return tokenRepository.findByTokenValueAndActiveTrue(token)
.map(t -> t.getExpiresAt() == null || t.getExpiresAt().isAfter(LocalDateTime.now()))
.orElse(false);
}
@Override
public boolean deleteToken(String token) {
return tokenRepository.findByTokenValue(token)
.map(t -> {
t.setActive(false);
tokenRepository.save(t);
return true;
})
.orElse(false);
}
@Override
public TokenMetadata getTokenMetadata(String token) {
return tokenRepository.findByTokenValue(token)
.map(t -> TokenMetadata.builder()
.tokenType(t.getTokenType())
.createdAt(t.getCreatedAt())
.expiresAt(t.getExpiresAt())
.active(t.isActive())
.metadata(t.getMetadata())
.build())
.orElse(null);
}
private void validateTokenizationRequest(TokenizationRequest request) {
if (request.getData() == null || request.getData().trim().isEmpty()) {
throw new IllegalArgumentException("Data cannot be null or empty");
}
if (request.getTokenType() == null || request.getTokenType().trim().isEmpty()) {
throw new IllegalArgumentException("Token type cannot be null or empty");
}
// Validate specific token types
switch (request.getTokenType().toUpperCase()) {
case "CREDIT_CARD":
validateCreditCard(request.getData());
break;
case "SSN":
validateSSN(request.getData());
break;
case "EMAIL":
validateEmail(request.getData());
break;
}
}
private void validateCreditCard(String creditCard) {
String cleaned = creditCard.replaceAll("[^0-9]", "");
if (cleaned.length() < 13 || cleaned.length() > 19) {
throw new IllegalArgumentException("Invalid credit card number length");
}
// Basic Luhn check
if (!isValidLuhn(cleaned)) {
throw new IllegalArgumentException("Invalid credit card number");
}
}
private void validateSSN(String ssn) {
String cleaned = ssn.replaceAll("[^0-9]", "");
if (cleaned.length() != 9) {
throw new IllegalArgumentException("SSN must be 9 digits");
}
// Check for invalid SSN patterns
if (cleaned.startsWith("000") || cleaned.startsWith("666") ||
cleaned.substring(3, 5).equals("00") || cleaned.substring(5).equals("0000")) {
throw new IllegalArgumentException("Invalid SSN pattern");
}
}
private void validateEmail(String email) {
String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
if (!email.matches(emailRegex)) {
throw new IllegalArgumentException("Invalid email format");
}
}
private boolean isValidLuhn(String number) {
int sum = 0;
boolean alternate = false;
for (int i = number.length() - 1; i >= 0; i--) {
int n = Integer.parseInt(number.substring(i, i + 1));
if (alternate) {
n *= 2;
if (n > 9) {
n = (n % 10) + 1;
}
}
sum += n;
alternate = !alternate;
}
return (sum % 10 == 0);
}
private TokenGenerator selectTokenGenerator(TokenizationRequest request) {
String generatorType = request.getTokenGeneratorType();
if (generatorType != null && tokenGenerators.containsKey(generatorType.toUpperCase())) {
return tokenGenerators.get(generatorType.toUpperCase());
}
// Default based on token type
return switch (request.getTokenType().toUpperCase()) {
case "CREDIT_CARD", "SSN", "PHONE" -> formatPreservingGenerator;
default -> secureTokenGenerator;
};
}
private String generateUniqueToken(TokenGenerator generator, String data, String tokenType) {
String token;
int attempts = 0;
int maxAttempts = 5;
do {
token = generator.generateToken(data, tokenType);
attempts++;
if (attempts > maxAttempts) {
throw new RuntimeException("Failed to generate unique token after " + maxAttempts + " attempts");
}
} while (tokenRepository.existsByTokenValue(token));
return token;
}
@Scheduled(cron = "0 0 2 * * ?") // Run daily at 2 AM
public void cleanupExpiredTokens() {
int deactivated = tokenRepository.deactivateExpiredTokens(LocalDateTime.now());
logger.info("Deactivated {} expired tokens", deactivated);
}
}
5. DTO Classes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenizationRequest {
@NotBlank
private String data;
@NotBlank
private String tokenType; // CREDIT_CARD, SSN, EMAIL, PHONE, etc.
private String tokenGeneratorType; // SECURE_RANDOM, FORMAT_PRESERVING
private Integer expiryMinutes;
private String metadata;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenizationResponse {
private boolean success;
private String token;
private String tokenType;
private LocalDateTime createdAt;
private LocalDateTime expiresAt;
private String errorMessage;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DetokenizationRequest {
@NotBlank
private String token;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DetokenizationResponse {
private boolean success;
private String originalData;
private String tokenType;
private String errorMessage;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenMetadata {
private String tokenType;
private LocalDateTime createdAt;
private LocalDateTime expiresAt;
private boolean active;
private String metadata;
}
6. REST Controller
@RestController
@RequestMapping("/api/v1/tokenization")
@Validated
public class TokenizationController {
private final TokenizationService tokenizationService;
public TokenizationController(TokenizationService tokenizationService) {
this.tokenizationService = tokenizationService;
}
@PostMapping("/tokenize")
public ResponseEntity<TokenizationResponse> tokenize(
@Valid @RequestBody TokenizationRequest request) {
TokenizationResponse response = tokenizationService.tokenize(request);
HttpStatus status = response.isSuccess() ?
HttpStatus.CREATED : HttpStatus.BAD_REQUEST;
return ResponseEntity.status(status).body(response);
}
@PostMapping("/detokenize")
public ResponseEntity<DetokenizationResponse> detokenize(
@Valid @RequestBody DetokenizationRequest request) {
DetokenizationResponse response = tokenizationService.detokenize(request);
HttpStatus status = response.isSuccess() ?
HttpStatus.OK : HttpStatus.BAD_REQUEST;
return ResponseEntity.status(status).body(response);
}
@GetMapping("/validate/{token}")
public ResponseEntity<Map<String, Object>> validateToken(@PathVariable String token) {
boolean isValid = tokenizationService.validateToken(token);
TokenMetadata metadata = tokenizationService.getTokenMetadata(token);
Map<String, Object> response = new HashMap<>();
response.put("valid", isValid);
response.put("metadata", metadata);
return ResponseEntity.ok(response);
}
@DeleteMapping("/{token}")
public ResponseEntity<Map<String, Object>> deleteToken(@PathVariable String token) {
boolean deleted = tokenizationService.deleteToken(token);
Map<String, Object> response = new HashMap<>();
response.put("deleted", deleted);
return deleted ?
ResponseEntity.ok(response) :
ResponseEntity.notFound().build();
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<TokenizationResponse> handleIllegalArgument(IllegalArgumentException ex) {
TokenizationResponse response = TokenizationResponse.builder()
.success(false)
.errorMessage(ex.getMessage())
.build();
return ResponseEntity.badRequest().body(response);
}
}
Configuration
application.yml
server: port: 8080 spring: datasource: url: jdbc:h2:mem:tokendb driverClassName: org.h2.Driver username: sa password: password jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true h2: console: enabled: true encryption: secret: key: "your-base64-encoded-256-bit-key-here" # Generate with: openssl rand -base64 32 tokenization: default: expiry-minutes: 1440 # 24 hours cleanup: enabled: true cron: "0 0 2 * * ?" logging: level: com.yourcompany.tokenization: DEBUG
Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/v1/tokenization/**").authenticated()
.anyRequest().permitRequired()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
Testing
Unit Tests
@ExtendWith(MockitoExtension.class)
class TokenizationServiceTest {
@Mock
private TokenRepository tokenRepository;
@Mock
private EncryptionService encryptionService;
@Mock
private TokenGenerator tokenGenerator;
@InjectMocks
private TokenizationServiceImpl tokenizationService;
@Test
void testTokenizeCreditCard() {
// Given
TokenizationRequest request = TokenizationRequest.builder()
.data("4111111111111111")
.tokenType("CREDIT_CARD")
.build();
when(tokenGenerator.generateToken(anyString(), anyString()))
.thenReturn("CC_1234567890123456");
when(encryptionService.encrypt(anyString()))
.thenReturn("encrypted-data");
when(tokenRepository.existsByTokenValue(anyString()))
.thenReturn(false);
when(tokenRepository.save(any(Token.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
// When
TokenizationResponse response = tokenizationService.tokenize(request);
// Then
assertTrue(response.isSuccess());
assertEquals("CC_1234567890123456", response.getToken());
assertEquals("CREDIT_CARD", response.getTokenType());
}
@Test
void testDetokenizeValidToken() {
// Given
Token token = new Token("test-token", "encrypted-data", "CREDIT_CARD");
token.setActive(true);
when(tokenRepository.findByTokenValueAndActiveTrue("test-token"))
.thenReturn(Optional.of(token));
when(encryptionService.decrypt("encrypted-data"))
.thenReturn("4111111111111111");
DetokenizationRequest request = new DetokenizationRequest("test-token");
// When
DetokenizationResponse response = tokenizationService.detokenize(request);
// Then
assertTrue(response.isSuccess());
assertEquals("4111111111111111", response.getOriginalData());
}
}
Key Management Best Practices
@Component
public class KeyManagementService {
@Value("${encryption.secret.key}")
private String currentKey;
private final Map<String, String> keyVersions = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
keyVersions.put("v1", currentKey);
}
public void rotateKey() {
try {
// Generate new key
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey newKey = keyGen.generateKey();
String newKeyBase64 = Base64.getEncoder().encodeToString(newKey.getEncoded());
// Store new key version
String newVersion = "v" + (keyVersions.size() + 1);
keyVersions.put(newVersion, newKeyBase64);
// Re-encrypt all tokens with new key (in background)
reencryptAllTokens(newKeyBase64);
// Update current key
this.currentKey = newKeyBase64;
} catch (Exception e) {
throw new RuntimeException("Key rotation failed", e);
}
}
@Async
public void reencryptAllTokens(String newKey) {
// Implementation for re-encrypting all tokens with new key
// This should be done in batches for large datasets
}
}
Performance Considerations
- Database Indexing: Ensure proper indexes on tokenValue, tokenType, and createdAt
- Caching: Implement caching for frequently accessed tokens
- Connection Pooling: Use connection pools for database operations
- Batch Operations: Process token cleanup in batches
- Monitoring: Implement metrics and monitoring
This tokenization service provides a solid foundation for securing sensitive data in your Java applications while maintaining compliance with various regulatory requirements.