JEP 510 Explained: Safe Key Derivation in Java 25

  • Last Updated: December 22, 2025
  • By: javahandson
  • Series
img

JEP 510 Explained: Safe Key Derivation in Java 25

JEP 510 Explained: Safe Key Derivation in Java 25 covers why Java needed a standard Key Derivation Function (KDF), explains HKDF with extract and expand, salt and context, common mistakes, and shows how Java 25 safely derives multiple keys from one secret using a built-in API.

Introduction

Modern applications rely heavily on cryptography, even if we do not always realize it. Whenever we secure an API call, encrypt sensitive data, establish an HTTPS connection, or protect tokens, we are ultimately relying on secret keys. As systems grow more complex, a single application rarely uses just one key. Instead, it needs multiple cryptographic keys, each serving a different purpose.

Why Java Needed JEP 510?

In real-world systems, we often start with one master secret—for example, a password, a shared secret from a handshake, or a key obtained during TLS negotiation. From this single secret, we need to derive many different keys:

  • One key for encryption
  • another key for message authentication (MAC)
  • separate keys for different services, sessions, or environments

If we reuse the same key everywhere, we introduce serious security risks.

Real-World Need for Multiple Keys

Let us consider a simple example. Suppose our application communicates with an external payment service:

  • One key encrypts request payloads
  • Another key signs requests
  • A third key encrypts stored audit logs

All these keys must be different, even though they may originate from the same master secret. Using separate keys limits damage—if one key is compromised, the others remain safe.

Risk of Key Reuse

Key reuse is one of the most common cryptographic mistakes. When the same key is used in multiple places:

  • Attacks against one algorithm can weaken others
  • Patterns can leak information about the secret
  • A single compromise can expose the entire system

Historically, Java developers had to handle this themselves—by manually hashing secrets, concatenating strings, or relying on third-party libraries. These approaches were error-prone and often inconsistent across projects.

Java 25 Introduces a Standard KDF API

Java 25 addresses this gap by introducing JEP 510, which adds a standard Key Derivation Function (KDF) API to the JDK. This API gives us:

  • A safe, well-defined way to derive multiple keys
  • Standard algorithms like HKDF, designed by cryptography experts
  • A consistent approach built directly into the Java platform

With JEP 510, Java finally provides a first-class solution for key derivation—reducing security risks and making cryptographic code clearer, safer, and easier to maintain.

Cryptography Basics We Need

Before we go any further, we need a very light understanding of what a cryptographic key actually is. We will keep this practical and intuitive, without getting into mathematical or theoretical details.

A cryptographic key is a piece of secret data that controls how encryption and decryption work. We can think of it like a key used to lock and unlock a door. The encryption algorithm is the lock, the data is what we want to protect, and the cryptographic key is what makes the lock usable. Even if someone knows the algorithm, they cannot read the data without the correct key.

In most everyday systems, we work with secret keys. A secret key is shared only between trusted parties and is used both to encrypt and decrypt data. For example, when our application encrypts sensitive information before storing it in a database or sending it over the network, it uses a secret key. The same key is required later to decrypt that information.

These secrets come from several real-world sources. Sometimes they are generated using secure random number generators. In other cases, they are derived from user passwords or exchanged during secure handshakes such as TLS connections. Very commonly, an application starts with one strong master secret and then needs multiple usable keys for different purposes like encryption, signing, or session handling.

Keeping keys secret is absolutely critical. If a cryptographic key is exposed, encryption immediately loses its value. An attacker who obtains the key can decrypt data, forge messages, or impersonate trusted systems. There is no gradual failure here—once the key is compromised, security collapses completely. This is why modern cryptography places such a strong emphasis on safe key handling, and why deriving new keys correctly is far safer than reusing the same key everywhere.

The Core Problem: One Secret, Many Responsibilities

In many systems, everything begins with one secret. This secret might come from a secure handshake, a configuration value, or a password-derived value. At first glance, it feels natural to use this same secret everywhere. After all, if it is strong and well-protected, why not reuse it?

The problem is that a single secret often ends up carrying many responsibilities. The same key may be used to encrypt data, authenticate messages, and manage sessions. For example, our application might use one key to encrypt payloads, the same key to generate message authentication codes, and again the same key to secure session tokens. While this may work functionally, it creates serious security risks.

