Skip to main content

Key Derivation Function API in Java (JEP 510): HKDF Explained with Real Examples

JEP 510 made javax.crypto.KDF final in JDK 25. A beginner-first guide to HKDF extract and expand, per-tenant key derivation with info context separation, RFC 5869 vectors, interop checks against Python cryptography and openssl, and the mistakes that silently break it.

Every system that encrypts customer data eventually hits the same question: you have one strong secret, and you need many keys. One per tenant, one for encryption and a different one for signing, a fresh one after rotation. Storing a thousand random keys is a thousand things to back up, leak and lose. A key derivation function (KDF) turns the question around: keep one master secret and compute every other key from it, on demand, the same way every time. Until Java 25 the JDK had no public API for this. People copy-pasted HKDF from Stack Overflow, pulled in BouncyCastle or Tink, or wrote the HMAC loop by hand. JEP 510 added javax.crypto.KDF, and in JDK 25 it is final, with no preview flag. This guide to the Java KDF API and HKDF starts from “what is a KDF” and ends with per-tenant keys, a hand-written second implementation, and comparisons against Python and OpenSSL. Every number below comes from a run, and the code is in the kdf module of javademos.
Versions tested. Temurin 25.0.5+8 (JDK 25 is an LTS release), no --enable-preview. Python cryptography 50.0.1 and the openssl 3.0.13 command line were used only as comparison tools. javax.crypto.KDF carries @since 25 and no @PreviewFeature annotation in this JDK (output/01). The JEP page itself (openjdk.org/jeps/510) lists Status Closed / Delivered, Release 25, and relates it to JEP 478, the JDK 24 preview. I did not have a JDK 24 to run, so I make no claims about the preview’s behaviour.

A KDF turns one secret into many keys, safely

A KDF takes some secret input (called input key material, IKM), a bit of context, and a length, and returns that many bytes that look random. Two properties matter. It is deterministic: the same inputs always give the same output, so you never need to store the derived key. And it is one-way and context-sensitive: knowing one derived key tells you nothing about the master key or about a key derived with different context. HKDF (RFC 5869) is the KDF to use when your input is already a strong secret: a random 256-bit master key from a KMS, a Diffie-Hellman shared secret, a session key. It works in two steps built on HMAC. Extract mixes the secret with an optional salt into a uniformly random pseudorandom key (PRK). Expand stretches the PRK into as many bytes as you ask for, steered by an info string that says what the key is for.
HKDF = Extract, then Expand IKMinput key material salt (optional)public, not secret ExtractHMAC(salt, IKM) PRKuniform, 32 bytes info: aes, tenant AExpand info: hmac, tenant AExpand info: aes, tenant BExpand Extract runs once and cleans up the secret. Expand runs once per label and is where one secret becomes many keys. Change one byte of info and the output is a completely different key; the PRK never leaves your code. The dashed salt box means it can be left out: HKDF then uses a string of zero bytes.
The diagram shows the whole idea. Extract happens once. Expand happens once per label, and the label (info) is what makes “AES key for tenant A” and “HMAC key for tenant A” two unrelated keys even though they come from the same PRK. Keep that sentence in mind: the secret comes from IKM, the separation comes from info. Most of the mistakes later in the post are one of those two roles being filled by the wrong input.
Going deeper: what the salt is really for Extract exists because real secrets are not always uniformly random: a Diffie-Hellman result has structure, and the concatenation of two secrets is lumpy. HMAC with a salt as key smooths that into a uniform PRK. A random salt strengthens this, but it is not secret and may be reused, and when you have none, RFC 5869 says to use HashLen zero bytes. If your IKM is already a uniformly random key (a 256-bit key from your KMS), the RFC allows skipping Extract and going straight to Expand; the JDK exposes this as expandOnly.

The smallest thing that works

Here is the first RFC 5869 test vector (22 bytes of 0x0b as the secret, a 13-byte salt, a 10-byte info, 42 bytes out) through the JDK 25 API. From src/ApiTour.java:
        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));
HKDFParameterSpec.ofExtract() starts a builder; you add the secret and salt, then thenExpand(info, length) says “also run Expand” and finishes the spec. deriveData hands back raw bytes. Use deriveKey("AES", spec) instead when you want a SecretKey that you can pass straight to a Cipher. The output, from output/02-api-tour.txt:
== 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
The 42 bytes at the top are exactly the OKM printed in RFC 5869 appendix A.1, and the PRK matches the RFC’s PRK. The next-but-one section shows that this is asserted, not eyeballed.

