Add kdf module: Key Derivation Function API (JEP 510, final in JDK 25), HKDF, RFC 5869 vectors, interop with Python cryptography and openssl, per-tenant keys

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
2026-10-28 18:07:57 +00:00
co-authored by Claude Sonnet 5.5
parent c93a1fffbb
commit eca73ad544
21 changed files with 995 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.util.Arrays;
/** What the JDK 25 KDF API offers: algorithm names, the three spec shapes, and what each returns. */
public class ApiTour {
public static void main(String[] args) throws Exception {
Fx.header("KDF algorithms registered by the providers of this JDK");
for (Provider p : Security.getProviders())
for (Provider.Service s : p.getServices())
if (s.getType().equals("KDF"))
System.out.println(p.getName() + " " + s.getAlgorithm());
Fx.header("getInstance: what works and what does not");
KDF kdf = KDF.getInstance("HKDF-SHA256");
System.out.println("algorithm=" + kdf.getAlgorithm() + " provider=" + kdf.getProviderName() + " parameters=" + kdf.getParameters());
for (String bad : new String[] {"HKDF", "HKDF-SHA1", "hkdf-sha256", "PBKDF2WithHmacSHA256", "Argon2"}) {
try {
KDF.getInstance(bad);
System.out.println(bad + " -> available");
} catch (NoSuchAlgorithmException e) {
System.out.println(bad + " -> NoSuchAlgorithmException: " + e.getMessage());
}
}
byte[] ikm = Fx.fill(0x0b, 22), salt = Fx.range(0, 13), info = Fx.range(0xf0, 10);
Fx.header("Shape 1: extract then expand in one call (the common case)");
HKDFParameterSpec both = HKDFParameterSpec.ofExtract().addIKM(ikm).addSalt(salt).thenExpand(info, 42);
byte[] okm = kdf.deriveData(both);
System.out.println("deriveData -> byte[" + okm.length + "] " + Fx.hex(okm));
SecretKey k = kdf.deriveKey("AES", HKDFParameterSpec.ofExtract().addIKM(ikm).addSalt(salt).thenExpand(info, 32));
System.out.println("deriveKey -> algorithm=" + k.getAlgorithm() + " format=" + k.getFormat() + " bytes=" + k.getEncoded().length);
Fx.header("Shape 2: extract only (returns the pseudorandom key, PRK)");
SecretKey prk = kdf.deriveKey("Generic", HKDFParameterSpec.ofExtract().addIKM(ikm).addSalt(salt).extractOnly());
System.out.println("PRK = " + Fx.hex(prk.getEncoded()));
Fx.header("Shape 3: expand only (you already hold a uniformly random key)");
byte[] again = kdf.deriveData(HKDFParameterSpec.expandOnly(prk, info, 42));
Fx.check("extractOnly + expandOnly equals the one-call result", Arrays.equals(okm, again));
Fx.header("The same KDF object can be used again, and gives the same bytes");
Fx.check("second call with the same spec is identical", Arrays.equals(okm, kdf.deriveData(both)));
Fx.header("Several IKM or salt pieces are concatenated in the order added");
byte[] split = kdf.deriveData(HKDFParameterSpec.ofExtract()
.addIKM(Arrays.copyOfRange(ikm, 0, 10)).addIKM(Arrays.copyOfRange(ikm, 10, 22))
.addSalt(salt).thenExpand(info, 42));
Fx.check("addIKM(first 10) + addIKM(last 12) == addIKM(all 22)", Arrays.equals(okm, split));
Fx.header("deriveKey does not check key sizes; Cipher.init does");
for (int len : new int[] {16, 24, 32, 20}) {
try {
SecretKey a = kdf.deriveKey("AES", HKDFParameterSpec.ofExtract().addIKM(ikm).thenExpand(info, len));
System.out.println("deriveKey(\"AES\") length " + len + " -> ok, " + a.getEncoded().length + " bytes");
javax.crypto.Cipher.getInstance("AES/GCM/NoPadding").init(javax.crypto.Cipher.ENCRYPT_MODE, a);
System.out.println(" Cipher.init -> ok");
} catch (Exception e) {
System.out.println(" Cipher.init -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
Fx.done();
}
}