Using one key everywhere tightly couples unrelated cryptographic operations. Each algorithm has different security properties, and weaknesses in one place can affect everything else. If an attacker manages to exploit a flaw in how one operation uses the key, they may gain information that helps break other operations using the same key. This is not a theoretical concern—it is a well-known class of cryptographic failures.

This is where the idea of key separation comes in. Instead of reusing a single secret directly, we derive multiple independent keys from it. Each derived key has a single, well-defined purpose: one for encryption, one for authentication, one for sessions, and so on. Even though all these keys originate from the same master secret, they are cryptographically isolated from each other.

Key separation drastically reduces risk. If one derived key is compromised, the damage is contained. Other keys remain secure, and the overall system does not collapse. This principle is at the heart of modern cryptographic design and is one of the main reasons Java needed a standard and safe way to derive multiple keys from a single secret.

How Developers Solved This Before Java 25?

Before Java 25, Java developers clearly understood the need for multiple keys, but the platform did not provide a standard Key Derivation Function (KDF) API. As a result, every team had to solve the same problem on its own, often in slightly different—and sometimes dangerous—ways.

1. No Standard KDF API in the JDK – The Java Cryptography Architecture (JCA) provided primitives like ciphers, message digests, and MACs, but it stopped short of offering a proper key derivation abstraction. There was no built-in way to say: “We have one master secret, now safely derive multiple independent keys from it.”

Because of this gap, key derivation logic was pushed into application code, where cryptography is hardest to get right.

2. Custom HKDF Implementations – Many teams tried to implement HKDF or similar algorithms themselves. This often involved manually chaining hash functions, concatenating byte arrays, and managing intermediate values. While HKDF is well-defined on paper, implementing it correctly requires careful handling of edge cases such as salt usage, context separation, and output length.

Even small mistakes—like reusing the same input values or skipping extraction steps—could silently weaken security, while still appearing to work in production.

package com.javahandson.jep510;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public class WeakKeyDerivationDemo {

    public static void main(String[] args) throws Exception {

        String masterSecret = "Secret";

        // "Derive" keys by hashing (seems okay, but often misused)
        byte[] authKeyBytes = sha256((masterSecret + "|auth").getBytes(StandardCharsets.UTF_8));
        byte[] encryptionKeyBytes = sha256((masterSecret + "|encryption").getBytes(StandardCharsets.UTF_8));

        SecretKey encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
        SecretKey authKey       = new SecretKeySpec(authKeyBytes, "HmacSHA256");

        System.out.println("Encryption key length: " + encryptionKey.getEncoded().length);
        System.out.println("Auth key length: " + authKey.getEncoded().length);
    }

    static byte[] sha256(byte[] input) throws Exception {
        return MessageDigest.getInstance("SHA-256").digest(input);
    }
}
Output: 
Encryption key length: 32
Auth key length: 32

The above code works functionally. We get two different byte arrays, and the application can happily encrypt data and generate authentication codes. The problem is not that the code fails — the problem is that it gives a false sense of security.

This approach is not a real Key Derivation Function. It skips critical design steps that HKDF was created to enforce. In particular, it assumes that the masterSecret is already perfectly random and suitable for direct hashing. In real systems, that assumption is often false. The secret may come from a password, a handshake, or configuration material that does not have uniform randomness. When we directly hash such input, patterns in the original secret can leak into the derived keys.

The danger increases because both derived keys are mathematically related. They differ only by a small, predictable label like “encryption” or “auth”. If an attacker ever gains partial information about one derived key — for example, through a weakness in how authentication is implemented — that information can help narrow down the possibilities for the other key. This directly violates the principle of key separation.

Ex. Attacker recovers masterSecret

package com.javahandson.jep510;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;

public class RelatedKeysAttackDemo {

