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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user