/*
* Copyright (C) 2012-2016 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.facebook.nifty.ssl;
import com.google.common.io.BaseEncoding;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import static java.util.Objects.requireNonNull;
/**
* Crypto-related utilities.
*/
class CryptoUtil {
/** Length of a SHA256 hash, in bytes */
static final int SHA256_OUTPUT_BYTES = 256 / 8;
/** Maximum value for the outputLength parameter to hkdf(). */
static final int MAX_HKDF_OUTPUT_LENGTH = SHA256_OUTPUT_BYTES * 255;
/** Default all-0 salt used by HKDF if a null salt parameter is provided. */
private static final byte[] NULL_SALT = new byte[SHA256_OUTPUT_BYTES];
/** Empty byte array. */
private static final byte[] EMPTY_BYTES = new byte[0];
/** Name of the HMAC algorithm used by hkdf() function. */
private static final String MAC_ALGORITHM = "HmacSHA256";
/**
* Initializes a {@link Mac} object using the given key.
*
* @param key the HMAC key.
* @return the initialized Mac object.
* @throws IllegalArgumentException if the provided key is invalid.
*/
private static Mac initHmacSha256(byte[] key) {
try {
SecretKeySpec keySpec = new SecretKeySpec(key, MAC_ALGORITHM);
Mac mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(keySpec);
return mac;
}
catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
}
/**
* The "extract" stage of the extract-then-expand HKDF algorithm.
*
* @param salt an optional salt value. If null, will use the all-zeros constant NULL_SALT.
* @param inputKeyingMaterial input keying material. Must not be null.
* @return a pseudo-random key of length SHA256_OUTPUT_BYTES to be used by the expand() stage.
*/
private static byte[] extract(byte[] salt, byte[] inputKeyingMaterial) {
return initHmacSha256(salt == null ? NULL_SALT : salt).doFinal(inputKeyingMaterial);
}
/**
* The "expand" stage of the extract-then-expand HKDF algorithm. All arguments are required (may not be null).
*
* @param prk the pseudo-random key generated by the extract() stage.
* @param previousRoundResult the output of the previous expand() round. For the first round, it should be a
* zero-length byte array.
* @param info the info parameter to hkdf().
* @param counter the counter for the current expand stage.
* @return output keying material of length SHA256_OUTPUT_BYTES.
*/
private static byte[] expand(byte[] prk, byte[] previousRoundResult, byte[] info, byte counter) {
Mac mac = initHmacSha256(prk);
mac.update(previousRoundResult);
mac.update(info);
mac.update(counter);
return mac.doFinal();
}
/**
* The HKDF key-derivation function, as defined in RFC 5869 (see https://tools.ietf.org/html/rfc5869).
*
* @param inputKeyingMaterial the input keying material to HKDF. May not be null.
* @param salt optional salt parameter, may be null. This value does not need to be secret and can be safely
* reused between different calls to hkdf(). See the RFC for recommended practices for salt
* selection.
* @param info optional application- and/or context-specific information used to bind the derived key material
* to a particular context or use case. See the RFC for recommended practices for info parameter
* selection.
* @param outputLength desired length of the output key, in bytes. May not exceed MAX_HKDF_OUTPUT_LENGTH.
* @return a pseudo-random key of outputLength bytes long.
* @throws IllegalArgumentException if outputLength > MAX_HKDF_OUTPUT_LENGTH.
*/
static byte[] hkdf(byte[] inputKeyingMaterial, byte[] salt, byte[] info, int outputLength) {
if (outputLength > MAX_HKDF_OUTPUT_LENGTH) {
throw new IllegalArgumentException("Output length too large " + outputLength);
}
byte[] prk = extract(salt, inputKeyingMaterial);
int numRounds = (int) Math.ceil((double) outputLength / SHA256_OUTPUT_BYTES);
byte[] outputData = new byte[outputLength];
int idx = 0;
byte[] current = EMPTY_BYTES;
if (info == null) {
info = EMPTY_BYTES;
}
for (int i = 0; i < numRounds; ++i) {
current = expand(prk, current, info, (byte) (i + 1));
System.arraycopy(current, 0, outputData, idx, Math.min(SHA256_OUTPUT_BYTES, outputLength - idx));
idx += SHA256_OUTPUT_BYTES;
}
return outputData;
}
/**
* Decodes a hex-encoded string into a byte array. Accepts both lower- and upper-case [a-f] characters.
*
* @param hex the hexadecimal string to decode.
* @return the input string converted to a binary byte array.
* @throws IllegalArgumentException if the input string contains any non-hex characters, or if the length of
* the input string is not a multiple of 2.
*/
static byte[] decodeHex(String hex) {
requireNonNull(hex);
return BaseEncoding.base16().decode(hex.toUpperCase());
}
}