    public static void main(String[] args) throws Exception {

        // Our system's (weak) master secret for demo purposes: a 4-digit PIN-like secret.
        // In real life this might be a password-derived value, env config secret, etc.
        String masterSecret = "0420";

        // Our naive derivation (the kind teams often wrote themselves)
        byte[] authKey = sha256((masterSecret + "|auth").getBytes(StandardCharsets.UTF_8));
        byte[] encryptionKey = sha256((masterSecret + "|encryption").getBytes(StandardCharsets.UTF_8));

        System.out.println("System derived authKey (leaked to attacker): " + hex(authKey));
        System.out.println("System derived encryptionKey (secret):     " + hex(encryptionKey));

        // ---------------- ATTACKER SIDE ----------------
        // Attacker knows:
        // 1) the derivation pattern (because it's in code, or reverse engineered, or leaked in docs)
        // 2) the label "auth" is predictable
        // 3) the authKey got exposed (log/memdump/etc.)
        byte[] leakedAuthKey = authKey;

        // Attacker brute-forces possible masterSecret candidates offline
        String recoveredMaster = null;
        for (int pin = 0; pin <= 9999; pin++) {
            String guess = String.format("%04d", pin);

            byte[] guessAuthKey = sha256((guess + "|auth").getBytes(StandardCharsets.UTF_8));
            if (Arrays.equals(guessAuthKey, leakedAuthKey)) {
                recoveredMaster = guess;
                break;
            }
        }

        if (recoveredMaster == null) {
            throw new IllegalStateException("Master secret not found (try smaller search space in demo)");
        }

        System.out.println("\nAttacker recovered masterSecret: " + recoveredMaster);

        // Now attacker can derive the encryption key too (key separation is broken)
        byte[] attackerEncryptionKey =
                sha256((recoveredMaster + "|encryption").getBytes(StandardCharsets.UTF_8));

        System.out.println("Attacker derived encryptionKey:  " + hex(attackerEncryptionKey));
        System.out.println("Matches system encryptionKey?    " + Arrays.equals(attackerEncryptionKey, encryptionKey));
    }

    static byte[] sha256(byte[] input) throws Exception {
        return MessageDigest.getInstance("SHA-256").digest(input);
    }

    static String hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}
Output:
System derived authKey (leaked to attacker): 3df08935d1e38ca9e63aac3f94d08350e808e73cea145b395577b6b938ebeb9b
System derived encryptionKey (secret):     59d3ba469015809641f9bb71cc8f30d72f122dbadb3b343acc976d356cc4e4a6

Attacker recovered masterSecret: 0420
Attacker derived encryptionKey:  59d3ba469015809641f9bb71cc8f30d72f122dbadb3b343acc976d356cc4e4a6
Matches system encryptionKey?    true

3. Reliance on Third-Party Libraries – To avoid writing cryptography from scratch, some developers turned to third-party libraries such as Bouncy Castle. While these libraries are powerful and well-respected, they introduce other challenges:

  • additional dependencies to manage
  • inconsistent APIs across projects
  • varying defaults that developers may not fully understand

More importantly, different teams often made different choices, leading to fragmentation and inconsistent security practices across Java applications.

Example: Same Goal, Different Libraries, Different Security

Suppose two teams in the same organization need to derive multiple keys from a single master secret.

Team A – Uses Bouncy Castle HKDF

package com.javahandson.jep510;

import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;

import java.nio.charset.StandardCharsets;

public class TeamA_BouncyCastleHKDF {

    public static void main(String[] args) {
        byte[] masterSecret = "Secret".getBytes(StandardCharsets.UTF_8);

        byte[] encryptionKey = deriveKey(masterSecret, null, "encryption", 32);
        byte[] authKey       = deriveKey(masterSecret, null, "auth", 32);

        System.out.println("Team A encryptionKey(32) = " + hex(encryptionKey));
        System.out.println("Team A authKey(32)       = " + hex(authKey));
    }

    private static byte[] deriveKey(byte[] ikm, byte[] salt, String info, int length) {
        HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());

        // Team A passes salt as null (allowed by library, but a design choice)
        HKDFParameters params = new HKDFParameters(
                ikm,
                salt,
                info.getBytes(StandardCharsets.UTF_8)
        );

        hkdf.init(params);

        byte[] out = new byte[length];
        hkdf.generateBytes(out, 0, out.length);
        return out;
    }

    private static String hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}
