Crypto recipe

Key derivation with HKDF

Derive one or more independent keys from a high-entropy secret using HKDF-SHA256.

HKDF turns one high-entropy secret — a Diffie-Hellman shared secret, a master key — into one or more independent keys of the length you need. The info parameter binds each derived key to a purpose, so an encryption key and a MAC key drawn from the same secret never collide.

HKDF is for high-entropy inputs. If your input is a password, HKDF is the wrong tool — use Argon2id (or PBKDF2) instead.

Get it right

  • Use HKDF to expand a high-entropy secret into subkeys — not for passwords.
  • Give each derived key a distinct info string so keys are domain-separated.
  • Salt is optional and need not be secret, but a random salt strengthens extraction.
  • Derive exactly the length you need; don't hand-truncate from a longer block.

Implementation

Setup go get golang.org/x/crypto/hkdf. (Go 1.24+ also ships a stdlib crypto/hkdf.)

Go
package main

import (
	"crypto/sha256"
	"fmt"
	"io"

	"golang.org/x/crypto/hkdf"
)

func main() {
	secret := []byte("shared secret from ECDH or a master key")
	salt := []byte("optional non-secret salt")
	info := []byte("app v1: aes-256-key") // binds the key to a context

	r := hkdf.New(sha256.New, secret, salt, info)

	key := make([]byte, 32) // derive a 256-bit key
	if _, err := io.ReadFull(r, key); err != nil {
		panic(err)
	}
	fmt.Printf("%x", key)
}

Setup pip install cryptography.

Python
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

secret = b"shared secret from ECDH or a master key"

key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,                     # 256-bit derived key
    salt=b"optional non-secret salt",
    info=b"app v1: aes-256-key",   # context binding
).derive(secret)

Setup Built in (node:crypto; hkdfSync since Node 15).

Node.js
import { hkdfSync } from 'node:crypto';

const secret = 'shared secret from ECDH or a master key';
const salt = 'optional non-secret salt';
const info = 'app v1: aes-256-key'; // binds the key to a context

// hkdfSync returns an ArrayBuffer; wrap it in a Buffer to use as a key.
const key = Buffer.from(hkdfSync('sha256', secret, salt, info, 32)); // 256-bit key

Setup Built in. System.Security.Cryptography.HKDF is .NET 5+.

.NET
using System.Security.Cryptography;
using System.Text;

byte[] secret = Encoding.UTF8.GetBytes("shared secret from ECDH or a master key");
byte[] salt = Encoding.UTF8.GetBytes("optional non-secret salt");
byte[] info = Encoding.UTF8.GetBytes("app v1: aes-256-key");

byte[] key = HKDF.DeriveKey(HashAlgorithmName.SHA256, secret, outputLength: 32, salt, info);

Setup libsodium 1.0.19+ (crypto_kdf_hkdf_sha256; not in the 1.0.18 many distros still ship).

C++
#include <sodium.h>
#include <cstring>
#include <string>

int main() {
    if (sodium_init() < 0) return 1;

    std::string secret = "shared secret from ECDH or a master key";
    const unsigned char salt[] = "optional non-secret salt";
    const char *info = "app v1: aes-256-key";

    // Extract-then-expand (RFC 5869).
    unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES];
    crypto_kdf_hkdf_sha256_extract(prk, salt, sizeof salt - 1,
        reinterpret_cast<const unsigned char *>(secret.data()), secret.size());

    unsigned char key[32];
    crypto_kdf_hkdf_sha256_expand(key, sizeof key, info, std::strlen(info), prk);
}

Setup org.bouncycastle:bcprov-jdk18on. (The JDK has no built-in HKDF as of Java 21.)

Java
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;

byte[] secret = "shared secret from ECDH or a master key".getBytes();
byte[] salt = "optional non-secret salt".getBytes();
byte[] info = "app v1: aes-256-key".getBytes();

HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest());
hkdf.init(new HKDFParameters(secret, salt, info));

byte[] key = new byte[32];
hkdf.generateBytes(key, 0, key.length);

Setup Cargo.toml: hkdf = "0.12", sha2 = "0.10".

Rust
use hkdf::Hkdf;
use sha2::Sha256;

fn main() {
    let secret = b"shared secret from ECDH or a master key";
    let salt = b"optional non-secret salt";
    let info = b"app v1: aes-256-key";

    let hk = Hkdf::<Sha256>::new(Some(salt), secret);

    let mut key = [0u8; 32];
    hk.expand(info, &mut key).expect("32 is a valid output length");
}