The API in one table: three spec shapes, three algorithm names

The JDK offers three shapes of request, all built from HKDFParameterSpec:
You wantBuild it withReturns
Secret to keys (normal case)ofExtract().addIKM(..).addSalt(..).thenExpand(info, len)OKM bytes or a SecretKey
Only the PRKofExtract().addIKM(..).addSalt(..).extractOnly()the PRK as a SecretKey
Only Expand, from a key you already trustHKDFParameterSpec.expandOnly(prk, info, len)OKM bytes or a SecretKey
And exactly three algorithms are registered, all in the SunJCE provider. Anything else, including plain "HKDF" without a hash and SHA-1 variants, is a NoSuchAlgorithmException:

From output/02-api-tour.txt:

== 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
Two behaviours surprised me while writing the demos. First, deriveKey("AES", ..., 20 bytes) succeeds: the KDF does not know AES key sizes, and the error only appears later at Cipher.init (output/02-api-tour.txt, last block). Second, algorithm names are case-insensitive, as everywhere in JCA (hkdf-sha256 worked).
Do not share one KDF object between threads. The class Javadoc says KDF objects are not thread-safe. They are cheap to create, so call KDF.getInstance("HKDF-SHA256") inside the method that derives, as Tenants.java does.
Going deeper: the complete API surface and the provider SPI The output below is javap on JDK 25.0.5, taken from output/01-api-shape.txt. KDFParameters is an empty marker interface and HKDF in the JDK does not use it (getParameters() returned null above). KDFSpi is the extension point a provider implements to add another KDF; the JDK itself ships only HKDF. PBKDF2 stays where it always was, SecretKeyFactory.
$ 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();
}

Under the hood: Expand is a short HMAC loop

You should be able to trust a cryptographic API only as far as you can explain it, so here is HKDF with nothing but javax.crypto.Mac. Extract is one HMAC call. Expand chains blocks: T(1) = HMAC(PRK, info | 0x01), T(2) = HMAC(PRK, T(1) | info | 0x02), and so on, and the output is the first L bytes of T(1) | T(2) | .... From src/RefHkdf.java:
    /** HKDF-Extract: PRK = HMAC-Hash(salt, IKM); an absent salt is HashLen zero bytes. */
    static byte[] extract(String hmac, byte[] salt, byte[] ikm) throws Exception {
        Mac mac = Mac.getInstance(hmac);
        byte[] s = (salt == null || salt.length == 0) ? new byte[mac.getMacLength()] : salt;
        mac.init(new SecretKeySpec(s, hmac));
        return mac.doFinal(ikm);
    }

    /** HKDF-Expand: T(i) = HMAC(PRK, T(i-1) | info | i), OKM = first L bytes of T(1) | T(2) | ... */
    static byte[] expand(String hmac, byte[] prk, byte[] info, int length) throws Exception {
        Mac mac = Mac.getInstance(hmac);
        int hashLen = mac.getMacLength();
        if (length > 255 * hashLen) throw new IllegalArgumentException("L > 255*HashLen");
        mac.init(new SecretKeySpec(prk, hmac));
        ByteArrayOutputStream okm = new ByteArrayOutputStream();
        byte[] t = new byte[0];
        for (int i = 1; okm.size() < length; i++) {
            mac.update(t);
            mac.update(info);
            mac.update((byte) i);
            t = mac.doFinal();
            okm.write(t);
        }
        return Arrays.copyOf(okm.toByteArray(), length);
    }
The single byte counter is why the output length is capped at 255 × hash length: 8,160 bytes for SHA-256, 16,320 for SHA-512. The JDK enforces this, though its error message is vague (shown in the mistakes section below). This reference class is also the independent second implementation used in the next section.

How to know it is correct: RFC vectors and two outside implementations

