Skip to content

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

StandardRequirementAWS Service Alignment
FIPS 140-2 Level 2Validated cryptographic modulesAWS KMS HSMs, ACM
AES-256256-bit symmetric keys for data at restS3 SSE-KMS, EBS, RDS encryption
RSA-2048+Minimum asymmetric key size for signaturesACM certificates, KMS asymmetric keys
SHA-256+Minimum hash strength; forbid MD5/SHA-1ACM TLS 1.2+, CodeSigning
PCI DSS Req 3Protect stored cardholder data with strong cryptographyKMS + SSE-KMS
SOC 2Encryption at rest and in transit, key management controlsKMS + 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

  1. 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.

  2. Least privilege — IAM policies must scope KMS permissions to the minimum necessary: grant kms:Decrypt only to resources that decrypt, kms:GenerateDataKey only to services that write encrypted data, and always restrict by kms:CallerAccount and kms:ViaService condition keys.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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., BCFIPS or the JDK's SunPKCS11 with a FIPS token).