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
+56
View File
@@ -0,0 +1,56 @@
$ javap javax.crypto.KDF javax.crypto.KDFParameters javax.crypto.KDFSpi javax.crypto.spec.HKDFParameterSpec 'javax.crypto.spec.HKDFParameterSpec$Builder' ... (25.0.5+8-LTS)
public final class javax.crypto.KDF {
static final boolean $assertionsDisabled;
public java.lang.String getAlgorithm();
public java.lang.String getProviderName();
public javax.crypto.KDFParameters getParameters();
public static javax.crypto.KDF getInstance(java.lang.String) throws java.security.NoSuchAlgorithmException;
public static javax.crypto.KDF getInstance(java.lang.String, java.lang.String) throws java.security.NoSuchAlgorithmException, java.security.NoSuchProviderException;
public static javax.crypto.KDF getInstance(java.lang.String, java.security.Provider) throws java.security.NoSuchAlgorithmException;
public static javax.crypto.KDF getInstance(java.lang.String, javax.crypto.KDFParameters) throws java.security.NoSuchAlgorithmException, java.security.InvalidAlgorithmParameterException;
public static javax.crypto.KDF getInstance(java.lang.String, javax.crypto.KDFParameters, java.lang.String) throws java.security.NoSuchAlgorithmException, java.security.NoSuchProviderException, java.security.InvalidAlgorithmParameterException;
public static javax.crypto.KDF getInstance(java.lang.String, javax.crypto.KDFParameters, java.security.Provider) throws java.security.NoSuchAlgorithmException, java.security.InvalidAlgorithmParameterException;
public javax.crypto.SecretKey deriveKey(java.lang.String, java.security.spec.AlgorithmParameterSpec) throws java.security.InvalidAlgorithmParameterException, java.security.NoSuchAlgorithmException;
public byte[] deriveData(java.security.spec.AlgorithmParameterSpec) throws java.security.InvalidAlgorithmParameterException;
static {};
}
public interface javax.crypto.KDFParameters {
}
public abstract class javax.crypto.KDFSpi {
protected javax.crypto.KDFSpi(javax.crypto.KDFParameters) throws java.security.InvalidAlgorithmParameterException;
protected abstract javax.crypto.KDFParameters engineGetParameters();
protected abstract javax.crypto.SecretKey engineDeriveKey(java.lang.String, java.security.spec.AlgorithmParameterSpec) throws java.security.InvalidAlgorithmParameterException, java.security.NoSuchAlgorithmException;
protected abstract byte[] engineDeriveData(java.security.spec.AlgorithmParameterSpec) throws java.security.InvalidAlgorithmParameterException;
}
public interface javax.crypto.spec.HKDFParameterSpec extends java.security.spec.AlgorithmParameterSpec {
public static javax.crypto.spec.HKDFParameterSpec$Builder ofExtract();
public static javax.crypto.spec.HKDFParameterSpec$Expand expandOnly(javax.crypto.SecretKey, byte[], int);
}
public final class javax.crypto.spec.HKDFParameterSpec$Builder {
public javax.crypto.spec.HKDFParameterSpec$Extract extractOnly();
public javax.crypto.spec.HKDFParameterSpec$ExtractThenExpand thenExpand(byte[], int);
public javax.crypto.spec.HKDFParameterSpec$Builder addIKM(javax.crypto.SecretKey);
public javax.crypto.spec.HKDFParameterSpec$Builder addIKM(byte[]);
public javax.crypto.spec.HKDFParameterSpec$Builder addSalt(javax.crypto.SecretKey);
public javax.crypto.spec.HKDFParameterSpec$Builder addSalt(byte[]);
}
public final class javax.crypto.spec.HKDFParameterSpec$Extract implements javax.crypto.spec.HKDFParameterSpec {
public java.util.List<javax.crypto.SecretKey> ikms();
public java.util.List<javax.crypto.SecretKey> salts();
}
public final class javax.crypto.spec.HKDFParameterSpec$Expand implements javax.crypto.spec.HKDFParameterSpec {
public javax.crypto.SecretKey prk();
public byte[] info();
public int length();
}
public final class javax.crypto.spec.HKDFParameterSpec$ExtractThenExpand implements javax.crypto.spec.HKDFParameterSpec {
public java.util.List<javax.crypto.SecretKey> ikms();
public java.util.List<javax.crypto.SecretKey> salts();
public byte[] info();
public int length();
}
$ javap -v javax.crypto.KDF | grep -ci PreviewFeature (0 = not a preview API)
0
$ unzip -p src.zip java.base/javax/crypto/KDF.java | grep @since
* @since 25
+34
View File
@@ -0,0 +1,34 @@
$ java -cp classes ApiTour (25.0.5+8-LTS)
== KDF algorithms registered by the providers of this JDK
SunJCE HKDF-SHA256
SunJCE HKDF-SHA384
SunJCE HKDF-SHA512
== getInstance: what works and what does not
algorithm=HKDF-SHA256 provider=SunJCE parameters=null
HKDF -> NoSuchAlgorithmException: No available provider supports the specified algorithm
HKDF-SHA1 -> NoSuchAlgorithmException: No available provider supports the specified algorithm
hkdf-sha256 -> available
PBKDF2WithHmacSHA256 -> NoSuchAlgorithmException: No available provider supports the specified algorithm
Argon2 -> NoSuchAlgorithmException: No available provider supports the specified algorithm
== Shape 1: extract then expand in one call (the common case)
deriveData -> byte[42] 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865
deriveKey -> algorithm=AES format=RAW bytes=32
== Shape 2: extract only (returns the pseudorandom key, PRK)
PRK = 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5
== Shape 3: expand only (you already hold a uniformly random key)
PASS extractOnly + expandOnly equals the one-call result
== The same KDF object can be used again, and gives the same bytes
PASS second call with the same spec is identical
== Several IKM or salt pieces are concatenated in the order added
PASS addIKM(first 10) + addIKM(last 12) == addIKM(all 22)
== deriveKey does not check key sizes; Cipher.init does
deriveKey("AES") length 16 -> ok, 16 bytes
Cipher.init -> ok
deriveKey("AES") length 24 -> ok, 24 bytes
Cipher.init -> ok
deriveKey("AES") length 32 -> ok, 32 bytes
Cipher.init -> ok
deriveKey("AES") length 20 -> ok, 20 bytes
Cipher.init -> InvalidKeyException: Invalid AES key length: 20 bytes
ALL CHECKS PASSED
exit=0
+17
View File
@@ -0,0 +1,17 @@
$ java -cp classes Rfc5869Vectors (25.0.5+8-LTS)
== RFC 5869 A.1 basic (IKM 22 B, salt 13 B, info 10 B, L=42)
PASS extract+expand -> OKM
PASS extractOnly -> PRK
PASS expandOnly(PRK) -> OKM
== RFC 5869 A.2 long inputs (IKM 80 B, salt 80 B, info 80 B, L=82)
PASS extract+expand -> OKM
PASS extractOnly -> PRK
PASS expandOnly(PRK) -> OKM
== RFC 5869 A.3 empty salt and info (IKM 22 B, salt 0 B, info 0 B, L=42)
PASS extract+expand -> OKM
PASS extractOnly -> PRK
PASS expandOnly(PRK) -> OKM
== RFC 5869 cases 4 to 7 use SHA-1, which this provider does not offer
PASS HKDF-SHA1 -> NoSuchAlgorithmException
ALL CHECKS PASSED
exit=0
@@ -0,0 +1,19 @@
$ java -cp classes RefCheck (25.0.5+8-LTS)
== RefHkdf (HmacSHA256 only) on RFC 5869 A.1 basic
PASS PRK
PASS OKM
== RefHkdf (HmacSHA256 only) on RFC 5869 A.2 long inputs
PASS PRK
PASS OKM
== RefHkdf (HmacSHA256 only) on RFC 5869 A.3 empty salt and info
PASS PRK
PASS OKM
ALL CHECKS PASSED
exit=0
$ java -cp classes Interop interop/cases.tsv (25.0.5+8-LTS)
== interop/cases.tsv: 300 cases {SHA256=100, SHA384=100, SHA512=100}, longest output 16320 bytes
PASS JDK KDF API matches Python cryptography on 300 of 300
PASS hand-written RefHkdf matches Python cryptography on 300 of 300
ALL CHECKS PASSED
exit=0
+5
View File
@@ -0,0 +1,5 @@
python cryptography 50.0.1
wrote 300 cases
cases.tsv regenerates byte-identically
OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)
openssl kdf HKDF: 32 of 32 cases match (2 RFC 5869 + every 10th row of cases.tsv)
+18
View File
@@ -0,0 +1,18 @@
$ java -cp classes Tenants (25.0.5+8-LTS)
== Per-tenant, per-purpose, per-version keys (first 8 bytes shown)
tenant-acme aes v1 = 503b6a158de948ef hmac v1 = de11af197467596c aes v2 = 0c80331430e74d8d
tenant-globex aes v1 = 3e7dd7c93250c77e hmac v1 = ab1c9c0b01b4e64b aes v2 = 22859d1db92cf04b
PASS same inputs give the same key every time (nothing to store)
PASS two tenants get different keys
PASS encryption key and MAC key of one tenant differ
PASS key version 2 differs from version 1 (rotation)
== Isolation in practice: tenant A's ciphertext is opaque to tenant B's key
acme key decrypts: invoice #4711 for acme
globex key fails: AEADBadTagException: Tag mismatch
PASS tenant B cannot open tenant A's data
== HMAC key from the same master, used for a signed token
tag = 3cb1fb88afc1c173475dac3a12f68f05...
== The info bytes that separate the contexts (hex) for tenant-acme / aes-gcm / v1
000f61636d652d62696c6c696e672f763100076165732d67636d000b74656e616e742d61636d65000400000001
ALL CHECKS PASSED
exit=0
+18
View File
@@ -0,0 +1,18 @@
$ java -cp classes Mistakes (25.0.5+8-LTS)
== 1. Same info for two purposes: the 'two' keys are one key
PASS encryption key == MAC key when info is the same
PASS distinct info labels give distinct keys
== 2. Gluing fields together without a delimiter: tenant 'ab' + 'c' collides with 'a' + 'bc'
PASS naive concatenation collides
PASS length-prefixed fields (Tenants.info) do not collide
== 3. Salt: leaving it out is the same as HashLen zero bytes, and a salt is not a secret
PASS no salt == 32 zero bytes (SHA-256)
PASS a different salt changes every output, so a salt change is a key change
== 4. Asking for more than 255 * HashLen bytes
length 8160 -> ok (8160 bytes)
length 8161 -> InvalidAlgorithmParameterException: The specified SunJCE provider does not support this input
== 5. Deriving a key from a low-entropy secret (a 4-digit PIN)
HKDF-derived key: attacker finds PIN 4821 after 4822 guesses
PASS PBKDF2 (600000 iterations) is at least 1000 times slower per guess than HKDF
ALL CHECKS PASSED
exit=0