HKDFKeyGenerator.java
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
*/
package org.forgerock.json.crypto.simple;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.forgerock.util.Reject;
/**
* Implements the <a href="https://tools.ietf.org/rfc/rfc5869.txt">HKDF</a> key deriviation function to allow a
* single input key to be expanded into multiple component keys.
*/
final class HKDFKeyGenerator {
private static final String MASTER_KEY_ALGORITHM = "HKDF";
static final String HMAC_ALGORITHM = "HmacSHA256";
static final int HASH_LEN = 256 / 8;
/**
* Secure random generator used for generating unique salt to strength the entropy of the master key. Note that
* despite any warnings in container logs on shutdown, this will not leak memory as SecureRandom is a core JRE
* class loaded with the system classloader rather than the application classloader.
*/
private static final ThreadLocal<SecureRandom> THREAD_LOCAL_SECURE_RANDOM = new ThreadLocal<SecureRandom>() {
@Override
protected SecureRandom initialValue() {
return new SecureRandom();
}
};
/**
* The HKDF "extract" phase that generates a master key from some input key material. This method adds 128-bits
* of random salt to the derived key. This master key should not be used directly, but instead fed into
* {@link #expandKey(HKDFMasterKey, String, String, int)} to derive a specific key for a particular usage.
*
* @param inputKeyMaterial the input master key material.
* @return the derived master key.
*/
static HKDFMasterKey extractMasterKey(byte[] inputKeyMaterial) {
final byte[] salt = new byte[16];
THREAD_LOCAL_SECURE_RANDOM.get().nextBytes(salt);
return extractMasterKey(inputKeyMaterial, salt);
}
/**
* The HKDF "extract" phase that generates a master key from some input key material. This method uses the random
* salt value passed as a parameter. This master key should not be used directly, but instead fed into
* {@link #expandKey(HKDFMasterKey, String, String, int)} to derive a specific key for a particular usage.
*
* @param inputKeyMaterial the input master key material.
* @param salt the random salt to use when deriving the master key. Should be at least 128 bits and uniformly
* random.
* @return the derived master key.
*/
static HKDFMasterKey extractMasterKey(byte[] inputKeyMaterial, byte[] salt) {
Reject.ifNull(inputKeyMaterial);
Reject.ifFalse(inputKeyMaterial.length >= 16, "Input key should be at least 128 bits");
// As per the RFC, if salt is not supplied then it should be HASH_LEN zeros
if (salt == null || salt.length == 0) {
salt = new byte[HASH_LEN];
}
return new HKDFMasterKey(getHmac(new SecretKeySpec(salt, HMAC_ALGORITHM)).doFinal(inputKeyMaterial), salt);
}
/**
* Expands a master key into a derived key for a specific purpose. The key is derived by repeatedly applying
* HMAC-SHA-256 using the master key as the key and the given parameters (together with an incrementing counter) as
* input.
*
* @param masterKey the HKDF master key.
* @param outputKeyAlgorithm the algorithm for which the derived key is to be used, e.g. {@literal "AES"}.
* @param purpose an arbitrary application-specific string describing the purpose of this key (e.g. {@literal
* "OpenID Connect token signing"}.
* @param outputKeySize the output key size, in bytes. This can be between 0 and 8160 bytes.
* @return the derived key.
*/
static Key expandKey(HKDFMasterKey masterKey, String outputKeyAlgorithm, String purpose, int outputKeySize) {
return expandKey(masterKey, outputKeyAlgorithm, purpose.getBytes(StandardCharsets.UTF_8), outputKeySize);
}
/**
* Expands a master key into a derived key for a specific purpose. The key is derived by repeatedly applying
* HMAC-SHA-256 using the master key as the key and the given parameters (together with an incrementing counter) as
* input.
*
* @param masterKey the HKDF master key.
* @param outputKeyAlgorithm the algorithm for which the derived key is to be used, e.g. {@literal "AES"}.
* @param info an arbitrary application-specific byte-string to include in the key derivation.
* @param outputKeySize the output key size, in bytes. This can be between 0 and 8160 bytes.
* @return the derived key.
*/
static Key expandKey(HKDFMasterKey masterKey, String outputKeyAlgorithm, byte[] info, int outputKeySize) {
Reject.ifFalse(outputKeySize <= 255 * HASH_LEN, "Cannot derive more than " + (255 * HASH_LEN)
+ " bytes of key material");
final byte[] output = new byte[outputKeySize];
final Mac hmac = getHmac(masterKey);
int block = 1;
for (int i = 0; i < outputKeySize; i += HASH_LEN) {
if (i > 0) {
hmac.update(output, i - HASH_LEN, HASH_LEN);
}
hmac.update(info);
hmac.update((byte) block++);
System.arraycopy(hmac.doFinal(), 0, output, i, Math.min(HASH_LEN, outputKeySize - i));
hmac.reset();
}
return new SecretKeySpec(output, outputKeyAlgorithm);
}
/**
* Expands a master key into a derived key for a specific purpose. The key is derived by repeatedly applying
* HMAC-SHA-256 using the master key as the key and the given parameters (together with an incrementing counter) as
* input. This is identical to the {@link #expandKey(HKDFMasterKey, String, String, int)} method except that the
* {@code outputKeyAlgorithm} is also used as the {@code purpose} when deriving the key.
*
* @param masterKey the HKDF master key.
* @param outputKeyAlgorithm the algorithm for which the derived key is to be used, e.g. {@literal "AES"}.
* @param outputKeySize the output key size, in bytes. This can be between 0 and 8160 bytes.
* @return the derived key.
*/
static Key expandKey(HKDFMasterKey masterKey, String outputKeyAlgorithm, int outputKeySize) {
return expandKey(masterKey, outputKeyAlgorithm, outputKeyAlgorithm, outputKeySize);
}
private static Mac getHmac(Key key) {
try {
Mac hmac = Mac.getInstance(HMAC_ALGORITHM);
hmac.init(key);
return hmac;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid HKDF key", e);
}
}
/**
* A secret key designed to be used as the master key for HKDF key generation. In addition to the secret key
* material, this also has a non-secret random salt parameter.
*/
static class HKDFMasterKey extends SecretKeySpec {
private static final long serialVersionUID = 1L;
private final byte[] salt;
HKDFMasterKey(final byte[] keyBytes, final byte[] salt) {
super(keyBytes, MASTER_KEY_ALGORITHM);
this.salt = salt;
}
byte[] getSalt() {
return salt;
}
}
private HKDFKeyGenerator() {
// Utility class
}
}