Key-derivation functions in pure Standard ML, complementing the PBKDF2 already
in sml-crypto:
- HKDF (RFC 5869) — extract-then-expand over HMAC-SHA-256 / HMAC-SHA-512.
- scrypt (RFC 7914) — the memory-hard password KDF, with the Salsa20/8 core implemented in-repo and the PBKDF2-HMAC-SHA256 wrapper supplied by
sml-crypto.
HMAC / SHA / PBKDF2 come from the vendored
sml-crypto (and its bundled
sml-codec). No FFI, no external
dependencies, and deterministic — byte-identical under both
MLton and Poly/ML.
- 24 assertions, green on MLton and Poly/ML (byte-identical output).
- Vendors
sml-crypto+sml-codec(Layout B), so the repo builds standalone. - Validated against the official RFC vectors:
- RFC 5869 Appendix A — HKDF-SHA256 test cases 1, 2 and 3 (PRK and OKM, byte-exact), including the zero-length-salt/info case.
- RFC 7914 — scrypt reference vectors
("", "", N=16, r=1, p=1)and("password", "NaCl", N=1024, r=8, p=16), byte-exact (64-byte outputs).
- All inputs/outputs are raw byte strings (one char per byte, 0–255).
This release ships HKDF + scrypt. Argon2id (RFC 9106) is a planned
phase-2 milestone: Argon2 uses BLAKE2b internally and no BLAKE2b primitive yet
exists in the ecosystem (sml-blake3 is BLAKE3, a different function), so
Argon2id is intentionally deferred until a BLAKE2b primitive lands. PBKDF2
is not re-implemented here — it already lives in sml-crypto
(Pbkdf2.pbkdf2Sha256/512).
With smlpkg:
smlpkg add github.com/sjqtentacles/sml-kdf
smlpkg sync
Include the MLB from your own (it pulls in the vendored sml-crypto +
sml-codec):
local
$(SML_LIB)/basis/basis.mlb
lib/github.com/sjqtentacles/sml-kdf/... (via smlpkg)
in
...
end
This brings structure Kdf (and the vendored crypto/codec structures) into
scope.
(* all values are raw byte strings: one char per byte, 0-255 *)
(* HKDF: derive a 32-byte key from input keying material *)
val okm = Kdf.Hkdf.derive Kdf.HmacSha256
{ salt = saltBytes, ikm = ikmBytes, info = "app context v1", len = 32 }
(* or the two HKDF steps separately *)
val prk = Kdf.Hkdf.extract Kdf.HmacSha256 {salt = saltBytes, ikm = ikmBytes}
val okm' = Kdf.Hkdf.expand Kdf.HmacSha256 {prk = prk, info = "ctx", len = 64}
(* scrypt: memory-hard key derivation *)
val key = Kdf.scrypt
{ password = "correct horse", salt = "battery staple"
, n = 16384, r = 8, p = 1, dkLen = 32 }make example runs examples/demo.sml (scrypt parameters
kept small so it runs quickly):
sml-kdf demo
============
HKDF-SHA256:
PRK (extract) : 9bc354559c49cfb0108dce954481c14eb6d09c160f4e0f0a06722ef0f31f0deb
OKM (expand, 32) : 4bde14d46f1d3146e11a87d3cec53b419908be2f4aa09ee254341eff6a8bee6b
scrypt (N=256, r=8, p=1, dkLen=32):
derived key : 09cd65d28b2dd2033a31ca20fb206310c0d80ff6ebbb9c756f724c8f80c14872
datatype prf = HmacSha256 | HmacSha512
exception Kdf of string
structure Hkdf :
sig
val extract : prf -> {salt:string, ikm:string} -> string
val expand : prf -> {prk:string, info:string, len:int} -> string
val derive : prf -> {salt:string, ikm:string, info:string, len:int} -> string
end
val scrypt : {password:string, salt:string, n:int, r:int, p:int, dkLen:int}
-> string| Function | Behavior |
|---|---|
Hkdf.extract prf {salt, ikm} |
HKDF-Extract: PRK = HMAC(salt, ikm); a zero-length salt is replaced by HashLen zero bytes per RFC 5869 |
Hkdf.expand prf {prk, info, len} |
HKDF-Expand to len bytes; raises Kdf if len > 255*HashLen |
Hkdf.derive prf {salt, ikm, info, len} |
expand (extract …) in one call |
scrypt {password, salt, n, r, p, dkLen} |
RFC 7914 scrypt; n is the cost (a power of two > 1), r the block factor, p the parallelisation factor; raises Kdf on invalid parameters |
- Bytes as
string. Keys, salts, IKM, info and outputs are raw byte strings (one char per byte, 0–255), matching the rest of the sjqtentacles crypto/codec family. - scrypt's core is Salsa20/8. RFC 7914's
BlockMix/ROMixare built on the Salsa20/8 core (not SHA, not ChaCha20); it is implemented directly in this repo. SHA-256 appears only inside the outer/inner PBKDF2 wrapper, supplied bysml-crypto. - Pick parameters deliberately. scrypt is memory-hard by design; large
Ncosts both time and memory. The supplied RFCN=1024, r=8, p=16vector takes a few seconds (notably under Poly/ML).
make test # MLton
make test-poly # Poly/ML
make all-tests # both
make example # build + run examples/demo.sml
make clean
MIT — see LICENSE.