Part 10 of 15

Key Encapsulation Mechanism API (JEP 452): Post-Quantum Cryptography in Java

Why Post-Quantum Cryptography Now?

Classical public-key cryptography (RSA, ECDH) relies on mathematical problems that are hard for classical computers — factoring large integers or solving the discrete logarithm problem. A sufficiently powerful quantum computer running Shor’s algorithm could solve these problems efficiently, breaking all existing RSA and ECC-based security.

Quantum computers capable of breaking 2048-bit RSA don’t exist yet. But “harvest now, decrypt later” attacks are real: adversaries intercept and store encrypted traffic today, planning to decrypt it once quantum computers mature. Data encrypted today with classical algorithms may be exposed in 10–20 years.

Java 21’s KEM API is the first step toward post-quantum security in the Java platform — a standard interface for Key Encapsulation Mechanisms, which are the building blocks of post-quantum key exchange.


What Is a Key Encapsulation Mechanism?

A KEM establishes a shared symmetric key between two parties using public-key cryptography, without transmitting the key itself:

sequenceDiagram
    participant Sender
    participant Receiver

    Note over Receiver: Has key pair (publicKey, privateKey)
    Receiver->>Sender: publicKey (can be shared openly)

    Note over Sender: Runs KEM encapsulate(publicKey)
    Note over Sender: Produces: sharedSecret + encapsulatedKey
    Sender->>Receiver: encapsulatedKey (sent over network)

    Note over Receiver: Runs KEM decapsulate(privateKey, encapsulatedKey)
    Note over Receiver: Recovers: sharedSecret

    Note over Sender,Receiver: Both have sharedSecret\nNeither transmitted it directly\nCan now use sharedSecret for AES encryption

The sharedSecret is used as a symmetric key (e.g., for AES-256-GCM) — this is called hybrid encryption: asymmetric KEM to establish the key, symmetric cipher for the data.


The KEM API — Core Classes

// javax.crypto.KEM       — factory and entry point
// javax.crypto.KEM.Encapsulated — result of encapsulation
// javax.crypto.KEM.Encapsulator — performs encapsulation
// javax.crypto.KEM.Decapsulator — performs decapsulation

Full Hybrid Encryption Example

Receiver Side — Generate Key Pair

import javax.crypto.*;
import java.security.*;

// Generate a key pair (receiver does this once; publicKey is distributed)
KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519");
KeyPair keyPair = kpg.generateKeyPair();
PublicKey  publicKey  = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();

Sender Side — Encapsulate

// Sender has the receiver's publicKey
KEM kem = KEM.getInstance("DHKEM(X25519, HKDF-SHA256)");
KEM.Encapsulator encapsulator = kem.newEncapsulator(publicKey);

// Encapsulate: generates sharedSecret + encapsulatedKey in one step
KEM.Encapsulated encapsulated = encapsulator.encapsulate();

SecretKey sharedSecret    = encapsulated.key();            // use as AES key
byte[]    encapsulatedKey = encapsulated.encapsulation();  // send to receiver
byte[]    params          = encapsulated.params();         // optional KDF params

Sender — Encrypt Data with the Shared Secret

// Use the shared secret to encrypt data with AES-256-GCM
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12];  // 96-bit IV for GCM
new SecureRandom().nextBytes(iv);

cipher.init(Cipher.ENCRYPT_MODE, sharedSecret, new GCMParameterSpec(128, iv));
byte[] plaintext  = "Order confirmed: #42".getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = cipher.doFinal(plaintext);

// Send to receiver: encapsulatedKey + iv + ciphertext

Receiver Side — Decapsulate and Decrypt

// Receiver has their privateKey and receives encapsulatedKey + iv + ciphertext

KEM kem = KEM.getInstance("DHKEM(X25519, HKDF-SHA256)");
KEM.Decapsulator decapsulator = kem.newDecapsulator(privateKey);

// Recover the same sharedSecret
SecretKey sharedSecret = decapsulator.decapsulate(
    encapsulatedKey, 0, encapsulatedKey.length,
    "AES"           // desired algorithm for SecretKey
);

// Decrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, sharedSecret, new GCMParameterSpec(128, iv));
byte[] plaintext = cipher.doFinal(ciphertext);
System.out.println(new String(plaintext));  // "Order confirmed: #42"

KEM Algorithms in Java 21

Java 21 ships with DHKEM algorithms from RFC 9180 (HPKE — Hybrid Public Key Encryption):

