/*
* Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.services.dynamodbv2.datamodeling.internal;
import com.amazonaws.util.StringUtils;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;
/**
* HMAC-based Key Derivation Function.
*
* @see RFC 5869
*/
public final class Hkdf {
private static final byte[] EMPTY_ARRAY = new byte[0];
private final String algorithm;
private final Provider provider;
private SecretKey prk = null;
/**
* Returns an Hkdf
object using the specified algorithm.
*
* @param algorithm the standard name of the requested MAC algorithm. See the Mac section in the
*
* Java Cryptography Architecture Standard Algorithm Name Documentation for information
* about standard algorithm names.
* @return the new Hkdf
object
* @throws NoSuchAlgorithmException if no Provider supports a MacSpi implementation for the
* specified algorithm.
*/
public static Hkdf getInstance(final String algorithm) throws NoSuchAlgorithmException {
// Constructed specifically to sanity-test arguments.
Mac mac = Mac.getInstance(algorithm);
return new Hkdf(algorithm, mac.getProvider());
}
/**
* Returns an Hkdf
object using the specified algorithm.
*
* @param algorithm the standard name of the requested MAC algorithm. See the Mac section in the
*
* Java Cryptography Architecture Standard Algorithm Name Documentation for information
* about standard algorithm names.
* @param provider the name of the provider
* @return the new Hkdf
object
* @throws NoSuchAlgorithmException if a MacSpi implementation for the specified algorithm is not
* available from the specified provider.
* @throws NoSuchProviderException if the specified provider is not registered in the security
* provider list.
*/
public static Hkdf getInstance(final String algorithm, final String provider)
throws NoSuchAlgorithmException, NoSuchProviderException {
// Constructed specifically to sanity-test arguments.
Mac mac = Mac.getInstance(algorithm, provider);
return new Hkdf(algorithm, mac.getProvider());
}
/**
* Returns an Hkdf
object using the specified algorithm.
*
* @param algorithm the standard name of the requested MAC algorithm. See the Mac section in the
*
* Java Cryptography Architecture Standard Algorithm Name Documentation for information
* about standard algorithm names.
* @param provider the provider
* @return the new Hkdf
object
* @throws NoSuchAlgorithmException if a MacSpi implementation for the specified algorithm is not
* available from the specified provider.
*/
public static Hkdf getInstance(final String algorithm, final Provider provider)
throws NoSuchAlgorithmException {
// Constructed specifically to sanity-test arguments.
Mac mac = Mac.getInstance(algorithm, provider);
return new Hkdf(algorithm, mac.getProvider());
}
/**
* Initializes this Hkdf with input keying material. A default salt of HashLen zeros will be used
* (where HashLen is the length of the return value of the supplied algorithm).
*
* @param ikm the Input Keying Material
*/
public void init(final byte[] ikm) {
init(ikm, null);
}
/**
* Initializes this Hkdf with input keying material and a salt. If
* salt
is null
or of length 0, then a default salt of HashLen zeros will be
* used (where HashLen is the length of the return value of the supplied algorithm).
*
* @param salt the salt used for key extraction (optional)
* @param ikm the Input Keying Material
*/
public void init(final byte[] ikm, final byte[] salt) {
byte[] realSalt = (salt == null) ? EMPTY_ARRAY : salt.clone();
byte[] rawKeyMaterial = EMPTY_ARRAY;
try {
Mac extractionMac = Mac.getInstance(algorithm, provider);
if (realSalt.length == 0) {
realSalt = new byte[extractionMac.getMacLength()];
Arrays.fill(realSalt, (byte) 0);
}
extractionMac.init(new SecretKeySpec(realSalt, algorithm));
rawKeyMaterial = extractionMac.doFinal(ikm);
SecretKeySpec key = new SecretKeySpec(rawKeyMaterial, algorithm);
Arrays.fill(rawKeyMaterial, (byte) 0); // Zeroize temporary array
unsafeInitWithoutKeyExtraction(key);
} catch (GeneralSecurityException e) {
// We've already checked all of the parameters so no exceptions
// should be possible here.
throw new RuntimeException("Unexpected exception", e);
} finally {
Arrays.fill(rawKeyMaterial, (byte) 0); // Zeroize temporary array
}
}
/**
* Initializes this Hkdf to use the provided key directly for creation of new keys. If
* rawKey
is not securely generated and uniformly distributed over the total key-space,
* then this will result in an insecure key derivation function (KDF). DO NOT USE THIS UNLESS
* YOU ARE ABSOLUTELY POSITIVE THIS IS THE CORRECT THING TO DO.
*
* @param rawKey the pseudorandom key directly used to derive keys
* @throws InvalidKeyException if the algorithm for rawKey
does not match the
* algorithm this Hkdf was created with
*/
public void unsafeInitWithoutKeyExtraction(final SecretKey rawKey) throws InvalidKeyException {
if (!rawKey.getAlgorithm().equals(algorithm)) {
throw new InvalidKeyException(
"Algorithm for the provided key must match the algorithm for this Hkdf. Expected "
+ algorithm
+ " but found "
+ rawKey.getAlgorithm());
}
this.prk = rawKey;
}
private Hkdf(final String algorithm, final Provider provider) {
if (!algorithm.startsWith("Hmac")) {
throw new IllegalArgumentException(
"Invalid algorithm " + algorithm + ". Hkdf may only be used with Hmac algorithms.");
}
this.algorithm = algorithm;
this.provider = provider;
}
/**
* Returns a pseudorandom key of length
bytes.
*
* @param info optional context and application specific information (can be a zero-length
* string). This will be treated as UTF-8.
* @param length the length of the output key in bytes
* @return a pseudorandom key of length
bytes.
* @throws IllegalStateException if this object has not been initialized
*/
public byte[] deriveKey(final String info, final int length) throws IllegalStateException {
return deriveKey((info != null ? info.getBytes(StringUtils.UTF8) : null), length);
}
/**
* Returns a pseudorandom key of length
bytes.
*
* @param info optional context and application specific information (can be a zero-length array).
* @param length the length of the output key in bytes
* @return a pseudorandom key of length
bytes.
* @throws IllegalStateException if this object has not been initialized
*/
public byte[] deriveKey(final byte[] info, final int length) throws IllegalStateException {
byte[] result = new byte[length];
try {
deriveKey(info, length, result, 0);
} catch (ShortBufferException ex) {
// This exception is impossible as we ensure the buffer is long
// enough
throw new RuntimeException(ex);
}
return result;
}
/**
* Derives a pseudorandom key of length
bytes and stores the result in output
*
.
*
* @param info optional context and application specific information (can be a zero-length array).
* @param length the length of the output key in bytes
* @param output the buffer where the pseudorandom key will be stored
* @param offset the offset in output
where the key will be stored
* @throws ShortBufferException if the given output buffer is too small to hold the result
* @throws IllegalStateException if this object has not been initialized
*/
public void deriveKey(final byte[] info, final int length, final byte[] output, final int offset)
throws ShortBufferException, IllegalStateException {
assertInitialized();
if (length < 0) {
throw new IllegalArgumentException("Length must be a non-negative value.");
}
if (output.length < offset + length) {
throw new ShortBufferException();
}
Mac mac = createMac();
if (length > 255 * mac.getMacLength()) {
throw new IllegalArgumentException(
"Requested keys may not be longer than 255 times the underlying HMAC length.");
}
byte[] t = EMPTY_ARRAY;
try {
int loc = 0;
byte i = 1;
while (loc < length) {
mac.update(t);
mac.update(info);
mac.update(i);
t = mac.doFinal();
for (int x = 0; x < t.length && loc < length; x++, loc++) {
output[loc] = t[x];
}
i++;
}
} finally {
Arrays.fill(t, (byte) 0); // Zeroize temporary array
}
}
private Mac createMac() {
try {
Mac mac = Mac.getInstance(algorithm, provider);
mac.init(prk);
return mac;
} catch (NoSuchAlgorithmException ex) {
// We've already validated that this algorithm is correct.
throw new RuntimeException(ex);
} catch (InvalidKeyException ex) {
// We've already validated that this key is correct.
throw new RuntimeException(ex);
}
}
/**
* Throws an IllegalStateException
if this object has not been initialized.
*
* @throws IllegalStateException if this object has not been initialized
*/
private void assertInitialized() throws IllegalStateException {
if (prk == null) {
throw new IllegalStateException("Hkdf has not been initialized");
}
}
}