AWS Key Management Service (KMS) is the cornerstone for managing cryptographic keys and operations in the AWS ecosystem. Unlike traditional key stores, KMS provides a fully managed, highly available service for creating and controlling encryption keys. Java applications can leverage KMS for everything from simple encryption to complex multi-region key strategies.
AWS KMS Core Concepts
Key Types:
- Customer Master Keys (CMKs): Your primary resources in KMS (now called KMS keys)
- AWS Managed Keys: Automatically created for AWS services
- AWS Owned Keys: Shared across multiple AWS customers
Key Operations:
- Encrypt/Decrypt: Symmetric encryption operations
- GenerateDataKey: Create data encryption keys
- Sign/Verify: Asymmetric signing operations
- Re-Encrypt: Change encryption context or key
AWS SDK Setup and Configuration
1. Maven Dependencies
<properties>
<aws.sdk.version>2.20.0</aws.sdk.version>
</properties>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>kms</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>auth</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- For enhanced async support -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
</dependencies>
2. Client Configuration
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.KmsAsyncClient;
public class KMSClientFactory {
public static KmsClient createSyncClient() {
return KmsClient.builder()
.region(Region.US_EAST_1) // Your preferred region
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("ACCESS_KEY", "SECRET_KEY")))
.build();
}
public static KmsAsyncClient createAsyncClient() {
return KmsAsyncClient.builder()
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("ACCESS_KEY", "SECRET_KEY")))
.build();
}
// For production with IAM roles
public static KmsClient createClientWithDefaultCredentials() {
return KmsClient.builder()
.region(Region.US_EAST_1)
.build(); // Uses default credential chain
}
}
Core KMS Operations
1. Basic Encryption and Decryption
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.model.*;
public class BasicKMSOperations {
private final KmsClient kmsClient;
private final String keyId; // Can be key ID, ARN, or alias
public BasicKMSOperations(KmsClient kmsClient, String keyId) {
this.kmsClient = kmsClient;
this.keyId = keyId;
}
public byte[] encryptData(byte[] plaintext, Map<String, String> encryptionContext) {
try {
EncryptRequest encryptRequest = EncryptRequest.builder()
.keyId(keyId)
.plaintext(SdkBytes.fromByteArray(plaintext))
.encryptionContext(encryptionContext)
.build();
EncryptResponse encryptResponse = kmsClient.encrypt(encryptRequest);
return encryptResponse.ciphertextBlob().asByteArray();
} catch (KmsException e) {
throw new RuntimeException("Encryption failed", e);
}
}
public byte[] decryptData(byte[] ciphertext, Map<String, String> encryptionContext) {
try {
DecryptRequest decryptRequest = DecryptRequest.builder()
.ciphertextBlob(SdkBytes.fromByteArray(ciphertext))
.encryptionContext(encryptionContext)
.build();
DecryptResponse decryptResponse = kmsClient.decrypt(decryptRequest);
return decryptResponse.plaintext().asByteArray();
} catch (KmsException e) {
throw new RuntimeException("Decryption failed", e);
}
}
// String-based convenience methods
public String encryptString(String plaintext, Map<String, String> encryptionContext) {
byte[] encrypted = encryptData(plaintext.getBytes(StandardCharsets.UTF_8), encryptionContext);
return Base64.getEncoder().encodeToString(encrypted);
}
public String decryptString(String encryptedBase64, Map<String, String> encryptionContext) {
byte[] encrypted = Base64.getDecoder().decode(encryptedBase64);
byte[] decrypted = decryptData(encrypted, encryptionContext);
return new String(decrypted, StandardCharsets.UTF_8);
}
// Example usage
public static void main(String[] args) {
KmsClient kmsClient = KMSClientFactory.createSyncClient();
BasicKMSOperations kmsOps = new BasicKMSOperations(kmsClient, "alias/my-app-key");
Map<String, String> context = Map.of(
"environment", "production",
"service", "user-service",
"purpose", "user-data-encryption"
);
String sensitiveData = "Credit card: 4111-1111-1111-1111";
String encrypted = kmsOps.encryptString(sensitiveData, context);
String decrypted = kmsOps.decryptString(encrypted, context);
System.out.println("Original: " + sensitiveData);
System.out.println("Encrypted: " + encrypted);
System.out.println("Decrypted: " + decrypted);
}
}
2. Envelope Encryption with Data Keys
Envelope encryption is the recommended pattern for large data encryption:
public class EnvelopeEncryptionService {
private final KmsClient kmsClient;
private final String keyId;
public EnvelopeEncryptionService(KmsClient kmsClient, String keyId) {
this.kmsClient = kmsClient;
this.keyId = keyId;
}
public static class EncryptedData {
private final byte[] encryptedDataKey;
private final byte[] encryptedData;
private final Map<String, String> encryptionContext;
private final String keyAlgorithm; // e.g., "AES/GCM/NoPadding"
// Constructor, getters, and serialization methods
public EncryptedData(byte[] encryptedDataKey, byte[] encryptedData,
Map<String, String> encryptionContext, String keyAlgorithm) {
this.encryptedDataKey = encryptedDataKey;
this.encryptedData = encryptedData;
this.encryptionContext = encryptionContext;
this.keyAlgorithm = keyAlgorithm;
}
public String toJson() {
// JSON serialization for storage
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("encryptedDataKey", Base64.getEncoder().encodeToString(encryptedDataKey));
jsonMap.put("encryptedData", Base64.getEncoder().encodeToString(encryptedData));
jsonMap.put("encryptionContext", encryptionContext);
jsonMap.put("keyAlgorithm", keyAlgorithm);
return new Gson().toJson(jsonMap);
}
public static EncryptedData fromJson(String json) {
// JSON deserialization
Map<String, Object> jsonMap = new Gson().fromJson(json, Map.class);
byte[] encryptedDataKey = Base64.getDecoder().decode((String) jsonMap.get("encryptedDataKey"));
byte[] encryptedData = Base64.getDecoder().decode((String) jsonMap.get("encryptedData"));
@SuppressWarnings("unchecked")
Map<String, String> context = (Map<String, String>) jsonMap.get("encryptionContext");
String algorithm = (String) jsonMap.get("keyAlgorithm");
return new EncryptedData(encryptedDataKey, encryptedData, context, algorithm);
}
}
public EncryptedData encryptLargeData(byte[] data, Map<String, String> encryptionContext) {
try {
// Step 1: Generate data key from KMS
GenerateDataKeyRequest dataKeyRequest = GenerateDataKeyRequest.builder()
.keyId(keyId)
.keySpec(DataKeySpec.AES_256)
.encryptionContext(encryptionContext)
.build();
GenerateDataKeyResponse dataKeyResponse = kmsClient.generateDataKey(dataKeyRequest);
byte[] plaintextDataKey = dataKeyResponse.plaintext().asByteArray();
byte[] encryptedDataKey = dataKeyResponse.ciphertextBlob().asByteArray();
// Step 2: Use data key for local encryption
byte[] encryptedData = encryptWithLocalKey(plaintextDataKey, data);
// Step 3: Securely wipe plaintext key from memory
Arrays.fill(plaintextDataKey, (byte) 0);
return new EncryptedData(encryptedDataKey, encryptedData, encryptionContext, "AES/GCM/NoPadding");
} catch (Exception e) {
throw new RuntimeException("Envelope encryption failed", e);
}
}
public byte[] decryptLargeData(EncryptedData encryptedData) {
try {
// Step 1: Decrypt data key using KMS
DecryptRequest decryptRequest = DecryptRequest.builder()
.ciphertextBlob(SdkBytes.fromByteArray(encryptedData.getEncryptedDataKey()))
.encryptionContext(encryptedData.getEncryptionContext())
.build();
DecryptResponse decryptResponse = kmsClient.decrypt(decryptRequest);
byte[] plaintextDataKey = decryptResponse.plaintext().asByteArray();
// Step 2: Use decrypted data key for local decryption
byte[] decryptedData = decryptWithLocalKey(plaintextDataKey, encryptedData.getEncryptedData());
// Step 3: Securely wipe plaintext key from memory
Arrays.fill(plaintextDataKey, (byte) 0);
return decryptedData;
} catch (Exception e) {
throw new RuntimeException("Envelope decryption failed", e);
}
}
private byte[] encryptWithLocalKey(byte[] key, byte[] data) throws GeneralSecurityException {
// Use AES/GCM for authenticated encryption
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Generate IV
byte[] iv = new byte[12];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
byte[] encrypted = cipher.doFinal(data);
// Combine IV and encrypted data
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(iv);
output.write(encrypted);
return output.toByteArray();
}
private byte[] decryptWithLocalKey(byte[] key, byte[] encryptedData) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Extract IV (first 12 bytes)
byte[] iv = Arrays.copyOfRange(encryptedData, 0, 12);
byte[] actualEncryptedData = Arrays.copyOfRange(encryptedData, 12, encryptedData.length);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
return cipher.doFinal(actualEncryptedData);
}
}
3. Asynchronous KMS Operations
public class AsyncKMSOperations {
private final KmsAsyncClient kmsAsyncClient;
private final String keyId;
public AsyncKMSOperations(KmsAsyncClient kmsAsyncClient, String keyId) {
this.kmsAsyncClient = kmsAsyncClient;
this.keyId = keyId;
}
public CompletableFuture<byte[]> encryptAsync(byte[] plaintext, Map<String, String> encryptionContext) {
EncryptRequest request = EncryptRequest.builder()
.keyId(keyId)
.plaintext(SdkBytes.fromByteArray(plaintext))
.encryptionContext(encryptionContext)
.build();
return kmsAsyncClient.encrypt(request)
.thenApply(EncryptResponse::ciphertextBlob)
.thenApply(SdkBytes::asByteArray)
.exceptionally(throwable -> {
throw new RuntimeException("Async encryption failed", throwable);
});
}
public CompletableFuture<byte[]> decryptAsync(byte[] ciphertext, Map<String, String> encryptionContext) {
DecryptRequest request = DecryptRequest.builder()
.ciphertextBlob(SdkBytes.fromByteArray(ciphertext))
.encryptionContext(encryptionContext)
.build();
return kmsAsyncClient.decrypt(request)
.thenApply(DecryptResponse::plaintext)
.thenApply(SdkBytes::asByteArray)
.exceptionally(throwable -> {
throw new RuntimeException("Async decryption failed", throwable);
});
}
// Batch encryption example
public CompletableFuture<List<byte[]>> encryptBatch(List<byte[]> plaintexts,
Map<String, String> encryptionContext) {
List<CompletableFuture<byte[]>> futures = plaintexts.stream()
.map(data -> encryptAsync(data, encryptionContext))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()));
}
}
Advanced KMS Patterns
1. Multi-Region Key Strategy
public class MultiRegionKMSService {
private final Map<String, KmsClient> regionalClients;
private final String primaryKeyArn;
private final Map<String, String> replicaKeyArns; // region -> key ARN
public MultiRegionKMSService(String primaryRegion, String primaryKeyArn,
Map<String, String> replicaKeyArns) {
this.primaryKeyArn = primaryKeyArn;
this.replicaKeyArns = replicaKeyArns;
this.regionalClients = new HashMap<>();
// Initialize clients for all regions
regionalClients.put(primaryRegion, KmsClient.builder()
.region(Region.of(primaryRegion))
.build());
replicaKeyArns.keySet().forEach(region -> {
regionalClients.put(region, KmsClient.builder()
.region(Region.of(region))
.build());
});
}
public byte[] encryptInRegion(String region, byte[] plaintext,
Map<String, String> encryptionContext) {
KmsClient client = regionalClients.get(region);
String keyArn = region.equals(getPrimaryRegion()) ? primaryKeyArn : replicaKeyArns.get(region);
if (client == null || keyArn == null) {
throw new IllegalArgumentException("Unsupported region: " + region);
}
EncryptRequest request = EncryptRequest.builder()
.keyId(keyArn)
.plaintext(SdkBytes.fromByteArray(plaintext))
.encryptionContext(encryptionContext)
.build();
return client.encrypt(request).ciphertextBlob().asByteArray();
}
public byte[] decryptInAnyRegion(byte[] ciphertext, Map<String, String> encryptionContext) {
// Try primary region first
try {
return decryptInRegion(getPrimaryRegion(), ciphertext, encryptionContext);
} catch (Exception e) {
// Try replica regions
for (String region : replicaKeyArns.keySet()) {
try {
return decryptInRegion(region, ciphertext, encryptionContext);
} catch (Exception ex) {
// Continue to next region
}
}
throw new RuntimeException("Decryption failed in all regions", e);
}
}
private byte[] decryptInRegion(String region, byte[] ciphertext,
Map<String, String> encryptionContext) {
KmsClient client = regionalClients.get(region);
String keyArn = region.equals(getPrimaryRegion()) ? primaryKeyArn : replicaKeyArns.get(region);
DecryptRequest request = DecryptRequest.builder()
.ciphertextBlob(SdkBytes.fromByteArray(ciphertext))
.encryptionContext(encryptionContext)
.build();
return client.decrypt(request).plaintext().asByteArray();
}
private String getPrimaryRegion() {
return primaryKeyArn.split(":")[3]; // Extract region from ARN
}
}
2. Key Rotation and Key Policy Management
public class KeyManagementService {
private final KmsClient kmsClient;
public KeyManagementService(KmsClient kmsClient) {
this.kmsClient = kmsClient;
}
public String createKey(String description, String keyUsage, boolean enableRotation) {
CreateKeyRequest createKeyRequest = CreateKeyRequest.builder()
.description(description)
.keyUsage(KeyUsageType.fromValue(keyUsage))
.origin(Origin.AWS_KMS)
.build();
CreateKeyResponse createKeyResponse = kmsClient.createKey(createKeyRequest);
String keyId = createKeyResponse.keyMetadata().keyId();
if (enableRotation) {
enableKeyRotation(keyId);
}
return keyId;
}
public void enableKeyRotation(String keyId) {
EnableKeyRotationRequest rotationRequest = EnableKeyRotationRequest.builder()
.keyId(keyId)
.build();
kmsClient.enableKeyRotation(rotationRequest);
}
public void createAlias(String keyId, String aliasName) {
// Alias must start with "alias/"
String fullAlias = aliasName.startsWith("alias/") ? aliasName : "alias/" + aliasName;
CreateAliasRequest aliasRequest = CreateAliasRequest.builder()
.aliasName(fullAlias)
.targetKeyId(keyId)
.build();
kmsClient.createAlias(aliasRequest);
}
public KeyMetadata getKeyMetadata(String keyId) {
DescribeKeyRequest describeRequest = DescribeKeyRequest.builder()
.keyId(keyId)
.build();
return kmsClient.describeKey(describeRequest).keyMetadata();
}
public void updateKeyPolicy(String keyId, String policy) {
PutKeyPolicyRequest policyRequest = PutKeyPolicyRequest.builder()
.keyId(keyId)
.policyName("default") // Use "default" for the main key policy
.policy(policy)
.build();
kmsClient.putKeyPolicy(policyRequest);
}
public String generateKeyPolicy(String accountId, String keyId, List<String> allowedRoles) {
// Generate a least-privilege key policy
return String.format("""
{
"Version": "2012-10-17",
"Id": "key-policy-%s",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::%s:root"},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow access for Key Administrators",
"Effect": "Allow",
"Principal": {"AWS": %s},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
}
]
}
""", keyId, accountId, formatPrincipalArray(allowedRoles));
}
private String formatPrincipalArray(List<String> roles) {
return roles.stream()
.map(role -> "\"arn:aws:iam::" + role + "\"")
.collect(Collectors.joining(", ", "[", "]"));
}
}
3. Digital Signing and Verification
public class DigitalSignatureService {
private final KmsClient kmsClient;
public DigitalSignatureService(KmsClient kmsClient) {
this.kmsClient = kmsClient;
}
public byte[] signData(String keyId, byte[] data, SigningAlgorithm algorithm) {
SignRequest signRequest = SignRequest.builder()
.keyId(keyId)
.message(SdkBytes.fromByteArray(data))
.messageType(MessageType.RAW)
.signingAlgorithm(algorithm)
.build();
SignResponse signResponse = kmsClient.sign(signRequest);
return signResponse.signature().asByteArray();
}
public boolean verifySignature(String keyId, byte[] data, byte[] signature,
SigningAlgorithm algorithm) {
VerifyRequest verifyRequest = VerifyRequest.builder()
.keyId(keyId)
.message(SdkBytes.fromByteArray(data))
.signature(SdkBytes.fromByteArray(signature))
.signingAlgorithm(algorithm)
.build();
VerifyResponse verifyResponse = kmsClient.verify(verifyRequest);
return verifyResponse.signatureValid();
}
public String signDocument(String keyId, String document, SigningAlgorithm algorithm) {
byte[] signature = signData(keyId, document.getBytes(StandardCharsets.UTF_8), algorithm);
return Base64.getEncoder().encodeToString(signature);
}
public boolean verifyDocument(String keyId, String document, String base64Signature,
SigningAlgorithm algorithm) {
byte[] signature = Base64.getDecoder().decode(base64Signature);
return verifySignature(keyId, document.getBytes(StandardCharsets.UTF_8), signature, algorithm);
}
}
Production-Ready Integration
1. Spring Boot Configuration
@Configuration
@EnableConfigurationProperties(KMSProperties.class)
public class KMSConfiguration {
@Bean
@ConditionalOnMissingBean
public KmsClient kmsClient(KMSProperties properties) {
return KmsClient.builder()
.region(Region.of(properties.getRegion()))
.credentialsProvider(getCredentialsProvider(properties))
.build();
}
@Bean
public EnvelopeEncryptionService envelopeEncryptionService(KmsClient kmsClient,
KMSProperties properties) {
return new EnvelopeEncryptionService(kmsClient, properties.getKeyId());
}
private AwsCredentialsProvider getCredentialsProvider(KMSProperties properties) {
if (StringUtils.hasText(properties.getAccessKey()) &&
StringUtils.hasText(properties.getSecretKey())) {
return StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
}
return DefaultCredentialsProvider.create(); // Use IAM role
}
}
@ConfigurationProperties(prefix = "aws.kms")
@Data
public class KMSProperties {
private String region = "us-east-1";
private String keyId;
private String accessKey;
private String secretKey;
private String keyAlias;
}
application.yml:
aws:
kms:
region: us-east-1
key-id: alias/my-application-key
# access-key: ${AWS_ACCESS_KEY_ID}
# secret-key: ${AWS_SECRET_ACCESS_KEY}
2. Secure Configuration Service
@Service
public class SecureConfigurationService {
private final EnvelopeEncryptionService encryptionService;
private final Map<String, String> encryptedConfigs;
public SecureConfigurationService(EnvelopeEncryptionService encryptionService) {
this.encryptionService = encryptionService;
this.encryptedConfigs = new ConcurrentHashMap<>();
}
public String encryptAndStoreConfig(String configKey, String configValue) {
Map<String, String> context = Map.of(
"configKey", configKey,
"environment", getEnvironment(),
"timestamp", Instant.now().toString()
);
EnvelopeEncryptionService.EncryptedData encrypted =
encryptionService.encryptLargeData(configValue.getBytes(StandardCharsets.UTF_8), context);
String encryptedJson = encrypted.toJson();
encryptedConfigs.put(configKey, encryptedJson);
return encryptedJson;
}
public String getDecryptedConfig(String configKey, String encryptedJson) {
try {
EnvelopeEncryptionService.EncryptedData encrypted =
EnvelopeEncryptionService.EncryptedData.fromJson(encryptedJson);
byte[] decrypted = encryptionService.decryptLargeData(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt configuration: " + configKey, e);
}
}
private String getEnvironment() {
return System.getenv().getOrDefault("ENVIRONMENT", "development");
}
}
Error Handling and Monitoring
@Slf4j
public class KMSErrorHandler {
public static void handleKMSException(KmsException e, String operation, String keyId) {
log.error("KMS operation failed: {} for key: {}", operation, keyId, e);
if (e instanceof DisabledException) {
// Key is disabled - alert immediately
alertSecurityTeam("KMS key disabled: " + keyId);
} else if (e instanceof NotFoundException) {
// Key not found - check key configuration
log.warn("KMS key not found: {}", keyId);
} else if (e instanceof AccessDeniedException) {
// Permission issues - check IAM policies
log.error("Access denied for KMS operation: {}", operation);
} else if (e instanceof KmsInvalidStateException) {
// Key in invalid state
log.error("KMS key in invalid state: {}", keyId);
}
// Metric for monitoring
recordKMSError(operation, e.getClass().getSimpleName());
}
public static boolean isRetryableException(Exception e) {
return e instanceof TooManyRequestsException ||
e instanceof InternalFailureException ||
e instanceof ThrottlingException;
}
public static <T> T executeWithRetry(Supplier<T> operation, String operationName, int maxRetries) {
int attempts = 0;
while (attempts <= maxRetries) {
try {
return operation.get();
} catch (KmsException e) {
attempts++;
if (isRetryableException(e) && attempts <= maxRetries) {
log.warn("Retryable KMS error, attempt {}/{}: {}", attempts, maxRetries, e.getMessage());
exponentialBackoff(attempts);
} else {
handleKMSException(e, operationName, "unknown");
throw e;
}
}
}
throw new RuntimeException("Max retries exceeded for: " + operationName);
}
private static void exponentialBackoff(int attempt) {
try {
long delay = Math.min(1000 * (long) Math.pow(2, attempt), 30000); // Max 30 seconds
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Operation interrupted during backoff", ie);
}
}
}
Best Practices and Security Considerations
1. Key Security
public class KMSSecurityBestPractices {
// Always use encryption context for additional security
public static Map<String, String> createEncryptionContext(String dataType, String userId) {
return Map.of(
"dataType", dataType,
"userId", userId,
"environment", System.getenv().getOrDefault("ENV", "prod"),
"timestamp", Instant.now().toString(),
"service", "my-application"
);
}
// Validate encryption context during decryption
public static void validateEncryptionContext(Map<String, String> expected,
Map<String, String> actual) {
if (!actual.entrySet().containsAll(expected.entrySet())) {
throw new SecurityException("Encryption context validation failed");
}
}
// Secure key material handling
public static void secureWipe(byte[] sensitiveData) {
if (sensitiveData != null) {
Arrays.fill(sensitiveData, (byte) 0);
}
}
// Use secure random for local crypto operations
public static SecureRandom createSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
return new SecureRandom();
}
}
}
Testing Strategy
@ExtendWith(MockitoExtension.class)
class KMSServiceTest {
@Mock
private KmsClient kmsClient;
@InjectMocks
private BasicKMSOperations kmsOperations;
@Test
void testEncryptDecryptRoundTrip() {
// Given
String plaintext = "sensitive data";
Map<String, String> context = Map.of("purpose", "test");
// Mock encrypt response
byte[] ciphertext = "encrypted-data".getBytes();
when(kmsClient.encrypt(any(EncryptRequest.class)))
.thenReturn(EncryptResponse.builder()
.ciphertextBlob(SdkBytes.fromByteArray(ciphertext))
.build());
// Mock decrypt response
when(kmsClient.decrypt(any(DecryptRequest.class)))
.thenReturn(DecryptResponse.builder()
.plaintext(SdkBytes.fromByteArray(plaintext.getBytes()))
.build());
// When
String encrypted = kmsOperations.encryptString(plaintext, context);
String decrypted = kmsOperations.decryptString(encrypted, context);
// Then
assertEquals(plaintext, decrypted);
}
@Test
void testEncryptionContextValidation() {
// Test that encryption context is properly validated
// This is crucial for security
}
}
Conclusion
AWS KMS integration in Java provides:
- Managed Security: AWS handles key storage, rotation, and access control
- Envelope Encryption: Efficient encryption of large datasets
- Multi-Region Support: Disaster recovery and low-latency access
- Fine-Grained Access Control: IAM policies and key policies
- Compliance: Meets various regulatory requirements
Critical Success Factors:
- Proper IAM role and policy configuration
- Consistent use of encryption contexts
- Secure handling of plaintext keys in memory
- Comprehensive error handling and monitoring
- Regular key rotation and policy reviews
By following these patterns, Java applications can leverage AWS KMS for robust, scalable, and secure cryptographic operations while maintaining compliance with security best practices.