Skip to content
Kavachकवच

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
passwords.kav: Kavach code, then what running it prints

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
seal.kav: Kavach code, then what running it prints

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
sign.kav: Kavach code, then what running it prints

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
hashes.kav: Kavach code, then what running it prints
  • Bytes prints in hexadecimal, so the SHA-256 digest of abc is shown as it is usually written.
  • hmac_sha256 proves 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

FunctionGives
crypto.sha256(data) -> Bytes, crypto.sha384(data) -> Bytes, crypto.sha512(data) -> BytesThe 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) -> BytesHMAC-SHA-256 of data with key
crypto.equal(a, b) -> BoolWhether 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) -> Bytesn random bytes from the system's secure generator
crypto.token() -> String32 random bytes as URL-safe base64 text: a session id, a reset link, an API key
crypto.hash_password(password) -> StringThe password hashed with Argon2id, a new salt each time, as one text ($argon2id$v=19$…) to store
crypto.check_password(password, hash) -> BoolWhether password is the one hash was made from; false for a hash that isn't one
crypto.ed25519_secret() -> BytesA new Ed25519 secret key
crypto.ed25519_public(key) -> Bytes ! CryptoErrorThe public key that goes with the secret key key
crypto.ed25519_sign(key, data) -> Bytes ! CryptoErrorThe signature of data
crypto.ed25519_verify(public, data, signature) -> BoolWhether 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) -> BoolWhether 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() -> BytesA new 256-bit AES key
crypto.encrypt(key, data) -> Bytes ! CryptoErrordata sealed with AES-256-GCM: a new random nonce, then the ciphertext and its tag
crypto.decrypt(key, sealed) -> Bytes ! CryptoErrordata 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.