Output:
Team A encryptionKey(32) = 4fd94411bc3eacc8e654d871f3c5eab0a926b360164a2171d122bd01377a88a9
Team A authKey(32)       = 2fca3f6675532be2d43141c16ad00568265be4d1d1f41f3baecc14bf9f046fc6

This code works and uses a well-known library. However:

  • The salt is set to null
  • The output length is chosen arbitrarily
  • context handling depends on how the developer understands HKDF

A different developer might change any of these without realizing the security impact.

Team B – Uses a Different HKDF Style (Same Library)

package com.javahandson.jep510;

import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class TeamB_BouncyCastleHKDF {

    public static void main(String[] args) {
        byte[] masterSecret = "Secret".getBytes(StandardCharsets.UTF_8);

        // Team B uses a "shared salt" configured somewhere (another design choice).
        // (For demo we hardcode it so output is stable.)
        byte[] salt = "STATIC_SALT_FOR_DEMO_32_BYTES!!".getBytes(StandardCharsets.UTF_8);

        byte[] encryptionKey = deriveKey(masterSecret, salt, "encryption", 16);
        byte[] authKey       = deriveKey(masterSecret, salt, "auth", 16);

        System.out.println("Team B encryptionKey(16) = " + hex(encryptionKey));
        System.out.println("Team B authKey(16)       = " + hex(authKey));

        // Show incompatibility if Team B expects Team A's key format or vice versa:
        byte[] teamAEncryptionKey32_noSalt = deriveKey(masterSecret, null, "encryption", 32);
        System.out.println("\nDoes Team B encryptionKey match Team A's (first 16 bytes)? " +
                Arrays.equals(encryptionKey, Arrays.copyOf(teamAEncryptionKey32_noSalt, 16)));
    }

    private static byte[] deriveKey(byte[] ikm, byte[] salt, String info, int length) {
        HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());

        HKDFParameters params = new HKDFParameters(
                ikm,
                salt,
                info.getBytes(StandardCharsets.UTF_8)
        );

        hkdf.init(params);

        byte[] out = new byte[length];
        hkdf.generateBytes(out, 0, out.length);
        return out;
    }

    private static String hex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) sb.append(String.format("%02x", b));
        return sb.toString();
    }
}
Output:
Team B encryptionKey(16) = 7fbe7304324b3cf86e5f34be3c4381be
Team B authKey(16)       = c7329822b4bdecfa1ace199c722362da

Does Team B encryptionKey match Team A's (first 16 bytes)? false

Here, Team B made different decisions:

  • A random salt is used
  • A shorter key length is derived
  • Different context information is applied

Both implementations are technically correct, but the resulting keys have different security properties.

Now imagine these two teams need to integrate or rotate keys together:

  • Keys are derived differently for encryption vs authentication
  • One team assumes salt is mandatory, another assumes it is optional
  • Key sizes vary across services
  • Debugging or auditing security becomes extremely difficult

Worse, none of this is enforced by the platform. The security model lives in developer convention, not in code guarantees.

The Deeper Issue – The real problem is not that Bouncy Castle is unsafe — it is very safe. The problem is that each team becomes a cryptography designer.

Without a standard API in the JDK:

  • Teams pick different libraries
  • Teams choose different defaults
  • Teams interpret the same algorithm differently
  • Over time, this leads to fragmentation and inconsistent security practices across Java applications.

This is precisely the gap Java 25’s KDF API aims to close: providing a standard, well-defined, platform-level way to derive keys, so security decisions are consistent, reviewable, and harder to misuse.

Key Derivation Functions Explained

A Key Derivation Function (KDF) is a cryptographic function designed to safely turn one secret into many independent keys. In plain terms, a KDF takes a single piece of secret data and derives new keys from it in a controlled and secure way, instead of reusing the same secret everywhere.

The important idea is separation. Even though all derived keys originate from the same secret, a KDF ensures that each derived key behaves as if it were generated independently. This is what allows us to use one master secret for encryption, authentication, sessions, or tokens without those uses interfering with each other.

A KDF works by combining three main inputs. The first is the secret, sometimes called input key material. This is the original value we already have, such as a password-derived secret, a shared secret from a handshake, or a randomly generated master key. The KDF does not assume this secret is perfect; part of its job is to strengthen and normalize it.