AlgorithmKey typeKDFNotes
DHKEM(X25519, HKDF-SHA256)X25519HKDF-SHA256Recommended — fast, modern
DHKEM(X448, HKDF-SHA512)X448HKDF-SHA512Larger keys, higher security margin
DHKEM(P-256, HKDF-SHA256)EC P-256HKDF-SHA256NIST curve, FIPS-compatible
DHKEM(P-384, HKDF-SHA384)EC P-384HKDF-SHA384Higher security NIST
DHKEM(P-521, HKDF-SHA512)EC P-521HKDF-SHA512Highest security NIST

All of these are classical algorithms — they provide a clean, standardized KEM API. Post-quantum algorithms (ML-KEM, formerly CRYSTALS-Kyber) will be added as JDK providers evolve.


The JCA Provider Model

KEM follows the same provider architecture as other Java cryptography:

// Use the default provider (SunEC)
KEM kem = KEM.getInstance("DHKEM(X25519, HKDF-SHA256)");

// Specify a provider explicitly
KEM kem = KEM.getInstance("DHKEM(X25519, HKDF-SHA256)", "SunEC");

// Use a third-party post-quantum provider (e.g., Bouncy Castle)
Security.addProvider(new BouncyCastleProvider());
KEM kem = KEM.getInstance("KYBER512", "BC");  // ML-KEM via Bouncy Castle

This means once post-quantum algorithms like ML-KEM are standardized (NIST finalized in 2024), JDK providers will add them without changing your application code — just change the algorithm string.


Hybrid Encryption: Combining Classical and Post-Quantum

The recommended production approach is hybrid KEM — run both a classical KEM and a post-quantum KEM, XOR their shared secrets together:

// Hybrid: X25519 (classical) + ML-KEM-768 (post-quantum, via BC provider)
// If either is broken, the other still protects the secret

byte[] classicalSecret = classicalKemEncapsulate(publicKeyX25519);
byte[] pqSecret        = postQuantumKemEncapsulate(publicKeyKyber);

// Combine secrets securely (XOR is fine when both are uniformly random)
byte[] combinedSecret = new byte[classicalSecret.length];
for (int i = 0; i < classicalSecret.length; i++) {
    combinedSecret[i] = (byte) (classicalSecret[i] ^ pqSecret[i % pqSecret.length]);
}

// Use combinedSecret as AES key — protected against both classical and quantum attacks

KEM vs Traditional Key Exchange

MechanismJava APIQuantum-safe?Notes
RSA key transportCipher.getInstance("RSA")NoBroken by Shor’s algorithm
ECDHKeyAgreement.getInstance("ECDH")NoBroken by Shor’s algorithm
DHKeyAgreement.getInstance("DH")NoBroken by Shor’s algorithm
DHKEM (Java 21)KEM.getInstance(...)No (classical)Standardized, provider-extensible
ML-KEM (Kyber)KEM.getInstance("KYBER", provider)YesVia Bouncy Castle or future JDK

Encapsulated Secret Key Specification

Control the exact type and size of the derived key:

// Request a specific key type and size
KEM.Encapsulated encapsulated = encapsulator.encapsulate(
    0,       // offset into key material
    32,      // length of key in bytes (256 bits)
    "AES"    // algorithm for SecretKey
);

SecretKey aes256Key = encapsulated.key();
// aes256Key.getAlgorithm() == "AES"
// aes256Key.getEncoded().length == 32

Security Considerations

Always use authenticated encryption — combine the shared secret with AES-GCM, not AES-CBC. AES-GCM detects tampering; AES-CBC does not.

Bind context to the KEM — include context information (sender ID, purpose, timestamp) in the KDF to prevent key material from being reused across different contexts:

// Include application context in key derivation
KEM.Encapsulator encapsulator = kem.newEncapsulator(
    publicKey,
    null,  // params
    "order-service:v1:encrypt".getBytes()  // application context
);

Rotate key pairs — KEM key pairs should be rotated regularly. A compromised private key allows decapsulating all past encapsulated secrets.


Key Takeaways

  • KEM establishes a shared symmetric key using public-key cryptography — neither party transmits the key
  • Java 21’s KEM API follows the JCA provider model — KEM.getInstance(algorithm), then newEncapsulator(publicKey) / newDecapsulator(privateKey)
  • Java 21 ships DHKEM algorithms (RFC 9180/HPKE) based on X25519 and NIST curves — classical algorithms with a future-proof API
  • Post-quantum algorithms (ML-KEM/Kyber) will be added via JDK providers — your code using KEM.getInstance(...) won’t need to change
  • For maximum security today, use hybrid KEM: classical X25519 + post-quantum ML-KEM via Bouncy Castle
  • Always pair KEM with AES-GCM (authenticated encryption) for the actual data encryption

Next: Unnamed Patterns and Variables (JEP 443) — use _ to express intent clearly when pattern components or variables are intentionally unused.