Install
The library is a set of packages, one per algorithm. Add the module once, then import the packages you call. A package that is not imported is not linked, so the ones you never touch cost nothing.
moon add moonbitstack/mooncrypt
Every listing below is a body for fn main in a new
project that has this module added. The output line underneath is the
printed result, not an illustration of one.
A digest
hash takes a BytesView, which is a slice.
msg[:] turns a Bytes into one without
copying it.
let msg = b"the quick brown fox"
let digest = @sha2.hash(msg[:])
println(@base16.encode(digest))
A message that does not fit in memory is written in pieces. This is the same digest, computed by SHA-512 instead, and the two halves never have to meet.
let hasher = @sha2.Hasher::new(kind=Sha512)
hasher.write(b"the quick "[:])
hasher.write(b"brown fox"[:])
println(@base16.encode(hasher.finish()))
Every hash package has the same shape — Hasher::new,
write, finish — because they all answer the
@spec.Hash trait.
A keyed digest
HMAC takes a factory rather than a digest, because it runs the hash twice with the key folded in. The factory is what lets one HMAC serve every digest in the library.
let sha256 = fn() -> &@spec.Hash { @sha2.Hasher::new() }
let tag = @hmac.mac(b"shared secret"[:], msg[:], sha256)
println(@base16.encode(tag))
KMAC is the same idea built on SHA-3's sponge, and it takes the output length as an argument, so one call produces a tag of any size.
let kmac = @kmac.mac(b"shared secret"[:], msg[:], kind=Kmac256, len=32)
println(@base16.encode(kmac))
Comparing a tag is a place where == is wrong: it stops at
the first difference, and the time it took says how much of the tag
was right. @spec.eq reads every byte.
Deriving a key
HKDF turns one shared secret into several keys. info is
what separates them: the same input under a different label is a
different key.
let key = @hkdf.key(
b"input keying material"[:],
sha256,
salt=b"salt"[:],
info=b"client write key"[:],
len=32,
)
println(@base16.encode(key))
A password is not a key. It needs a function that is deliberately slow
and deliberately hungry, and scrypt is one: this setting spends
16 MiB, and @scrypt.sensitive spends 1 GiB.
let stretched = @scrypt.key(
b"correct horse"[:],
b"battery staple"[:],
params=@scrypt.Params::new(n=1024, r=8, p=1),
len=32,
)
println(@base16.encode(stretched))
Encrypting with a tag
AEAD does encryption and authentication in one pass. seal
returns the ciphertext with the tag on the end of it, and
open returns the plaintext or raises.
let boxed = @chachapoly.ChaChaPoly::new(key[:])
let nonce = Bytes::make(12, b'\x00')
let sealed_box = boxed.seal(nonce=nonce[:], plain=msg[:])
println(@base16.encode(sealed_box))
println(boxed.open(nonce=nonce[:], cipher=sealed_box[:]) == msg)
A nonce is unique per key, not secret. Reusing one with the same key is the mistake this construction cannot survive, which is why the nonce is an argument and not something the library invents.
Take one byte off the end and the message is gone — not shortened, gone.
let cut = sealed_box[0:sealed_box.length() - 1]
let opened = boxed.open(nonce=nonce[:], cipher=cut) catch { _ => b"" }
println(opened.length())
There is no partial result and no flag to check afterwards:
open either authenticates the whole ciphertext or it
raises.
Signing
An Ed25519 private key is its 32-byte seed; the public key is computed from it. Signing is deterministic, so the same key and message always produce the same signature.
let seed = @base16.decode_lossy(
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
)
let sk = @ed25519.PrivateKey::new(seed[:])
let sig = sk.sign(msg[:])
println(@base16.encode(sk.public().bytes()))
println(sk.public().verify(msg[:], sig[:]))
ECDSA has the same shape over a different curve. Its nonce comes from RFC 6979 rather than from a random source, so it is deterministic too — and a repeated nonce, which is what breaks ECDSA in the field, cannot happen.
let scalar = @base16.decode_lossy(
"c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721",
)
let ec = @ecdsa.PrivateKey::new(scalar[:])
let ec_sig = ec.sign(msg[:])
println(@base16.encode(ec.public().bytes()))
println(ec.public().verify(msg[:], ec_sig[:]))
Both implement @spec.Verifier, so a function written
against the trait verifies either without knowing which it has.
Agreeing on a secret
Two private scalars, two public points, one shared secret. The
arguments are scalars here for brevity; public is what
does the multiplication.
let alice = Bytes::make(32, b'\x11')
let bob = Bytes::make(32, b'\x22')
let secret = @ecdh.shared(alice[:], @ecdh.public(bob[:])[:])
println(@base16.encode(secret))
shared refuses a peer key that is not a point on the
curve, and one of low order, before any arithmetic touches it. Bob
calling the same function with Alice's public key gets the same
bytes.
Reading a key file
PEM is a label, base64, and two rules around it; DER is what the
base64 decodes to. armor writes one and
dearmor reads one, refusing a body that is not base64
rather than skipping the damage.
let pem = @key.armor("EC PRIVATE KEY", Bytes::make(48, b'\x42'))
let (label, der) = @key.dearmor(pem[:])
println(label)
println(der.length())
From there, parse_pkcs8, parse_spki,
parse_sec1 and the PKCS#1 pair take the DER apart into
numbers, and refuse a structure whose lengths do not add up.
Where randomness comes from
From the caller. No algorithm here reaches for an entropy source: a seed, a salt and a nonce are arguments, and the library never invents one. That is why every example on this page runs without a caveat, and why every test in the suite gives the same answer twice.
A fresh key takes a source instead: every private key type has
generate(source), and X25519, X448 and ECDH have a
generate(source) for their scalars. The source is any
moonrand
Source. Curve scalars are drawn as FIPS 186-5 A.2.2 draws
them, from the same bytes as Go's ecdsa.GenerateKey.
@system.source from moonbitstack/moonrand/system
is the platform's. When the platform has none, the call raises
Platform rather than falling back to a seeded generator;
a test hands over a Replay of fixed bytes instead.
Where to go next
The API reference lists every public type and function, grouped by package. The showcase runs three of these ideas in the browser: the avalanche of a one-bit change, a tag that refuses a touched ciphertext, and a one-time password counting down.
The source is at
moonbitstack/mooncrypt.
moon run src/examples/tour prints one line from every
family if you would rather read the output than the page.