“It produces 42 bytes” proves nothing. Correctness here means “it matches what every other HKDF produces”, and there are three kinds of evidence, each stronger than the last. All of it is checked on every run of run.sh; a mismatch fails the script. 1. The RFC 5869 test vectors. Appendix A has SHA-256 cases (1 to 3) and SHA-1 cases (4 to 7). The JDK has no SHA-1 HKDF, so cases 4 to 7 cannot run, but 1 to 3 do, each asserted three ways: the one-call OKM, the extract-only PRK, and the OKM from expandOnly on that PRK. From src/Rfc5869Vectors.java and output/03-rfc5869-vectors.txt:
== 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
2. A hand-written reference. The RefHkdf class above is first checked against the same RFC vectors, so it is known good, and then used as an oracle on 300 generated cases. It still uses the JDK’s HMAC, so it shares the HMAC code with the KDF, but not the HKDF code. 3. Different people’s code. interop/gen_cases.py uses Python’s cryptography package to compute the expected output for 300 random inputs (100 each for SHA-256, SHA-384, SHA-512; empty salt and empty info included; output lengths from 1 byte up to the 255×HashLen maximum). Java reads interop/cases.tsv and compares both the JDK API and RefHkdf with Python’s answer. Then interop/openssl_check.py asks the openssl kdf command, which involves neither Java nor Python, for the two RFC cases and every tenth row. From output/04 and output/05:
$ 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
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)
All agree. This is the pattern to copy if you ever store data under derived keys: pin the derivation with vectors from outside your codebase, because a key derived differently after an upgrade is data you can no longer decrypt. Note what this does not prove: it shows the JDK computes standard HKDF, not that your info design is sound.
Going deeper: reproducing the openssl and Python side by hand The openssl 3.x CLI exposes HKDF as openssl kdf. The exact command used inside openssl_check.py for RFC case A.1 is:
openssl kdf -keylen 42 -kdfopt digest:SHA256 -kdfopt hexkey:0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -kdfopt hexinfo:f0f1f2f3f4f5f6f7f8f9 -kdfopt mode:EXTRACT_AND_EXPAND -kdfopt hexsalt:000102030405060708090a0b0c HKDF
This command is quoted from the script; its result is one of the matches counted in output/05. I only ran the EXTRACT_AND_EXPAND mode; openssl documents two more modes, EXTRACT_ONLY and EXPAND_ONLY, which I did not run. Python’s version is HKDF(algorithm=hashes.SHA256(), length=42, salt=salt or None, info=info).derive(ikm), as in gen_cases.py.

Per-tenant keys: put the context in info

Now the job from the opening. A multi-tenant service has one master key in a KMS and needs, for every tenant, an AES key to encrypt data and an HMAC key to sign tokens, and later a rotated version of each. The design in src/Tenants.java: the master key is the IKM, a constant application string is the salt, and everything that distinguishes one key from another goes into info.
One master key, a tree of derived keys master keyKMS / secret store info = aes, acme, v1 info = hmac, acme, v1 info = aes, acme, v2 info = aes, globex, v1 acme AES key v1 acme HMAC key v1 acme AES key v2 globex AES key v1 Only the master key is stored. Every green box is recomputed on demand from the master key and its info string.
The diagram’s argument: only the red box is stored. The green boxes are never persisted; any server holding the master key recomputes them, which also means a key can be “rotated” by bumping the version number in info. The code:
    /** Unambiguous info: a fixed label, then each field prefixed with its 2-byte length. */
    static byte[] info(String purpose, String tenant, int keyVersion) {
        byte[][] fields = {
            "acme-billing/v1".getBytes(StandardCharsets.UTF_8),
            purpose.getBytes(StandardCharsets.UTF_8),
            tenant.getBytes(StandardCharsets.UTF_8),
            ByteBuffer.allocate(4).putInt(keyVersion).array()};
        int size = 0;
        for (byte[] f : fields) size += 2 + f.length;
        ByteBuffer b = ByteBuffer.allocate(size);
        for (byte[] f : fields) b.putShort((short) f.length).put(f);
        return b.array();
    }

    static SecretKey derive(SecretKey master, String alg, int bytes, String purpose, String tenant, int version)
            throws GeneralSecurityException {
        KDF kdf = KDF.getInstance("HKDF-SHA256");
        return kdf.deriveKey(alg, HKDFParameterSpec.ofExtract()
                .addIKM(master).addSalt(APP_SALT).thenExpand(info(purpose, tenant, version), bytes));
    }

Note the info encoding. It is not "acme-billing:aes-gcm:tenant-acme:1"; each field is prefixed with its length, for a reason the next section proves. The result of running it, from output/06-tenants.txt (first 8 bytes of each key shown, and the master key is a fixed demo value):