The second input is the salt. The salt is an additional value mixed into the derivation process to add randomness and uniqueness. Its role is to make attacks harder, especially offline guessing attacks. Even if two systems start with the same secret, using different salts ensures they derive different keys. The salt also protects against precomputed attacks and makes large-scale compromises much less practical.

The third input is info, also called context. This is where key separation is enforced. The info value describes what the key is for, such as “encryption”, “auth”, or “session”. By changing only the context, we can safely derive multiple keys from the same secret without linking them together in a dangerous way.

The output of a KDF is derived key material—a sequence of bytes that can be used directly as cryptographic keys. We can request exactly the length we need, whether that is 16 bytes for AES-128, 32 bytes for AES-256, or any other size required by our algorithms. Importantly, the output is designed to look random and independent, even when multiple keys are derived from the same inputs.

In short, a KDF gives us a safe, repeatable, and standardized way to turn one secret into many purpose-specific keys. This is the foundation that Java 25’s new KDF API builds on.

Why a Standard KDF API Matters (Even If Libraries Already Exist)

Bouncy Castle or other crypto libraries also “derive a key from (secret + salt + info)”. The difference is not that Bouncy Castle can’t do HKDF. The difference is where the capability lives, how consistent it is, and how hard it is to misuse across teams.

1. Standard API vs “every team’s wrapper” – With Bouncy Castle, we (or each team) usually end up writing our own deriveKey(…) wrapper. Another team writes a slightly different wrapper. Over time, we get:

  • different defaults (salt null vs non-null)
  • different output sizes (16 vs 32)
  • different label conventions (“auth” vs “AUTH”, “encryption” vs “enc”)
  • different encodings (UTF-8 vs platform default)
  • different algorithms (HKDF-SHA256 vs HKDF-SHA512)

So the algorithm is the same, but the project-to-project behavior becomes inconsistent.

With Java 25’s KDF API, we get a platform standard way to express key derivation, so “how we derive keys” becomes something we can keep consistent across services and teams.

2. Dependency & supply-chain stability – With Bouncy Castle, we must:

  • ship an external JAR everywhere
  • manage versions, upgrades, CVEs, compatibility, shading conflicts, etc.

With the JDK API, key derivation is built-in—no extra jar, no dependency drift between microservices.

3. Provider-based crypto (JCA/JCE integration) – Java’s security model is provider-based. A standard JDK API means:

  • We can switch providers (SunJCE, OpenJDK providers, FIPS providers, HSM-backed providers) without changing our code style much
  • policies and compliance requirements (like FIPS mode) are easier to enforce centrally
  • It fits the same way we already use Cipher, Mac, MessageDigest, etc.

Bouncy Castle code is a “direct library API”. It works, but it’s not the same as a standard JCA primitive integrated into the platform security ecosystem.

4. Less room for “crypto-design decisions” in app code – In practice, the biggest problem wasn’t ‘Bouncy Castle doesn’t work’. It was:

  • Developers choose salt=null because it still runs
  • picking key lengths randomly
  • Inventing labels inconsistently
  • mixing up byte encodings
  • reusing derived bytes for multiple purposes

A platform KDF API can guide usage patterns, encourage correct separation, and make code reviews easier because everyone uses the same abstraction.

5. Consistent behavior across the Java ecosystem – When something is in the JDK:

  • frameworks, libraries, and security tooling can rely on it.
  • documentation, examples, and best practices converge
  • The ecosystem stops re-inventing wrappers

That’s the real win: not new cryptography, but standardization + reduced misuse + reduced fragmentation.

HKDF in Practice: Extract, Expand, Salt & Info

Now that we understand what a Key Derivation Function is, let us look at the most widely used and well-studied KDF in practice: HKDF (HMAC-based Key Derivation Function). HKDF is not just a hash applied multiple times; it is a carefully designed two-step process that exists specifically to solve the problems we discussed earlier.

HKDF takes one secret and safely turns it into multiple independent keys. What makes HKDF special is that it separates concerns instead of mixing everything into a single operation. It does this using two distinct phases: Extract and Expand. Each phase has a clear responsibility, which makes the overall design robust and hard to misuse when implemented correctly.

