Part 12 of 12

Java 25 Security: Key Derivation Function API & PEM Encodings

Overview

Java 25 ships two important security additions:

  1. JEP 510 — Key Derivation Function (KDF) API — Final. A standard API for HKDF, PBKDF2, and other KDFs.
  2. JEP 470 — PEM Encodings of Cryptographic Objects — Preview. Read and write .pem files without third-party libraries.

These fill two long-standing gaps: Java had the underlying crypto but no clean standard API for key derivation and PEM I/O.


Part 1: Key Derivation Function API (JEP 510)

What Is Key Derivation?

A Key Derivation Function (KDF) takes a secret (a password, a shared secret from key exchange) and derives one or more cryptographic keys from it. You need this in:

  • Password-based encryption: turn a user’s password into an AES key (PBKDF2)
  • TLS and key exchange: derive session keys from a shared secret (HKDF)
  • Token generation: derive API tokens from a master secret
  • Key wrapping: derive a wrapping key from a master key

Before Java 25, there was no standard KDF API. You used SecretKeyFactory for PBKDF2 with awkward parameter classes, and HKDF was only available via Bouncy Castle or manual HMAC chaining.


The New API

import javax.crypto.KDF;
import javax.crypto.spec.*;

The KDF class follows the familiar Java crypto pattern:

KDF kdf = KDF.getInstance("algorithm");
SecretKey derived = kdf.deriveKey("AES", params);
byte[] derivedBytes = kdf.deriveData(params);

HKDF: Deriving Keys from a Shared Secret

HKDF (HMAC-based Key Derivation Function, RFC 5869) is the standard for TLS 1.3, Signal Protocol, and modern key exchange.

import module java.base;
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;

public class HKDFExample {

    public static void main(String[] args) throws Exception {
        // Simulate a shared secret from ECDH key exchange
        byte[] sharedSecret = hexToBytes("a3b5c7d9e1f2a4b6c8d0e2f4a6b8c0d2");

        // Optional salt (randomize with secure random in production)
        byte[] salt = hexToBytes("0102030405060708090a0b0c0d0e0f10");

        // Context info — identifies the purpose of this derived key
        byte[] info = "myapp-v1-encryption-key".getBytes();

        // ── Extract step: pseudo-random key from shared secret + salt ─────────
        KDF hkdf = KDF.getInstance("HKDF-SHA256");

        HKDFParameterSpec extractParams = HKDFParameterSpec.ofExtract()
                .addIKM(sharedSecret)   // Input Key Material
                .addSalt(salt)
                .extractOnly();

        SecretKey prk = hkdf.deriveKey("HKDF-PRK", extractParams);

        // ── Expand step: derive a 32-byte AES-256 key from the PRK ────────────
        HKDFParameterSpec expandParams = HKDFParameterSpec.expandOnly(prk, info, 32);
        SecretKey aesKey = hkdf.deriveKey("AES", expandParams);

        System.out.println("Derived AES-256 key: " + bytesToHex(aesKey.getEncoded()));
        System.out.println("Key algorithm: " + aesKey.getAlgorithm());
        System.out.println("Key size: " + (aesKey.getEncoded().length * 8) + " bits");
    }

    // ── Combined Extract + Expand in one call ─────────────────────────────────
    public static SecretKey deriveSessionKey(byte[] sharedSecret, byte[] salt,
                                              String purpose) throws Exception {
        KDF hkdf = KDF.getInstance("HKDF-SHA256");

        HKDFParameterSpec params = HKDFParameterSpec.ofExtract()
                .addIKM(sharedSecret)
                .addSalt(salt)
                .thenExpand(purpose.getBytes(), 32);  // extract then expand

        return hkdf.deriveKey("AES", params);
    }

    private static byte[] hexToBytes(String hex) {
        byte[] bytes = new byte[hex.length() / 2];
        for (int i = 0; i < bytes.length; i++)
            bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
        return bytes;
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}

PBKDF2: Password-Based Key Derivation

PBKDF2 (Password-Based Key Derivation Function 2, RFC 2898) is the standard for deriving keys from passwords — used in password managers, encrypted backups, and secure storage.

import module java.base;
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.PBEKeySpec;
import java.security.spec.PBKDF2KeySpec;

public class PBKDF2Example {

