-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a public HKDF API, per RFC 5869. (#1163)
* Add a public HKDF API, per RFC 5869. Implemented purely from the RFC, no other code reference used. Unit tests test against all available providers to ensure consistency. Creates new Mac instances for each expand/extract operation so probably thread-safe. Probably does too much array copying but we can fix that later if it's an issue.
- Loading branch information
Showing
5 changed files
with
380 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* | ||
* Copyright (C) 2023 The Android Open Source Project | ||
* | ||
* 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 org.conscrypt; | ||
|
||
import java.security.InvalidKeyException; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.util.Objects; | ||
|
||
import javax.crypto.Mac; | ||
import javax.crypto.spec.SecretKeySpec; | ||
|
||
/** | ||
* Hkdf - perform HKDF key derivation operations per RFC 5869. | ||
* <p> | ||
* Instances should be instantiated using the standard JCA name for the required HMAC. | ||
* <p> | ||
* Each invocation of expand or extract uses a new Mac instance and so instances | ||
* of Hkdf are thread-safe. | ||
*/ | ||
public final class Hkdf { | ||
// HMAC algorithm to use. | ||
private final String hmacName; | ||
private final int macLength; | ||
|
||
/** | ||
* Creates an Hkdf instance which will use hmacName as the name for the underlying | ||
* HMAC algorithm, which will be located using normal JCA precedence rules. | ||
* <p> | ||
* @param hmacName the name of the HMAC algorithm to use | ||
* @throws NoSuchAlgorithmException if hmacName is not a valid HMAC name | ||
*/ | ||
public Hkdf(String hmacName) throws NoSuchAlgorithmException { | ||
Objects.requireNonNull(hmacName); | ||
this.hmacName = hmacName; | ||
|
||
// Stash the MAC length with the bonus that we'll fail fast here if no such algorithm. | ||
macLength = Mac.getInstance(hmacName).getMacLength(); | ||
} | ||
|
||
// Visible for testing. | ||
int getMacLength() { | ||
return macLength; | ||
} | ||
|
||
/** | ||
* Performs an HKDF extract operation as specified in RFC 5869. | ||
* | ||
* @param salt the salt to use | ||
* @param ikm initial keying material | ||
* @return a pseudorandom key suitable for use in expand operations | ||
* @throws InvalidKeyException if the salt is not suitable for use as an HMAC key | ||
* @throws NoSuchAlgorithmException if the Mac algorithm is no longer available | ||
*/ | ||
|
||
public byte[] extract(byte[] salt, byte[] ikm) | ||
throws InvalidKeyException, NoSuchAlgorithmException { | ||
Objects.requireNonNull(salt); | ||
Objects.requireNonNull(ikm); | ||
Preconditions.checkArgument(ikm.length > 0, "Empty keying material"); | ||
if (salt.length == 0) { | ||
salt = new byte[getMacLength()]; | ||
} | ||
return getMac(salt).doFinal(ikm); | ||
} | ||
|
||
/** | ||
* Performs an HKDF expand operation as specified in RFC 5869. | ||
* | ||
* @param prk a pseudorandom key of at least HashLen octets, usually the output from the | ||
* extract step. Where HashLen is the key size of the underlying Mac | ||
* @param info optional context and application specific information, can be zero length | ||
* @param length length of output keying material in bytes (<= 255*HashLen) | ||
* @return output of keying material of length bytes | ||
* @throws InvalidKeyException if prk is not suitable for use as an HMAC key | ||
* @throws IllegalArgumentException if length is out of the allowed range | ||
* @throws NoSuchAlgorithmException if the Mac algorithm is no longer available | ||
*/ | ||
public byte[] expand(byte[] prk, byte[] info, int length) | ||
throws InvalidKeyException, NoSuchAlgorithmException { | ||
Objects.requireNonNull(prk); | ||
Objects.requireNonNull(info); | ||
Preconditions.checkArgument(length >= 0, "Negative length"); | ||
Preconditions.checkArgument(length < 255 * getMacLength(), "Length too long"); | ||
Mac mac = getMac(prk); | ||
int macLength = getMacLength(); | ||
|
||
byte[] t = new byte[0]; | ||
byte[] output = new byte[length]; | ||
int outputOffset = 0; | ||
byte[] counter = new byte[] { 0x00 }; | ||
while (outputOffset < length) { | ||
counter[0]++; | ||
mac.update(t); | ||
mac.update(info); | ||
t = mac.doFinal(counter); | ||
int size = Math.min(macLength, length - outputOffset); | ||
System.arraycopy(t, 0, output, outputOffset, size); | ||
outputOffset += size; | ||
} | ||
return output; | ||
} | ||
|
||
private Mac getMac(byte[] key) throws InvalidKeyException, NoSuchAlgorithmException { | ||
// Can potentially throw NoSuchAlgorithmException if the there has been a change | ||
// in installed Providers. | ||
Mac mac = Mac.getInstance(hmacName); | ||
mac.init(new SecretKeySpec(key, "RAW")); | ||
return mac; // https://www.youtube.com/watch?v=uB1D9wWxd2w | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Copyright (C) 2023 The Android Open Source Project | ||
* | ||
* 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 org.conscrypt; | ||
|
||
import static org.junit.Assert.assertArrayEquals; | ||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertThrows; | ||
|
||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
|
||
import java.security.NoSuchAlgorithmException; | ||
import java.util.List; | ||
|
||
import javax.crypto.Mac; | ||
|
||
@RunWith(JUnit4.class) | ||
public class HkdfTest { | ||
private final String SHA256 = "HmacSHA256"; | ||
|
||
@Test | ||
public void constructor() throws Exception { | ||
assertThrows(NullPointerException.class, () -> new Hkdf(null)); | ||
assertThrows(NoSuchAlgorithmException.class, () -> new Hkdf("No such MAC")); | ||
|
||
Hkdf hkdf = new Hkdf(SHA256); | ||
assertEquals(Mac.getInstance(SHA256).getMacLength(), hkdf.getMacLength()); | ||
} | ||
|
||
@Test | ||
public void extract() throws Exception { | ||
Hkdf hkdf = new Hkdf(SHA256); | ||
assertThrows(NullPointerException.class, () -> hkdf.extract(null, new byte[0])); | ||
assertThrows(NullPointerException.class, () -> hkdf.extract(new byte[0], null)); | ||
assertThrows(NullPointerException.class, () -> hkdf.extract(null, null)); | ||
assertThrows(IllegalArgumentException.class, () -> hkdf.extract(new byte[0], new byte[0])); | ||
} | ||
|
||
@Test | ||
public void expand() throws Exception { | ||
Hkdf hkdf = new Hkdf(SHA256); | ||
int macLen = hkdf.getMacLength(); | ||
assertThrows(NullPointerException.class, () -> hkdf.expand(null, new byte[0], 1)); | ||
assertThrows(NullPointerException.class, () -> hkdf.expand(new byte[macLen], null, 1)); | ||
assertThrows(NullPointerException.class, () -> hkdf.expand(null, null, 1)); | ||
assertThrows(NullPointerException.class, () -> hkdf.expand(null, null, 1)); | ||
// Negative length | ||
assertThrows(IllegalArgumentException.class, | ||
() -> hkdf.expand(new byte[macLen], new byte[0], -1)); | ||
// PRK too small | ||
assertThrows(IllegalArgumentException.class, | ||
() -> hkdf.expand(new byte[0], new byte[0], 1)); | ||
// Length too large | ||
assertThrows(IllegalArgumentException.class, | ||
() -> hkdf.expand(new byte[macLen], new byte[0], 255 * macLen + 1)); | ||
} | ||
|
||
@Test | ||
public void testVectors() throws Exception { | ||
List<TestVector> vectors = TestUtils.readTestVectors("crypto/hkdf.txt"); | ||
|
||
for (TestVector vector : vectors) { | ||
String errMsg = vector.getString("name"); | ||
String macName = vector.getString("hash"); | ||
byte[] ikm = vector.getBytes("ikm"); | ||
byte[] salt = vector.getBytesOrEmpty("salt"); | ||
byte[] prk_expected = vector.getBytes("prk"); | ||
|
||
Hkdf hkdf = new Hkdf(macName); | ||
byte[] prk = hkdf.extract(salt, ikm); | ||
assertArrayEquals(errMsg, prk_expected, prk); | ||
|
||
byte[] info = vector.getBytes("info"); | ||
int length = vector.getInt("l"); | ||
byte[] okm_expected = vector.getBytes("okm"); | ||
|
||
byte[] okm = hkdf.expand(prk, info, length); | ||
assertArrayEquals(errMsg, okm_expected, okm); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Test vectors from RFC 5869 | ||
|
||
Name = Basic test case with SHA-256 | ||
Hash = HmacSHA256 | ||
IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b | ||
salt = 000102030405060708090a0b0c | ||
info = f0f1f2f3f4f5f6f7f8f9 | ||
L = 42 | ||
PRK = 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5 | ||
OKM = 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865 | ||
|
||
Name = Test with SHA-256 and longer inputs/outputs | ||
Hash = HmacSHA256 | ||
IKM = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f | ||
salt = 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf | ||
info = b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff | ||
L = 82 | ||
PRK = 06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244 | ||
OKM = b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87 | ||
|
||
Name = Test with SHA-256 and zero-length salt/info | ||
Hash = HmacSHA256 | ||
IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b | ||
salt = | ||
info = | ||
L = 42 | ||
PRK = 19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04 | ||
OKM = 8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8 | ||
|
||
Name = Basic test case with SHA-1 | ||
Hash = HmacSHA1 | ||
IKM = 0b0b0b0b0b0b0b0b0b0b0b | ||
salt = 000102030405060708090a0b0c | ||
info = f0f1f2f3f4f5f6f7f8f9 | ||
L = 42 | ||
PRK = 9b6c18c432a7bf8f0e71c8eb88f4b30baa2ba243 | ||
OKM = 085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896 | ||
|
||
Name = Test with SHA-1 and longer inputs/outputs | ||
Hash = HmacSHA1 | ||
IKM = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f | ||
salt = 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf | ||
info = b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff | ||
L = 82 | ||
PRK = 8adae09a2a307059478d309b26c4115a224cfaf6 | ||
OKM = 0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4 | ||
|
||
Name = Test with SHA-1 and zero-length salt/info | ||
Hash = HmacSHA1 | ||
IKM = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b | ||
salt = | ||
info = | ||
L = 42 | ||
PRK = da8c8a73c7fa77288ec6f5e7c297786aa0d32d01 | ||
OKM = 0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918 | ||
|
||
Name = Test with SHA-1, salt not provided (defaults to HashLen zero octets), zero-length info | ||
Hash = HmacSHA1 | ||
IKM = 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c | ||
info = | ||
L = 42 | ||
PRK = 2adccada18779e7c2077ad2eb19d3f3e731385dd | ||
OKM = 2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.