Java 25 Key Derivation Function (KDF) API Tutorial

Introduction Cryptographic protocols rarely use a shared secret directly as an encryption key. A secret produced by a key exchange or a key encapsulation mechanismClick to read tutorial: Java 24 Quantum-Resistant Crypto: ML-KEM and ML-DSA may not be uniformly random, may be the wrong length, and is usually needed for several purposes - one key for encryption, another for authentication, separate keys for each direction of a connection. A key derivation function (KDF) turns one secret into as many strong, independent keys as needed. TLS 1.3, Signal, HPKE and WireGuard all rely on one, HKDF. Until now, Java had no standard API for this, so code used third-party libraries or wrote HKDF by hand with Mac. After a preview in Java 24, Java 25Click to read tutorial: Overview of New Features in Java 25 (JEP 510) finalized the Key Derivation Function API, javax.crypto.KDF. This tutorial explains key derivation and HKDF, and shows how to derive keys with the new API with runnable examples, checked against the official test vector. HKDF: extract and expand HKDF (HMAC-based Key Derivation Function, RFC 5869) works in two steps -
  1. Extract - takes the input keying material (IKM), such as a shared secret, and an optional salt, and condenses them into a fixed-length, uniformly random pseudorandom key (PRK).
  2. Expand - takes the PRK and an info string describing the purpose, such as "encryption key", and produces output keying material of the requested length. Different info strings give independent keys from the same PRK.
The KDF API models these steps with HKDFParameterSpec, and is available with SHA-256 and SHA-512 as HKDF-SHA256 and HKDF-SHA512.
Checking the API against the RFC 5869 test vector RFC 5869 publishes test vectors for HKDF. The program below runs the first one through the new API and compares the result with the expected output -
HKDF-SHA256 with the RFC 5869 test case 1
//HkdfTestVector.java
import java.security.spec.AlgorithmParameterSpec;
import java.util.HexFormat;
import javax.crypto.KDF;
import javax.crypto.spec.HKDFParameterSpec;

public class HkdfTestVector {
  public static void main(String[] args) throws Exception {
    HexFormat hex = HexFormat.of();
    byte[] ikm = hex.parseHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
    byte[] salt = hex.parseHex("000102030405060708090a0b0c");
    byte[] info = hex.parseHex("f0f1f2f3f4f5f6f7f8f9");
    String expected = "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865";

    KDF hkdf = KDF.getInstance("HKDF-SHA256");
    AlgorithmParameterSpec params = HKDFParameterSpec.ofExtract()
        .addIKM(ikm)
        .addSalt(salt)
        .thenExpand(info, 42);                    // extract, then expand to 42 bytes
    byte[] okm = hkdf.deriveData(params);

    System.out.println("OKM      : " + hex.formatHex(okm));
    System.out.println("Matches RFC 5869 test case 1: " + hex.formatHex(okm).equals(expected));
  }
}
 OUTPUT of the above code
OKM      : 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
Matches RFC 5869 test case 1: true
deriveData() returns raw bytes - useful for checking test vectors or for derived values which are not keys. The output matches the RFC exactly. Deriving several keys from one shared secret In practice, the input is a secret from a key exchange, and the outputs are SecretKeys for specific algorithms. The example establishes a shared secret with ML-KEM, then derives separate AES keys for each direction of a connection with deriveKey() -
From a key exchange to independent AES keys
//DeriveKeysDemo.java
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.Arrays;
import javax.crypto.KDF;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;

public class DeriveKeysDemo {
  static SecretKey derive(SecretKey shared, byte[] salt, String purpose) throws Exception {
    KDF hkdf = KDF.getInstance("HKDF-SHA256");
    return hkdf.deriveKey("AES", HKDFParameterSpec.ofExtract()
        .addIKM(shared)
        .addSalt(salt)
        .thenExpand(purpose.getBytes(StandardCharsets.UTF_8), 32));    // a 256-bit AES key
  }

  public static void main(String[] args) throws Exception {
    // A shared secret from a post-quantum key encapsulation (Java 24)
    KeyPair server = KeyPairGenerator.getInstance("ML-KEM").generateKeyPair();
    KEM kem = KEM.getInstance("ML-KEM");
    KEM.Encapsulated enc = kem.newEncapsulator(server.getPublic()).encapsulate();
    SecretKey clientShared = enc.key();
    SecretKey serverShared = kem.newDecapsulator(server.getPrivate()).decapsulate(enc.encapsulation());

    byte[] salt = "session-42".getBytes(StandardCharsets.UTF_8);
    SecretKey clientToServer = derive(clientShared, salt, "client to server");
    SecretKey serverToClient = derive(clientShared, salt, "server to client");
    SecretKey serverSideCopy = derive(serverShared, salt, "client to server");

    System.out.println("Key algorithm / size     : " + clientToServer.getAlgorithm() + " / " + clientToServer.getEncoded().length * 8 + " bits");
    System.out.println("Directions use different keys: " + !Arrays.equals(clientToServer.getEncoded(), serverToClient.getEncoded()));
    System.out.println("Both sides derive the same key: " + Arrays.equals(clientToServer.getEncoded(), serverSideCopy.getEncoded()));
  }
}
 OUTPUT of the above code
Key algorithm / size     : AES / 256 bits
Directions use different keys: true
Both sides derive the same key: true
Explanation of the code
  • Both sides of the ML-KEM exchange hold the same shared secret. Each derives keys from it with HKDF, so they end up with the same keys without ever sending them.
  • The two directions use different info strings, "client to server" and "server to client", so their keys are independent - learning one key reveals nothing about the other.
  • deriveKey("AES", ...) returns a SecretKey ready for Cipher, and addIKM() accepts a SecretKey directly, so the secret's bytes never need to be handled by application code.
Where the KDF API fits Summary In this tutorial we saw how a key derivation function turns one shared secret into independent, purpose-specific keys, and how HKDF does it in an extract and an expand step. With the Key Derivation Function API, final in Java 25, we reproduced the RFC 5869 test vector exactly with deriveData(), and derived separate AES keys for each direction of a connection from an ML-KEM shared secret with deriveKey(). Lastly, we saw where the API fits, and why it is not for passwords. To see all the changes in this release, read the overview of new features in Java 25Click to read tutorial: Overview of New Features in Java 25.