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
infostring 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.)
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.
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).
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+.
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).
#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.)
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".
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");
}