Appearance
Cryptography and Key Management
Introduction
Cryptography is the foundation of secure systems, providing confidentiality, integrity, authentication, and non-repudiation for data at rest and in transit. Modern applications rely on symmetric encryption for bulk data, asymmetric encryption for key exchange and signatures, and managed key services like AWS KMS to eliminate the risks of manual key handling. This guide covers Java cryptography APIs, AWS managed services, and the patterns that tie them together into a compliant, production-ready security posture.
Core Concepts
Symmetric vs Asymmetric Encryption
Symmetric Encryption (AES-256)
AES-256 (Advanced Encryption Standard with a 256-bit key) is the industry standard for bulk data encryption. Two modes dominate modern usage:
- CBC (Cipher Block Chaining) — requires separate HMAC for integrity; avoid for new code.
- GCM (Galois/Counter Mode) — authenticated encryption, provides both confidentiality and integrity in one pass. Always prefer GCM.
AES-256 Encryption and Decryption Flow
Java: AES-256 GCM Encryption Class
java
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.Base64;
public class AES256Encryption {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128; // bits
private static final int IV_LENGTH = 12; // bytes, recommended for GCM
/** Generate a 256-bit AES secret key. */
public static SecretKey generateKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(256, new SecureRandom());
return keyGen.generateKey();
}
/**
* Encrypt plaintext using AES-256-GCM.
* Returns a byte array of [IV (12 bytes) | ciphertext + auth tag].
*/
public static byte[] encrypt(String data, SecretKey key) throws Exception {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec paramSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
byte[] cipherText = cipher.doFinal(data.getBytes("UTF-8"));
// Prepend IV to ciphertext for storage/transmission
byte[] result = new byte[IV_LENGTH + cipherText.length];
System.arraycopy(iv, 0, result, 0, IV_LENGTH);
System.arraycopy(cipherText, 0, result, IV_LENGTH, cipherText.length);
return result;
}
/**
* Decrypt AES-256-GCM ciphertext.
* Expects the format produced by encrypt(): [IV (12 bytes) | ciphertext + auth tag].
*/
public static String decrypt(byte[] encryptedData, SecretKey key) throws Exception {
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(encryptedData, 0, iv, 0, IV_LENGTH);
byte[] cipherText = new byte[encryptedData.length - IV_LENGTH];
System.arraycopy(encryptedData, IV_LENGTH, cipherText, 0, cipherText.length);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec paramSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
return new String(cipher.doFinal(cipherText), "UTF-8");
}
public static void main(String[] args) throws Exception {
SecretKey key = generateKey();
String original = "Sensitive payload: account-number=1234567890";
byte[] encrypted = encrypt(original, key);
System.out.println("Encrypted (Base64): " + Base64.getEncoder().encodeToString(encrypted));
String decrypted = decrypt(encrypted, key);
System.out.println("Decrypted: " + decrypted);
System.out.println("Match: " + original.equals(decrypted));
}
}Asymmetric Encryption (RSA, ECC)
Asymmetric cryptography uses mathematically linked key pairs. The public key is freely distributed; only the corresponding private key can decrypt or sign. RSA-2048 is the minimum acceptable key size; RSA-4096 or ECC P-256/P-384 are preferred for long-lived keys.
RSA Key Pair Usage
Java: RSA Asymmetric Encryption Class
java
import javax.crypto.Cipher;
import java.security.*;
import java.util.Base64;
public class RSAEncryption {
private static final String ALGORITHM = "RSA";
private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
private static final int KEY_SIZE = 2048;
/** Generate a 2048-bit RSA key pair. */
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM);
generator.initialize(KEY_SIZE, new SecureRandom());
return generator.generateKeyPair();
}
/** Encrypt data with the RSA public key using OAEP padding. */
public static byte[] encrypt(String data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data.getBytes("UTF-8"));
}
/** Decrypt RSA-encrypted data with the private key. */
public static String decrypt(byte[] cipherData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(cipher.doFinal(cipherData), "UTF-8");
}
public static void main(String[] args) throws Exception {
KeyPair keyPair = generateKeyPair();
System.out.println("Public key algorithm : " + keyPair.getPublic().getAlgorithm());
System.out.println("Private key format : " + keyPair.getPrivate().getFormat());
String message = "Top-secret API token: sk-prod-abc123xyz";
byte[] encrypted = encrypt(message, keyPair.getPublic());
System.out.println("Encrypted (Base64): " + Base64.getEncoder().encodeToString(encrypted));
String decrypted = decrypt(encrypted, keyPair.getPrivate());
System.out.println("Decrypted: " + decrypted);
System.out.println("Match: " + message.equals(decrypted));
}
}Hashing (SHA-256, SHA-512)
Cryptographic hashes produce a fixed-size digest of arbitrary input. They are one-way: you cannot reverse a hash to recover the original data. SHA-256 produces a 256-bit digest; SHA-512 produces 512 bits. HMAC (Hash-based Message Authentication Code) adds a secret key to prove both integrity and authenticity.
SHA-256 Hashing and Integrity Verification
Java: SHA-256 Hashing and HMAC-SHA256
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
public class HashingUtility {
/** Compute SHA-256 hex digest of the input string. */
public static String hash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hashBytes);
}
/** Verify that hashing input produces the expectedHash. */
public static boolean verify(String input, String expectedHash) throws NoSuchAlgorithmException {
String actual = hash(input);
return MessageDigest.isEqual(actual.getBytes(), expectedHash.getBytes());
}
/**
* Compute HMAC-SHA256 for message authentication.
* The secret key must be shared between sender and receiver out-of-band.
*/
public static String hmacSHA256(String data, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
byte[] hmacBytes = mac.doFinal(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hmacBytes);
}
public static void main(String[] args) throws Exception {
String payload = "user-id=42&action=transfer&amount=500";
// SHA-256 integrity check
String digest = hash(payload);
System.out.println("SHA-256: " + digest);
System.out.println("Verify OK : " + verify(payload, digest));
System.out.println("Verify BAD : " + verify(payload + "tampered", digest));
// HMAC-SHA256 message authentication
String sharedSecret = "super-secret-signing-key";
String hmac = hmacSHA256(payload, sharedSecret);
System.out.println("HMAC-SHA256: " + hmac);
}
}Digital Signatures and Certificates
Digital signatures bind a message to a private key, allowing any holder of the corresponding public key to verify authenticity and non-repudiation. X.509 certificates package a public key with identity information and a CA signature, forming the chain of trust used in TLS.
Digital Signature Creation and Verification
Java: Digital Signature Class
java
import java.security.*;
import java.util.Base64;
public class DigitalSignatureExample {
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int KEY_SIZE = 2048;
/** Sign arbitrary byte data with an RSA private key. */
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey, new SecureRandom());
signer.update(data);
return signer.sign();
}
/** Verify a signature against data using the corresponding RSA public key. */
public static boolean verify(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {
Signature verifier = Signature.getInstance(SIGNATURE_ALGORITHM);
verifier.initVerify(publicKey);
verifier.update(data);
return verifier.verify(signature);
}
public static void main(String[] args) throws Exception {
// Generate a key pair (in production, load from a key store)
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(KEY_SIZE, new SecureRandom());
KeyPair keyPair = generator.generateKeyPair();
byte[] document = "Contract: Party A agrees to pay Party B $10,000".getBytes("UTF-8");
byte[] signature = sign(document, keyPair.getPrivate());
System.out.println("Signature (Base64): " + Base64.getEncoder().encodeToString(signature));
boolean valid = verify(document, signature, keyPair.getPublic());
System.out.println("Signature valid: " + valid);
// Tamper detection
byte[] tampered = "Contract: Party A agrees to pay Party B $99,999".getBytes("UTF-8");
boolean invalidAfterTamper = verify(tampered, signature, keyPair.getPublic());
System.out.println("Valid after tampering: " + invalidAfterTamper); // false
}
}AWS KMS (Key Management Service)
AWS KMS manages Customer Master Keys (CMKs) in FIPS 140-2 validated hardware. Applications never handle the raw CMK material; instead they request KMS to encrypt/decrypt data keys. This pattern — called envelope encryption — means only small data keys traverse the network.
AWS KMS Architecture and Envelope Encryption
Key Rotation Lifecycle
Java: AWS KMS Operations and Envelope Encryption
java
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.*;
import java.util.Base64;
public class AWSKMSExample {
/** Encrypt a plaintext string directly with a KMS CMK (suitable for small data). */
public static SdkBytes encryptWithKMS(KmsClient kmsClient, String keyId, String plaintext) {
EncryptRequest request = EncryptRequest.builder()
.keyId(keyId)
.plaintext(SdkBytes.fromUtf8String(plaintext))
.encryptionAlgorithm(EncryptionAlgorithmSpec.SYMMETRIC_DEFAULT)
.build();
EncryptResponse response = kmsClient.encrypt(request);
System.out.println("Encrypted with KMS CMK: " + keyId);
return response.ciphertextBlob();
}
/** Decrypt a KMS-encrypted blob back to plaintext. */
public static String decryptWithKMS(KmsClient kmsClient, String keyId, SdkBytes ciphertext) {
DecryptRequest request = DecryptRequest.builder()
.keyId(keyId)
.ciphertextBlob(ciphertext)
.encryptionAlgorithm(EncryptionAlgorithmSpec.SYMMETRIC_DEFAULT)
.build();
DecryptResponse response = kmsClient.decrypt(request);
return response.plaintext().asUtf8String();
}
/**
* Generate a data key for envelope encryption.
* Returns the response containing both plaintext and encrypted data key.
* The plaintext key must be zeroed from memory after use.
*/
public static GenerateDataKeyResponse generateDataKey(KmsClient kmsClient, String keyId) {
GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
.keyId(keyId)
.keySpec(DataKeySpec.AES_256)
.build();
return kmsClient.generateDataKey(request);
}
/** Enable automatic annual key rotation for a CMK. */
public static void rotateKey(KmsClient kmsClient, String keyId) {
EnableKeyRotationRequest request = EnableKeyRotationRequest.builder()
.keyId(keyId)
.build();
kmsClient.enableKeyRotation(request);
System.out.println("Automatic key rotation enabled for: " + keyId);
}
public static void main(String[] args) {
String keyId = "arn:aws:kms:us-east-1:123456789012:key/mrk-abc12345";
try (KmsClient kmsClient = KmsClient.create()) {
// --- Envelope encryption pattern ---
// 1. Generate a data key from KMS
GenerateDataKeyResponse dataKeyResponse = generateDataKey(kmsClient, keyId);
SdkBytes plaintextDataKey = dataKeyResponse.plaintext(); // use in memory
SdkBytes encryptedDataKey = dataKeyResponse.ciphertextBlob(); // store alongside data
System.out.println("Plaintext data key (hex, first 8): "
+ Base64.getEncoder().encodeToString(plaintextDataKey.asByteArray()).substring(0, 12));
System.out.println("Encrypted data key (Base64, first 8): "
+ Base64.getEncoder().encodeToString(encryptedDataKey.asByteArray()).substring(0, 12));
// 2. Use plaintextDataKey with AES256Encryption.encrypt() to encrypt payload
// 3. Store encryptedDataKey + encrypted payload together
// 4. To decrypt: call decryptWithKMS(encryptedDataKey) → recover data key → decrypt payload
// Enable rotation
rotateKey(kmsClient, keyId);
}
}
}AWS Secrets Manager
Secrets Manager stores, rotates, and audits secrets such as database credentials, API keys, and TLS certificates. Rotation is automated via Lambda functions that update both the secret value and the target system in a coordinated atomic swap.
Secrets Manager Rotation Flow
Java: AWS Secrets Manager Operations
java
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.*;
public class AWSSecretsManagerExample {
/** Create a new plaintext secret. */
public static void createSecret(SecretsManagerClient client,
String secretName, String secretValue) {
CreateSecretRequest request = CreateSecretRequest.builder()
.name(secretName)
.secretString(secretValue)
.description("Managed by application bootstrap")
.build();
CreateSecretResponse response = client.createSecret(request);
System.out.println("Created secret ARN: " + response.arn());
}
/** Retrieve the current value of a secret. */
public static String getSecret(SecretsManagerClient client, String secretName) {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId(secretName)
.build();
GetSecretValueResponse response = client.getSecretValue(request);
return response.secretString(); // or secretBinary() for binary secrets
}
/** Update an existing secret's value. */
public static void updateSecret(SecretsManagerClient client,
String secretName, String newValue) {
PutSecretValueRequest request = PutSecretValueRequest.builder()
.secretId(secretName)
.secretString(newValue)
.build();
client.putSecretValue(request);
System.out.println("Updated secret: " + secretName);
}
/** Schedule a secret for deletion (default 30-day recovery window). */
public static void deleteSecret(SecretsManagerClient client, String secretName) {
DeleteSecretRequest request = DeleteSecretRequest.builder()
.secretId(secretName)
.recoveryWindowInDays(30L)
.build();
client.deleteSecret(request);
System.out.println("Scheduled deletion for: " + secretName);
}
/** Enable automatic rotation using a Lambda function ARN. */
public static void rotateSecret(SecretsManagerClient client,
String secretName, String lambdaArn) {
RotateSecretRequest request = RotateSecretRequest.builder()
.secretId(secretName)
.rotationLambdaARN(lambdaArn)
.rotationRules(RotationRulesType.builder()
.automaticallyAfterDays(30L)
.build())
.build();
client.rotateSecret(request);
System.out.println("Rotation enabled every 30 days for: " + secretName);
}
public static void main(String[] args) {
try (SecretsManagerClient client = SecretsManagerClient.create()) {
String secretName = "prod/myapp/db-credentials";
String lambdaArn = "arn:aws:lambda:us-east-1:123456789012:function:MyRotationLambda";
// Typical application bootstrap: just read the secret
String secretJson = getSecret(client, secretName);
System.out.println("Retrieved secret (JSON): " + secretJson);
// Enable 30-day rotation
rotateSecret(client, secretName, lambdaArn);
}
}
}S3 Server-Side Encryption
S3 supports three SSE modes: SSE-S3 (AWS-managed AES-256), SSE-KMS (customer-controlled CMK), and SSE-C (customer-provided key). SSE-KMS is preferred because it integrates with CloudTrail for auditability and supports key access policies.
Java: S3 SSE Upload and Metadata Retrieval
java
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
public class S3ServerSideEncryption {
/** Upload an object encrypted with SSE-KMS (customer managed CMK). */
public static void uploadWithSSEKMS(S3Client s3, String bucket, String key,
byte[] data, String kmsKeyId) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.serverSideEncryption(ServerSideEncryption.AWS_KMS)
.ssekmsKeyId(kmsKeyId)
.build();
s3.putObject(request, RequestBody.fromBytes(data));
System.out.println("Uploaded with SSE-KMS: s3://" + bucket + "/" + key);
}
/** Upload an object encrypted with SSE-S3 (AWS-managed AES-256). */
public static void uploadWithSSES3(S3Client s3, String bucket, String key, byte[] data) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.serverSideEncryption(ServerSideEncryption.AES256)
.build();
s3.putObject(request, RequestBody.fromBytes(data));
System.out.println("Uploaded with SSE-S3 (AES-256): s3://" + bucket + "/" + key);
}
/** Retrieve object metadata and validate encryption mode. */
public static void validateEncryptionMetadata(S3Client s3, String bucket, String key) {
HeadObjectRequest request = HeadObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
HeadObjectResponse response = s3.headObject(request);
ServerSideEncryption sse = response.serverSideEncryption();
System.out.println("SSE algorithm : " + sse);
System.out.println("SSE-KMS key ID: " + response.ssekmsKeyId());
if (!ServerSideEncryption.AWS_KMS.equals(sse)) {
throw new SecurityException("Object is not encrypted with KMS! Actual: " + sse);
}
System.out.println("Encryption validation passed.");
}
}End-to-End Secure Data Pipeline
Compliance and Standards
| Standard | Requirement | AWS Service Alignment |
|---|---|---|
| FIPS 140-2 Level 2 | Validated cryptographic modules | AWS KMS HSMs, ACM |
| AES-256 | 256-bit symmetric keys for data at rest | S3 SSE-KMS, EBS, RDS encryption |
| RSA-2048+ | Minimum asymmetric key size for signatures | ACM certificates, KMS asymmetric keys |
| SHA-256+ | Minimum hash strength; forbid MD5/SHA-1 | ACM TLS 1.2+, CodeSigning |
| PCI DSS Req 3 | Protect stored cardholder data with strong cryptography | KMS + SSE-KMS |
| SOC 2 | Encryption at rest and in transit, key management controls | KMS + CloudTrail audit |
AWS KMS is FIPS 140-2 validated under CMVP certificate numbers available in the AWS FIPS documentation. Selecting kms.<region>.amazonaws.com endpoints in the FIPS partition ensures all cryptographic operations use validated modules.
Best Practices
Key rotation — Enable automatic annual rotation on all KMS CMKs (
EnableKeyRotation). For Secrets Manager, configure rotation every 30–90 days with a tested Lambda rotator.Least privilege — IAM policies must scope KMS permissions to the minimum necessary: grant
kms:Decryptonly to resources that decrypt,kms:GenerateDataKeyonly to services that write encrypted data, and always restrict bykms:CallerAccountandkms:ViaServicecondition keys.Authenticated encryption — Use AES-GCM (not AES-CBC) so that tampering of ciphertext is detected during decryption. AES-CBC requires a separate HMAC, which introduces implementation risk.
Never store plaintext keys — Application configuration must never contain raw AES or RSA private keys. Store all key material in KMS or a hardware security module (HSM). Load credentials from Secrets Manager at runtime.
Envelope encryption — Encrypt data with a locally generated AES data key; encrypt that data key with a KMS CMK. This limits the size and frequency of data sent to KMS, reduces cost, and keeps the CMK in the HSM boundary.
Strong algorithms only — Use AES-256, RSA-2048 or higher, SHA-256 or higher. Explicitly prohibit MD5, SHA-1, DES, 3DES, and RC4 in code review checklists and static analysis rules.
Secrets rotation — Automate rotation with Secrets Manager and test the rotation Lambda in staging before enabling it in production. Ensure applications read secrets at request time, not at startup, so rotated values take effect without restarts.
FIPS compliance — Configure the AWS SDK to use FIPS endpoints (
useFipsEndpoint(true)) when operating in regulated environments. Verify that the JVM's security provider list includes only FIPS-approved providers (e.g.,BCFIPSor the JDK'sSunPKCS11with a FIPS token).
Related Concepts
- OAuth 2.0 and Token Security — how JWTs are signed with RSA/ECDSA and verified using public keys
- AWS SSO and IAM Identity Center — federation patterns that rely on SAML assertions signed with X.509 certificates