    public static SecretKey deriveKeyFromPassword(
            char[] password, byte[] salt, int iterationCount, int keyLengthBits)
            throws Exception {

        KDF pbkdf2 = KDF.getInstance("PBKDF2WithHmacSHA256");

        PBKDF2KeySpec spec = new PBKDF2KeySpec(
                password,
                salt,
                iterationCount,
                keyLengthBits
        );

        return pbkdf2.deriveKey("AES", spec);
    }

    public static void main(String[] args) throws Exception {
        char[] password = "correct horse battery staple".toCharArray();

        // Generate a random salt — store this alongside the encrypted data
        byte[] salt = new byte[16];
        new java.security.SecureRandom().nextBytes(salt);

        // 600,000 iterations is the OWASP recommendation for PBKDF2-SHA256 in 2025
        SecretKey key = deriveKeyFromPassword(password, salt, 600_000, 256);

        System.out.println("Key algorithm: " + key.getAlgorithm());
        System.out.println("Key length: " + key.getEncoded().length * 8 + " bits");

        // Derive again with the same password + salt — should get the same key
        SecretKey key2 = deriveKeyFromPassword(password, salt, 600_000, 256);
        System.out.println("Keys match: " +
            Arrays.equals(key.getEncoded(), key2.getEncoded()));
    }
}

Derive Raw Bytes (Not a Key)

Sometimes you need raw bytes (e.g., an IV, an HMAC key, a nonce) rather than a SecretKey:

KDF hkdf = KDF.getInstance("HKDF-SHA256");

HKDFParameterSpec params = HKDFParameterSpec.ofExtract()
        .addIKM(sharedSecret)
        .addSalt(salt)
        .thenExpand("myapp-iv".getBytes(), 16);   // 16 bytes = 128-bit IV

byte[] iv = hkdf.deriveData(params);   // returns byte[] instead of SecretKey

Part 2: PEM Encodings (JEP 470) — Preview

Note: JEP 470 is a preview feature in Java 25. Enable with --enable-preview.

What Is PEM?

PEM (Privacy Enhanced Mail) is the text-based encoding format for cryptographic objects that you see everywhere:

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7o4qne60TB3wo
...
-----END PRIVATE KEY-----

Every TLS certificate, SSH key, HTTPS server, and JWT library uses PEM files. Before Java 25, reading a PEM file in standard Java required:

  • Stripping the header/footer lines manually
  • Base64 decoding the body
  • Wrapping in PKCS8EncodedKeySpec or X509EncodedKeySpec
  • Calling KeyFactory.generatePrivate() or CertificateFactory.generateCertificate()

Four steps, all boilerplate.

Java 25 Preview: PemDecoder and PemEncoder

import java.security.PemDecoder;
import java.security.PemEncoder;

Reading a PEM private key

import module java.base;
import java.security.*;

// --enable-preview required
public class PemExample {

    public static PrivateKey loadPrivateKey(Path pemFile) throws Exception {
        String pem = Files.readString(pemFile);

        // Parse PEM — handles header/footer and base64 automatically
        PemDecoder.PemObject pemObject = PemDecoder.decode(pem);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(pemObject.der());

        return KeyFactory.getInstance("RSA").generatePrivate(spec);
    }

    public static PublicKey loadPublicKey(Path pemFile) throws Exception {
        String pem = Files.readString(pemFile);
        PemDecoder.PemObject pemObject = PemDecoder.decode(pem);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(pemObject.der());
        return KeyFactory.getInstance("RSA").generatePublic(spec);
    }

    public static String encodePrivateKey(PrivateKey key) {
        return PemEncoder.encode("PRIVATE KEY", key.getEncoded());
    }

    public static String encodePublicKey(PublicKey key) {
        return PemEncoder.encode("PUBLIC KEY", key.getEncoded());
    }

