Java 25 Security: Key Derivation Function API & PEM Encodings
Overview
Java 25 ships two important security additions:
- JEP 510 — Key Derivation Function (KDF) API — Final. A standard API for HKDF, PBKDF2, and other KDFs.
- JEP 470 — PEM Encodings of Cryptographic Objects — Preview. Read and write
.pemfiles 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
PKCS8EncodedKeySpecorX509EncodedKeySpec - Calling
KeyFactory.generatePrivate()orCertificateFactory.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
| Feature | JEP | Status | What it gives you |
|---|---|---|---|
| KDF API | 510 | Final | Standard KDF.getInstance() for HKDF, PBKDF2 |
| PEM Encodings | 470 | Preview | PemDecoder / 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 |
|---|---|
| 1 | Java 25 Overview |
| 2 | Setting Up Java 25 |
| 3 | Flexible Constructor Bodies |
| 4 | Module Import Declarations |
| 5 | Compact Source Files & Instance Main Methods |
| 6 | Primitive Types in Patterns |
| 7 | Scoped Values |
| 8 | Structured Concurrency |
| 9 | Compact Object Headers |
| 10 | Generational Shenandoah |
| 11 | AOT Compilation |
| 12 | Security: 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.