Cryptography
● Intermediate5 minBefore this: BytesSpec 14.19
std.crypto hashes, signs and seals data, and stores passwords safely. It comes from vetted libraries, and needs no capability.
Passwords
Never store a password itself. Store its hash, and check a login against it. hash_password uses Argon2id with a new salt each time, so the same password never gives the same text twice:
use std.crypto
fn main() {
const stored = crypto.hash_password("correct horse")
print(stored.starts_with("$argon2id$"))
print(crypto.check_password("correct horse", stored))
print(crypto.check_password("wrong", stored))
print(crypto.hash_password("correct horse") == stored)
}The program ran and printed:
kavach run passwords.kav
truetruefalsefalse
Sealing data
encrypt seals data with AES-256-GCM. Only the same key opens it. Another key, or sealed data that was changed even by one bit, fails with Tampered instead of giving wrong data. A key of the wrong size fails with BadKey:
use std.crypto
fn open(key: Bytes, sealed: Bytes) {
match crypto.decrypt(key, sealed) {
ok(data) => print("opened: {data.text() ?? ""}")
err(CryptoError.Tampered) => print("refused: changed, or another key")
err(CryptoError.BadKey(reason)) => print("bad key: {reason}")
}
}
fn main() ! CryptoError {
const key = crypto.aes_key()
const sealed = try crypto.encrypt(key, "card 4242".bytes())
open(key, sealed)
open(crypto.aes_key(), sealed)
open(Bytes([1, 2, 3]), sealed)
}The program ran and printed:
kavach run seal.kav
opened: card 4242refused: changed, or another keybad key: an AES key is 32 bytes
Signatures
A signature proves who wrote some data. The secret key signs; anyone with the public key can check, but can't sign:
use std.crypto
fn main() ! CryptoError {
const signing = crypto.ed25519_secret()
const public = try crypto.ed25519_public(signing)
const signature = try crypto.ed25519_sign(signing, "order 7".bytes())
print(crypto.ed25519_verify(public, "order 7".bytes(), signature))
print(crypto.ed25519_verify(public, "order 8".bytes(), signature))
}The program ran and printed:
kavach run sign.kav
truefalse
Hashes, tokens and comparing
use std.crypto
fn main() {
print(crypto.sha256("abc".bytes()))
const key = "webhook secret".bytes()
const sent = crypto.hmac_sha256(key, "paid 30".bytes())
print(crypto.equal(crypto.hmac_sha256(key, "paid 30".bytes()), sent))
print(crypto.equal(crypto.hmac_sha256(key, "paid 3000".bytes()), sent))
print(crypto.token().len())
print(crypto.random_bytes(16).len())
}The program ran and printed:
kavach run hashes.kav
Bytes(ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad)truefalse4316
Bytesprints in hexadecimal, so the SHA-256 digest ofabcis shown as it is usually written.hmac_sha256proves a message came from someone who knows the key, as payment webhooks do.- Compare secrets with
crypto.equal, not==: it takes the same time wherever they differ, so a secret can't be guessed a byte at a time from how long the answer takes. token()gives 32 random bytes as 43 letters of URL-safe text: a session id or a reset link.
Every function
| Function | Gives |
|---|---|
crypto.sha256(data) -> Bytes, crypto.sha384(data) -> Bytes, crypto.sha512(data) -> Bytes | The SHA-256, SHA-384 or SHA-512 digest of data (Bytes). SHA-384 is what channel binding (RFC 5929) needs for a certificate signed with it |
crypto.hmac_sha256(key, data) -> Bytes | HMAC-SHA-256 of data with key |
crypto.equal(a, b) -> Bool | Whether two Bytes are the same, taking the same time wherever they differ, so a secret can't be guessed byte by byte from timing |
crypto.random_bytes(n) -> Bytes | n random bytes from the system's secure generator |
crypto.token() -> String | 32 random bytes as URL-safe base64 text: a session id, a reset link, an API key |
crypto.hash_password(password) -> String | The password hashed with Argon2id, a new salt each time, as one text ($argon2id$v=19$…) to store |
crypto.check_password(password, hash) -> Bool | Whether password is the one hash was made from; false for a hash that isn't one |
crypto.ed25519_secret() -> Bytes | A new Ed25519 secret key |
crypto.ed25519_public(key) -> Bytes ! CryptoError | The public key that goes with the secret key key |
crypto.ed25519_sign(key, data) -> Bytes ! CryptoError | The signature of data |
crypto.ed25519_verify(public, data, signature) -> Bool | Whether signature is data signed by the key public goes with |
crypto.p256_secret(), p256_public(key), p256_sign(key, data), p256_verify(public, data, signature) | The same with ECDSA on P-256 and SHA-256, as JWTs (ES256) and WebAuthn use |
crypto.rsa_verify(n, e, data, signature, pss: false) -> Bool | Whether signature is data signed with RSA and SHA-256 by the public key with modulus n and exponent e (big-endian Bytes, as a JWK's n and e decode): PKCS #1 v1.5 (RS256), or PSS (PS256) with pss: true. Keys of 2048 to 8192 bits. Only checking: OpenID Connect providers sign ID tokens this way, and a service signs its own with Ed25519 or P-256 |
crypto.aes_key() -> Bytes | A new 256-bit AES key |
crypto.encrypt(key, data) -> Bytes ! CryptoError | data sealed with AES-256-GCM: a new random nonce, then the ciphertext and its tag |
crypto.decrypt(key, sealed) -> Bytes ! CryptoError | data back; fails with Tampered when sealed was changed or the key is another |
CryptoError is BadKey(reason) for a key of the wrong kind or size, or Tampered.
Secret keys are Bytes to keep out of logs and prompts. Store them as a program stores any secret.