1. The Extract Step – The Extract step is responsible for cleaning up the original secret. In real systems, the input secret may not be perfectly random. It might come from a password, configuration, or a handshake that does not guarantee uniform randomness.

During extraction, HKDF combines the secret with a salt and runs it through a secure HMAC operation. The output of this step is called a pseudorandom key (PRK). The important point is that after extraction, we now have a fixed-length, high-quality key that is safe to use as the foundation for further derivation.

Conceptually, we can think of extraction as normalizing the secret into something cryptographically strong.

2. The Expand Step – The Expand step is where key separation actually happens. Using the pseudorandom key from the extract phase, HKDF generates one or more derived keys. This step takes three inputs:

  • the extracted key (PRK)
  • a context value called info
  • the desired output length

Each time we call the expand step with a different info value, HKDF produces a different derived key, even though the underlying secret remains the same. This is how HKDF allows one secret to safely serve many responsibilities.

3. What the Salt Does – The salt adds uniqueness and protection against large-scale attacks. Even if two systems accidentally use the same secret, different salts ensure that their derived keys are completely different. Salt also makes offline guessing attacks significantly harder, because attackers cannot reuse precomputed results.

An important detail is that salt does not have to be secret. Its purpose is not secrecy, but randomness and separation. When salt is omitted or reused carelessly, HKDF still works, but some of its strongest security guarantees are weakened.

4. What Info (Context) Does – The info parameter is what enforces key separation. It tells HKDF what the derived key is meant for. For example, we might use:

  • “encryption” for data encryption keys
  • “auth” for authentication keys
  • “session” for session-related keys

Even though these keys come from the same secret and the same salt, different context values ensure they are cryptographically independent.

Why Different Info Produces Different Keys

This is a critical point. When we change only the info value, HKDF deliberately changes the internal computation path. As a result, the output keys have no meaningful mathematical relationship to each other. Learning one derived key gives no practical advantage in guessing another.

This property is exactly what naive derivation schemes fail to provide. Simply hashing a secret with different labels creates related outputs. HKDF’s expand step is explicitly designed to avoid that problem.

Even though only the info value changes, HKDF treats that change as a cryptographic input, not a label. Internally, HKDF first extracts a strong intermediate key and then uses info to drive completely separate HMAC computations.

PRK = HMAC(salt, masterSecret)

encryptionKey = HMAC(PRK, "encryption" || 0x01)
authKey       = HMAC(PRK, "auth"       || 0x01)

Even though “encryption” and “auth” are short strings, HMAC treats them as full cryptographic inputs, not identifiers. A one-byte change in input completely randomizes the output.

This means:

  • The internal HMAC computation path is different
  • The resulting keys are statistically independent
  • There is no shortcut from one output to another

What JEP 510 Brings to Java

With Java 25, JEP 510 finally makes key derivation a first-class citizen in the Java platform. Instead of relying on ad-hoc utilities or third-party libraries, we now get a standard, well-defined API that fits naturally into Java’s existing cryptography ecosystem.

At the center of JEP 510 is the new javax.crypto.KDF API. This abstraction gives us a clear, consistent way to derive keys without exposing low-level cryptographic details. Rather than manually wiring together hash functions, HMACs, and byte arrays, we express intent: we provide a secret, a salt, a context, and the required output length, and the KDF handles the rest safely.

The design follows Java’s long-standing provider-based model. Just like Cipher, Mac, or MessageDigest, KDF implementations are supplied by security providers. This means we are not locked into a single implementation. Different providers can offer optimized, compliant, or hardware-backed versions of KDFs without changing application code. This is especially important for environments with strict compliance requirements, such as FIPS or HSM-backed deployments.

Out of the box, Java 25 includes a built-in HKDF implementation, which is the most widely recommended and reviewed KDF in modern cryptography. HKDF is a natural fit because it cleanly enforces extraction, expansion, salt usage, and context-based key separation — all the concepts we discussed earlier. Having HKDF directly in the JDK removes the need for external libraries for the most common key-derivation use cases.

Just as important, the API is future-proof. The KDF abstraction is not limited to HKDF. As cryptographic research evolves, new and improved key derivation algorithms can be added by providers and exposed through the same API. Our application code does not need to change — we simply select a different algorithm when needed.