== 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
Tenant globex’s key cannot decrypt acme’s ciphertext: AES-GCM reports Tag mismatch. Isolation here is a property of the derivation, so a bug in your query code that fetches the wrong tenant’s row still yields unreadable bytes.
Going deeper: where the master key lives, rotation and the blast radius HKDF does not make a leaked master key safe: everything derived from it falls with it. Keep the master in a KMS or HSM, and consider letting the KMS do the derivation if it supports it. Rotation by version number is cheap for new data, but old ciphertext needs the old version, so store the version (or a key id) next to each ciphertext; that is why version is part of info and not a global setting. If tenants must be able to demand “destroy my data”, derivation alone cannot give crypto-shredding, because the master can always re-derive the tenant key; store a per-tenant random key (wrapped by the master) for that requirement.

What goes wrong: the mistakes, reproduced

Each of these is a real, silent failure: nothing throws, and the result still looks like a key. Code in src/Mistakes.java, output in output/07-mistakes.txt.

Reusing info across purposes

If the encryption key and the MAC key are derived with the same info, they are the same key. That breaks the assumption of most constructions that use two independent keys.
== 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

Building info by concatenating fields

Tenant "ab" with suffix "c" and tenant "a" with suffix "bc" both produce "abc", hence the same key, hence tenants who can read each other’s data. Separators help only if they cannot appear in a field. Length-prefixing, as in the info method above, has no such condition.
== 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

Misunderstanding the salt

Leaving the salt out is not an error; it means a string of zero bytes. The demo shows no salt and 32 zero bytes give identical output. The opposite mistake is treating the salt as the tenant separator or as a secret. Salt is public and per-application or per-session; put the tenant in info. A fixed salt shared by all tenants is fine; changing the salt later changes every key.
== 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

Asking for too much, and the vague error

== 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
8,160 bytes works for SHA-256 and 8,161 fails with InvalidAlgorithmParameterException: The specified SunJCE provider does not support this input, which does not mention the 255-block limit. In practice you need 16 to 64 bytes per key, so if you hit this you are probably using HKDF as a random-number generator.

Deriving from a password or PIN

HKDF is not a password hash. It is designed to be fast, because its input is assumed to be a strong secret. If the input is a 4-digit PIN there are only 10,000 candidates, and a fast KDF lets an attacker try them all in milliseconds. The demo derives a key from the PIN 4821 with HKDF, then plays the attacker. For comparison it times PBKDF2 with 600,000 iterations, which is deliberately slow:
== 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
The HKDF-derived key falls after 4,822 guesses. The search space is only 10,000 PINs, so slow hashing can make each guess cost more but cannot shrink the space; the check line records that PBKDF2 at 600,000 iterations costs at least 1,000 times more per guess than HKDF on this machine. I removed the exact guesses-per-second figures from the committed output because they drift on a shared machine. For passwords use PBKDF2 via SecretKeyFactory (or Argon2 or scrypt from a library); you can then feed that strong result into HKDF as IKM if you need several keys from it.
Should you use HKDF directly at all? Yes for deriving subkeys from a strong secret, for protocol key schedules and for tenant or purpose separation. No for passwords, no as a replacement for a KMS, and no if your library (Tink, AWS Encryption SDK, your database driver) already derives keys for you; using theirs avoids designing your own info format. Also be honest about scope: this post covers the JDK API and HKDF, not a security review of your design. The checks above show the JDK matches the standard on the cases I ran.
Going deeper: smaller edge cases
  • Multiple addIKM or addSalt calls concatenate in the order added, and empty arrays are discarded (Javadoc); the demo checks that 10 + 12 bytes equal one 22-byte IKM (output/02).
  • RFC 5869 SHA-1 cases cannot run: the JDK only registers SHA-256, 384 and 512.
  • The master key can be passed as a SecretKey directly (addIKM(SecretKey)), which keeps it out of a byte[] you must then wipe; the demo uses that in Tenants.
  • JDK 24 had this API as a preview (JEP 478); code compiled there with preview enabled must be recompiled on 25. I did not run JDK 24.
  • Related: the PEM encodings post for loading the keys you might wrap, the FIPS 140-3 post for provider constraints, and the Java 21 to 25 features guide.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.