Skip to content
ecdsa.com

ML-KEM-768 encapsulation · Java

How to encapsulate and decapsulate a shared secret with ML-KEM-768 in Java

To encapsulate and decapsulate a shared secret with ML-KEM-768 in Java, use the JDK's KEM API (JDK 24 and later, JEP 496): KEM.getInstance("ML-KEM").newEncapsulator(publicKey).encapsulate() gives the sender a SecretKey and the ciphertext, and newDecapsulator(privateKey).decapsulate(ciphertext) gives the receiver the same key. This recipe also reads OpenSSL 3.5 keys and decapsulates a ciphertext OpenSSL made.

Tested with OpenJDK 25.0.4.1 (Eclipse Temurin 25.0.4.1+1, javax.crypto.KEM (JEP 496)).

MlKem.java — key pair, encapsulation, decapsulation
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
public class MlKem {
public static void main(String[] args) throws Exception {
// Receiver: a key pair (JDK 24+); only the public key is handed out
KeyPair keys = KeyPairGenerator.getInstance("ML-KEM-768").generateKeyPair();
KEM kem = KEM.getInstance("ML-KEM");
// Sender: a fresh shared secret and the ciphertext ("encapsulation") that carries it
KEM.Encapsulated sent = kem.newEncapsulator(keys.getPublic()).encapsulate();
SecretKey senderKey = sent.key();
byte[] ciphertext = sent.encapsulation();
// Receiver: the same secret from the ciphertext
SecretKey receiverKey = kem.newDecapsulator(keys.getPrivate()).decapsulate(ciphertext);
System.out.println("public key " + keys.getPublic().getEncoded().length + " bytes ("
+ keys.getPublic().getFormat() + "), private key " + keys.getPrivate().getEncoded().length
+ " bytes (" + keys.getPrivate().getFormat() + "), ciphertext " + ciphertext.length + " bytes");
System.out.println("shared secret agreed: " + MessageDigest.isEqual(senderKey.getEncoded(), receiverKey.getEncoded()));
}
}

How it works

  1. KeyPairGenerator.getInstance("ML-KEM-768") creates the pair; the public key encodes as a 1206-byte SPKI (X.509), the private key as an 86-byte seed-only PKCS#8.
  2. newEncapsulator(publicKey).encapsulate() returns a KEM.Encapsulated: key() is the 32-byte shared secret, encapsulation() the 1088-byte ciphertext.
  3. newDecapsulator(privateKey).decapsulate(ciphertext) returns the same secret as a SecretKey with algorithm Generic; decapsulate(ciphertext, 0, 32, "AES") returns it as an AES key directly.
  4. KeyFactory.getInstance("ML-KEM") reads OpenSSL's SPKI and seed-only PKCS#8; the run decapsulates OpenSSL's ciphertext to the shared secret dee35810…30f9.

Gotchas

  • A tampered ciphertext throws nothing and yields a different key (implicit rejection); the error only shows when that key fails to decrypt or authenticate data.
  • A ciphertext of the wrong length throws DecapsulateException: Invalid key encapsulation message length.
  • Compare the encoded secrets with MessageDigest.isEqual, not Arrays.equals, if you compare them at all — in a real protocol each side simply uses its key.

Related recipes