In short, JEP 510 does not invent new cryptography. It standardizes a critical capability, integrates it into Java’s security architecture, and gives us a safe, consistent foundation for key derivation going forward.

Hands-On Example: One Secret → Multiple Keys

In practice, we usually start with one input secret (IKM / master secret) and then derive multiple independent outputs by changing only the info/context. JEP 510 supports two useful output styles:

#deriveKey(…) → returns a SecretKey (great when we want to plug it into Cipher, Mac, etc.)

#deriveData(…) → returns raw bytes (useful for non-key material, or when we want to wrap bytes ourselves)

Below is one clean example that does both.

package com.javahandson.jep510;

import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;

public class Jep510_HkdfOneSecretManyKeys {

    public static void main(String[] args) throws Exception {
        // 1) One starting secret (IKM). In real systems this could come from a handshake, config, etc.
        byte[] ikm = new byte[32];
        new SecureRandom().nextBytes(ikm);

        // 2) Salt (doesn't have to be secret, but should be unique/random per environment/session)
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);

        // 3) Our KDF engine (HKDF with SHA-256)
        KDF hkdf = KDF.getInstance("HKDF-SHA256");

        // A) Derive an AES key as SecretKey using deriveKey(...)
        byte[] encInfo = "payments-service|v1|encryption".getBytes(StandardCharsets.UTF_8);

        AlgorithmParameterSpec encSpec = HKDFParameterSpec.ofExtract()
                .addIKM(ikm)
                .addSalt(salt)
                .thenExpand(encInfo, 32); // 32 bytes = 256-bit key material

        SecretKey aesKey = hkdf.deriveKey("AES", encSpec);

        // B) Derive raw bytes using deriveData(...) (e.g., bytes we want to use as an HMAC key)
        byte[] authInfo = "payments-service|v1|auth".getBytes(StandardCharsets.UTF_8);

        AlgorithmParameterSpec authSpec = HKDFParameterSpec.ofExtract()
                .addIKM(ikm)
                .addSalt(salt)
                .thenExpand(authInfo, 32); // 32 bytes for HMAC-SHA256 key material

        byte[] hmacKeyBytes = hkdf.deriveData(authSpec);

        System.out.println("AES Key (SecretKey, Base64): " +
                Base64.getEncoder().encodeToString(aesKey.getEncoded()));

        System.out.println("HMAC Key (raw bytes, Base64): " +
                Base64.getEncoder().encodeToString(hmacKeyBytes));
    }
}
Output:
AES Key (SecretKey, Base64): uVdSsXu+zLxj5+QhvGeJIGuR4Xfui+w9ZjaSTH5k/FI=
HMAC Key (raw bytes, Base64): Tf8hAJObzs16Dj8PfmGgtvgyN0PEOio3oPFnm8b1eVA=

What we should notice here is that IKM + salt stays the same, and only info changes (“…|encryption” vs “…|auth”). That single change is enough for HKDF to produce different key material, giving us clean key separation without inventing our own crypto utilities.

When to Use KDF (and When Not To)

Key Derivation Functions are powerful, but like all cryptographic tools, they are effective only when used in the right context. Understanding when a KDF should be used—and when it should not—is just as important as knowing how it works.

Security Benefits of Using a KDF

The primary security advantage of a KDF is safe key separation. Instead of reusing a single secret for multiple cryptographic purposes, a KDF allows us to derive independent keys in a controlled and mathematically sound way. This dramatically reduces blast radius: if one derived key is compromised, the others remain secure.

A proper KDF also enforces good cryptographic structure. It normalizes weak or imperfect secrets, incorporates salt to resist large-scale attacks, and uses context (info) to ensure keys are purpose-bound. Most importantly, it removes the need for developers to invent their own derivation logic—one of the most common sources of cryptographic vulnerabilities.

When Should We Use a KDF

We should use a KDF whenever we already have a secret and need more than one key from it. Typical examples include:

  • deriving encryption and authentication keys from a single handshake secret
  • generating per-service or per-session keys from a master secret
  • producing keys for tokens, cookies, or secure storage from a shared root secret

In short, if the question is “We have one secret—how do we safely get many keys?”, a KDF is the correct answer.

When a KDF Is Not the Right Tool

