A .NET implementation of HKDF, with support for SHA256, SHA384, and SHA512.
You can find the NuGet package here.
The easiest way to install this is via the NuGet Package Manager in Visual Studio, as explained here. JetBrains Rider also has a package manager, and instructions can be found here.
- Download the latest release.
- Move the downloaded
.dll
file into your project folder. - Click on the
Project
tab andAdd Project Reference...
in Visual Studio. - Go to
Browse
, click theBrowse
button, and select the downloaded.dll
file. - Add
using HkdfDotNet;
to the top of each code file that needs the library.
- HKDF is NOT suitable for passwords. You MUST use a password-based KDF algorithm, such as Argon2 or scrypt, for such purposes. Both algorithms are available in the libsodium library.
- Although the
info
parameter is optional according to the specification, it SHOULD always be used to insert some randomness into HKDF and to bind derived keys to application- and context-specific information. Inadequate context information CAN lead to subtle vulnerabilities. The info SHOULD be independent of the input keying material (IKM). - The
salt
parameter SHOULD NOT be used. You SHOULD randomly generate one 128-bit or 256-bit salt that gets used for all derived subkeys but placed into theinfo
parameter alongside other context information using concatenation. Make sure you AVOID canonicalization attacks when doing this.
HKDF consists of three different functions:
DeriveKey()
: if the input keying material (IKM) is not distributed uniformly (e.g. a shared secret from a key exchange) and you want to derive one or more keys using the same context information for all of those keys, then you should use this function. This calls theExtract()
andExpand()
functions internally.Extract()
: if the input keying material (IKM) is not distributed uniformly (e.g. a shared secret from a key exchange) and you want to derive one or more keys using different context information for different keys, then you should call this function once to derive a pseudorandom key. Then you can callExpand()
on the output to derive multiple subkeys, as explained below.Expand()
: if the input keying material (IKM) is distributed uniformly (e.g. the output ofExtract()
or randomly generated using a cryptographically secure random number generator), then you can call this function multiple times to derive separate subkeys using different context information but the same randomly generated salt.
Here are my recommendations:
HashAlgorithmName
: use SHA512 in all cases.inputKeyingMaterial
: at least 256-bits long and MUST be high-entropy (e.g. a shared secret, NOT a password).outputLength
: at least 256-bits, which is equivalent to 32 bytes. If deriving multiple keys using one call toDeriveKey()
, then use an output length as long as the required key lengths added together (e.g. a 256-bit encryption key and a 512-bit MAC key means a 96 byte output length).salt
: leave this null to get the standard security definition for HKDF. Randomly generate one 128-bit or 256-bit salt for all subkeys and concatenate it to theinfo
parameter instead, as explained below.info
: one randomly generated 128-bit or 256-bit salt used for all subkeys, followed by a different hardcoded, globally unique, application-specific string for each subkey converted into a byte array using UTF8 encoding. The default context string format should be"[application] [date and time] [purpose]"
. You can also include things like protocol/version numbers, algorithm identifiers, and user identities. The salt and context information should be concatenated together using Array.Copy() in a way that is resistant to canonicalization attacks (please see that link and the code example below).
Here is an example of how to derive a 256-bit key for an AEAD, followed by how to derive two separate keys for Encrypt-then-MAC, using an X25519 shared secret as the input keying material (IKM), a random 128-bit salt, and hardcoded context strings:
const int saltLength = 16;
const string context = "HKDF Demo 15/11/2021 21:52 Deriving an encryption key for ChaCha20-Poly1305";
const int outputLength = 32;
// The high-entropy input keying material (NOT a password) that you want to derive subkeys from
byte[] inputKeyingMaterial = GetSharedSecret(senderPrivateKey, recipientPublicKey);
// A random 128-bit or 256-bit salt to feed into info
byte[] salt = SecureRandom.GetBytes(saltLength);
// The hardcoded, globally unique, and application-specific context information
byte[] info = Encoding.UTF8.GetBytes(context);
// Convert the length of each parameter to concatenate in info into bytes
byte[] saltLength = BitConversion.GetBytes(salt.Length);
byte[] infoLength = BitConversion.GetBytes(info.Length);
// Concatenate the salt, info, and lengths
info = Arrays.Concat(salt, info, saltLength, infoLength);
// Derive a single subkey (e.g. for encryption with an AEAD, like ChaCha20-Poly1305 or AES-GCM)
byte[] subkey = Hkdf.DeriveKey(HashAlgorithmName.SHA512, inputKeyingMaterial, outputLength, info);
// Or derive multiple subkeys (e.g. for Encrypt-then-MAC)
byte[] pseudorandomKey = Hkdf.Extract(HashAlgorithmName.SHA512, inputKeyingMaterial);
byte[] encryptionInfo = Encoding.UTF8.GetBytes("HKDF Demo 15/11/2021 21:54 ChaCha20 encryption key");
byte[] encryptionInfoLength = BitConversion.GetBytes(encryptionInfo.Length);
encryptionInfo = Arrays.Concat(salt, encryptionInfo, saltLength, encryptionInfoLength);
byte[] encryptionKey = Hkdf.Expand(HashAlgorithmName.SHA512, pseudorandomKey, outputLength, encryptionInfo);
byte[] macInfo = Encoding.UTF8.GetBytes("HKDF Demo 15/11/2021 21:55 BLAKE2b MAC key");
byte[] macInfoLength = BitConversion.GetBytes(macInfo.Length);
macInfo = Arrays.Concat(salt, macInfo, saltLength, macInfoLength);
byte[] macKey = Hkdf.Expand(HashAlgorithmName.SHA512, pseudorandomKey, outputLength * 2, macInfo);
Note that the Arrays.Concat()
function looks like this, and the BitConversion.GetBytes()
function looks like this.