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.