A KDF like HKDF is not meant to protect low-entropy secrets such as raw user passwords. If we feed a simple password directly into HKDF, an attacker can still perform fast offline guessing attacks. HKDF assumes the input secret already has reasonable entropy or comes from a secure process like a handshake or random generator.

This is where confusion often arises.

Clear Distinction: HKDF ≠ PBKDF2

HKDF and PBKDF2 solve different problems.

  • HKDF is designed for key expansion and separation. It is fast and intended for secrets that are already strong.
  • PBKDF2 is designed for password hardening. It is intentionally slow and uses repeated hashing to make brute-force attacks expensive.

If we are starting from a user password, PBKDF2 (or similar password-based KDFs) should be used first. The output of PBKDF2 can then act as a strong secret, from which HKDF can derive multiple keys if needed.

Confusing these two is a common mistake. HKDF does not replace PBKDF2, and PBKDF2 does not replace HKDF—they complement each other.

How KDFs Help Prevent Misuse

By providing a clear, standardized API, KDFs reduce the temptation to “just hash something and hope it’s fine.” They encode best practices directly into the design: explicit inputs, explicit context, explicit output sizes. This makes cryptographic intent clearer in code reviews and far harder to misuse accidentally.

That is exactly why having a built-in KDF API in Java matters. It nudges us toward correct-by-design cryptography, instead of relying on discipline and tribal knowledge.

Common Mistakes, Takeaways & FAQs

Common Mistakes

Even with a solid KDF like HKDF, mistakes usually happen around how it is used rather than what it is. A very common error is treating HKDF as a simple hashing trick—changing labels or concatenating strings and assuming key separation is achieved. Another frequent issue is skipping or misusing salt, either by hard-coding it everywhere or ignoring it completely without understanding the trade-offs.

We also often see confusion between raw bytes and keys. Developers sometimes derive bytes and reuse them across multiple algorithms, or derive a key once and reuse it for encryption, authentication, and sessions. This defeats the entire purpose of key separation. Finally, using HKDF directly on low-entropy inputs like user passwords is a serious mistake; HKDF is not designed to slow down attackers.

Key Takeaways

The biggest takeaway is simple: one secret can safely serve many purposes—but only if we derive keys correctly. HKDF gives us a proven structure (extract + expand) that enforces separation, resists common attacks, and removes guesswork from cryptographic design.

JEP 510 brings this capability into the Java platform itself. That means fewer ad-hoc utilities, fewer inconsistent conventions across teams, and much less room for accidental misuse. We stop writing crypto glue code and start expressing intent clearly: what secret we have, what the key is for, and how much material we need.

FAQs

Is HKDF encryption?

No. HKDF does not encrypt or decrypt data. It only derives keys. The derived keys are then used with encryption algorithms like AES or authentication algorithms like HMAC.

Is salt mandatory?

Salt is strongly recommended but not always strictly required. Using salt adds protection against large-scale and precomputed attacks and ensures uniqueness across systems. Omitting it weakens some security guarantees, even though HKDF will still function.

Can we use HKDF for passwords?

No. HKDF is not meant to protect raw user passwords. Passwords should first be processed using a password-based KDF like PBKDF2. The output of that process can then be safely fed into HKDF if multiple keys are needed.

Conclusion

JEP 510 closes a long-standing gap in the Java platform by making key derivation a first-class, standardized capability. Instead of relying on custom utilities, fragile conventions, or third-party libraries used differently across teams, we now have a clear, provider-based API that fits naturally into Java’s existing cryptography model.

Throughout this article, we saw why reusing one secret for many purposes is dangerous, how naive derivation breaks key separation, and why HKDF’s extract–expand design matters. JEP 510 does not invent new cryptography—it codifies proven best practices and makes them easier to apply correctly and consistently.

With javax.crypto.KDF, built-in HKDF support, and a future-ready design, Java 25 helps us write cryptographic code that is clearer to read, easier to review, and harder to misuse. In practice, this means fewer subtle security bugs, less fragmentation across services, and stronger foundations for modern Java applications.

In short, JEP 510 nudges us away from “crypto done by convention” and toward crypto done by design—and that is a meaningful step forward for the Java ecosystem.

Leave a Comment