// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.security; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Objects; /** * Implementation of RFC-5869 HMAC-based Extract-and-Expand Key Derivation Function (HKDF). * *

The HKDF is initialized ("extracted") from a (non-secret) salt and a secret key. * From this, any number of secret keys can be derived ("expanded") deterministically.

* *

When multiple keys are to be derived from the same initial keying/salting material, * each separate key should use a distinct "context" in the {@link #expand(int, byte[])} * call. This ensures that there exists a domain separation between the keys. * Using the same context as another key on a HKDF initialized with the same salt+key * results in the exact same derived key material as that key.

* *

This implementation only offers HMAC-SHA256-based key derivation.

* * @see RFC-5869 * @see HKDF on Wikipedia * * @author vekterli */ public final class HKDF { private static final int HASH_LEN = 32; // Fixed output size of HMAC-SHA256. Corresponds to HashLen in the spec private static final byte[] EMPTY_BYTES = new byte[0]; private static final byte[] ALL_ZEROS_SALT = new byte[HASH_LEN]; public static final int MAX_OUTPUT_SIZE = 255 * HASH_LEN; private final byte[] pseudoRandomKey; // Corresponds to "PRK" in spec private HKDF(byte[] pseudoRandomKey) { this.pseudoRandomKey = pseudoRandomKey; } private static Mac createHmacSha256() { try { return Mac.getInstance("HmacSHA256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } /** * @return the computed pseudo-random key (PRK) used as input for each expand() call. */ public byte[] pseudoRandomKey() { return this.pseudoRandomKey; } /** * @return a new HKDF instance initially keyed with the given PRK */ public static HKDF ofPseudoRandomKey(byte[] prk) { return new HKDF(prk); } private static SecretKeySpec hmacKeyFrom(byte[] rawKey) { return new SecretKeySpec(rawKey, "HmacSHA256"); } private static Mac createKeyedHmacSha256(byte[] rawKey) { var hmac = createHmacSha256(); try { hmac.init(hmacKeyFrom(rawKey)); } catch (InvalidKeyException e) { throw new RuntimeException(e); } return hmac; } private static void validateExtractionParams(byte[] salt, byte[] ikm) { Objects.requireNonNull(salt); Objects.requireNonNull(ikm); if (ikm.length == 0) { throw new IllegalArgumentException("HKDF extraction IKM array can not be empty"); } if (salt.length == 0) { throw new IllegalArgumentException("HKDF extraction salt array can not be empty"); } } /** * Creates and returns a new HKDF instance extracted from the given salt and key. * *

Both the salt and input key value may be of arbitrary size, but it is recommended * to have both be at least 16 bytes in size.

* * @param salt a non-secret salt value. Should ideally be high entropy and functionally * "as if random". May not be empty, use {@link #unsaltedExtractedFrom(byte[])} * if unsalted extraction is desired (though this is not recommended). * @param ikm secret initial Input Keying Material value. * @return a new HKDF instance ready for deriving keys based on the salt and IKM. */ public static HKDF extractedFrom(byte[] salt, byte[] ikm) { validateExtractionParams(salt, ikm); /* RFC-5869, Step 2.2, Extract: HKDF-Extract(salt, IKM) -> PRK Options: Hash a hash function; HashLen denotes the length of the hash function output in octets Inputs: salt optional salt value (a non-secret random value); if not provided, it is set to a string of HashLen zeros. IKM input keying material Output: PRK a pseudorandom key (of HashLen octets) The output PRK is calculated as follows: PRK = HMAC-Hash(salt, IKM) */ var mac = createKeyedHmacSha256(salt); // Note: HKDF is initially keyed on the salt, _not_ on ikm! mac.update(ikm); return new HKDF(/*PRK = */ mac.doFinal()); } /** * Creates and returns a new unsalted HKDF instance extracted from the given key. * *

Prefer using the salted {@link #extractedFrom(byte[], byte[])} method if possible.

* * @param ikm secret initial Input Keying Material value. * @return a new HKDF instance ready for deriving keys based on the IKM and an all-zero salt. */ public static HKDF unsaltedExtractedFrom(byte[] ikm) { return extractedFrom(ALL_ZEROS_SALT, ikm); } /** * Derives a key with a given number of bytes for a particular context. The returned * key is always deterministic for a given unique context and a HKDF initialized with * a specific salt+IKM pair. * *

Thread safety: multiple threads can safely call expand() simultaneously * on the same HKDF object.

* * @param wantedBytes Positive number of output bytes. Must be less than or equal to {@link #MAX_OUTPUT_SIZE} * @param context Context for key derivation. Derivation is deterministic for a given context. * Note: this maps to the "info" field in RFC-5869. * @return A byte buffer of size wantedBytes filled with derived key material */ public byte[] expand(int wantedBytes, byte[] context) { Objects.requireNonNull(context); verifyWantedBytesWithinBounds(wantedBytes); return expandImpl(wantedBytes, context); } /** * Derives a key with a given number of bytes. The returned key is always deterministic * for a HKDF initialized with a specific salt+IKM pair. * *

If more than one key is to be derived, use {@link #expand(int, byte[])}

* *

Thread safety: multiple threads can safely call expand() simultaneously * on the same HKDF object.

* * @param wantedBytes Positive number of output bytes. Must be less than or equal to {@link #MAX_OUTPUT_SIZE} * @return A byte buffer of size wantedBytes filled with derived key material */ public byte[] expand(int wantedBytes) { return expand(wantedBytes, EMPTY_BYTES); } private void verifyWantedBytesWithinBounds(int wantedBytes) { if (wantedBytes <= 0) { throw new IllegalArgumentException("Requested negative or zero number of HKDF output bytes"); } if (wantedBytes > MAX_OUTPUT_SIZE) { throw new IllegalArgumentException("Too many requested HKDF output bytes (max %d, got %d)" .formatted(MAX_OUTPUT_SIZE, wantedBytes)); } } private byte[] expandImpl(int wantedBytes, byte[] context) { /* RFC-5869, Step 2.3, Expand: HKDF-Expand(PRK, info, L) -> OKM Inputs: PRK a pseudorandom key of at least HashLen octets (usually, the output from the extract step) info optional context and application specific information (can be a zero-length string) L length of output keying material in octets (<= 255*HashLen) Output: OKM output keying material (of L octets) The output OKM is calculated as follows: N = ceil(L/HashLen) T = T(1) | T(2) | T(3) | ... | T(N) OKM = first L octets of T where: T(0) = empty string (zero length) T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) ... */ var prkHmac = createKeyedHmacSha256(pseudoRandomKey); int blocks = (wantedBytes / HASH_LEN) + ((wantedBytes % HASH_LEN) != 0 ? 1 : 0); // N var buffer = ByteBuffer.allocate(blocks * HASH_LEN); // T byte[] lastBlock = EMPTY_BYTES; // initially T(0) for (int i = 0; i < blocks; ++i) { prkHmac.update(lastBlock); prkHmac.update(context); prkHmac.update((byte)(i + 1)); // Number of blocks shall never exceed 255 // HMAC instance can be reused across doFinal() calls; resets back to initially keyed state. lastBlock = prkHmac.doFinal(); buffer.put(lastBlock); } buffer.flip(); byte[] outputKeyingMaterial = new byte[wantedBytes]; // OKM buffer.get(outputKeyingMaterial); return outputKeyingMaterial; } }