    public static void main(String[] args) throws Exception {
        // Generate an RSA key pair for demonstration
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair pair = gen.generateKeyPair();

        // Encode to PEM
        String privatePem = encodePrivateKey(pair.getPrivate());
        String publicPem  = encodePublicKey(pair.getPublic());

        System.out.println(privatePem);
        System.out.println(publicPem);

        // Write to files
        Files.writeString(Path.of("private.pem"), privatePem);
        Files.writeString(Path.of("public.pem"),  publicPem);

        // Round-trip: read back and verify
        PrivateKey loadedPrivate = loadPrivateKey(Path.of("private.pem"));
        System.out.println("Keys match: " +
            Arrays.equals(pair.getPrivate().getEncoded(), loadedPrivate.getEncoded()));
    }
}

Reading a PEM certificate chain

public static List<Certificate> loadCertificateChain(Path pemFile) throws Exception {
    String pem = Files.readString(pemFile);
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    List<Certificate> certs = new ArrayList<>();

    // PemDecoder.decodeAll handles multi-certificate PEM files
    for (PemDecoder.PemObject obj : PemDecoder.decodeAll(pem)) {
        certs.add(cf.generateCertificate(
            new java.io.ByteArrayInputStream(obj.der())
        ));
    }
    return certs;
}

Putting It Together: Secure Messaging Example

// Combine KDF + encryption for a complete secure messaging primitive
import module java.base;
import javax.crypto.*;
import javax.crypto.spec.*;

public class SecureMessage {

    // Encrypt a message using a password-derived key
    public static byte[] encrypt(String message, char[] password) throws Exception {
        // Random salt and IV
        byte[] salt = new byte[16];
        byte[] iv   = new byte[12];   // 96-bit IV for GCM
        new SecureRandom().nextBytes(salt);
        new SecureRandom().nextBytes(iv);

        // Derive AES-256 key using PBKDF2
        KDF pbkdf2 = KDF.getInstance("PBKDF2WithHmacSHA256");
        SecretKey aesKey = pbkdf2.deriveKey("AES",
            new PBKDF2KeySpec(password, salt, 600_000, 256));

        // Encrypt with AES-GCM
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(128, iv));
        byte[] ciphertext = cipher.doFinal(message.getBytes(StandardCharsets.UTF_8));

        // Prepend salt + IV to ciphertext: [16 bytes salt][12 bytes iv][ciphertext]
        byte[] result = new byte[16 + 12 + ciphertext.length];
        System.arraycopy(salt,       0, result,  0, 16);
        System.arraycopy(iv,         0, result, 16, 12);
        System.arraycopy(ciphertext, 0, result, 28, ciphertext.length);
        return result;
    }

    // Decrypt using the same password
    public static String decrypt(byte[] data, char[] password) throws Exception {
        byte[] salt       = Arrays.copyOfRange(data,  0, 16);
        byte[] iv         = Arrays.copyOfRange(data, 16, 28);
        byte[] ciphertext = Arrays.copyOfRange(data, 28, data.length);

        KDF pbkdf2 = KDF.getInstance("PBKDF2WithHmacSHA256");
        SecretKey aesKey = pbkdf2.deriveKey("AES",
            new PBKDF2KeySpec(password, salt, 600_000, 256));

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(128, iv));
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        char[] password = "hunter2".toCharArray();
        String original = "Hello, secure world!";

        byte[] encrypted = encrypt(original, password);
        System.out.println("Encrypted (" + encrypted.length + " bytes): " +
            HexFormat.of().formatHex(encrypted));

        String decrypted = decrypt(encrypted, password);
        System.out.println("Decrypted: " + decrypted);
        System.out.println("Match: " + original.equals(decrypted));
    }
}

Summary

FeatureJEPStatusWhat it gives you
KDF API510FinalStandard KDF.getInstance() for HKDF, PBKDF2
PEM Encodings470PreviewPemDecoder / PemEncoder for reading/writing PEM files

The KDF API is final and ready for production use today — no flags needed. PEM support is preview but the API is straightforward and likely to finalize in Java 26.


Series Complete

You have now covered all 12 parts of the Java 25 Tutorial series:

#Article
1Java 25 Overview
2Setting Up Java 25
3Flexible Constructor Bodies
4Module Import Declarations
5Compact Source Files & Instance Main Methods
6Primitive Types in Patterns
7Scoped Values
8Structured Concurrency
9Compact Object Headers
10Generational Shenandoah
11AOT Compilation
12Security: KDF API & PEM Encodings ← you are here

Java 25 LTS is the strongest Java release since Java 21. The language improvements reduce boilerplate, the concurrency APIs finally graduate from years of preview, and the JVM performance improvements are available with a JVM flag and zero code changes. It is ready for production.