Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Standard library

Standard library

Hashing and cryptography

On this page

The hash and crypto operations hash values, encrypt and decrypt them, hash and check passwords, sign and check messages, and check tokens and one-time codes. Each job has one construction, chosen to be safe, so you never choose an algorithm.

Operation Gives Fails SQL form
hash.sha256(value) Bytes, the 32-byte SHA-256 digest of a value built on Bytes, or of a Text's UTF-8 bytes sha256(x), or sha256(convert_to(x, 'UTF8')) for text
crypto.encrypt(value, key:, context:) Bytes: XChaCha20-Poly1305 with a fresh random nonce, the format's version and the key's identity in front. value is Bytes or a Text's UTF-8; context: text bound to the result (a record's id), so a sealed value can't be moved to another row. Actions only none
crypto.decrypt(sealed, key:, context:) Bytes, trying the current key, then _PREVIOUS unreadable (wrong key or context, or altered) none
crypto.hash_password(password) PasswordHash of a Password, a built-in text type holding the PHC string: Argon2id at the parameters fixed for the release (OWASP's: 19 MiB, 2 passes, 1 lane). Actions only none
crypto.verify_password(password, hash) PasswordCheck for a Password: wrong, correct, or correct_outdated (store a fresh hash) none
crypto.hmac(value, key:) Bytes, HMAC-SHA-256 with the current key none
crypto.verify_hmac(value, mac, key:) Bool, in constant time, against the current or previous key none
crypto.sign(value, key:) Bytes, an Ed25519 signature; key: a PrivateKey setting none
crypto.public_key(key:) PublicKey, the Ed25519 public half of a PrivateKey setting, which the app may publish so others can check what it signs none
crypto.verify_signature(value, signature, key) Bool; key a PublicKey, which is not secret and may be read none
crypto.verify_jwt(token, keys:, issuer:, audience:, as:) JwtClaims, checked against a set of Jwks, each key's own algorithm only (never none, never chosen by the token): Ed25519, RS256 for an RSA key and ES256 for a P-256 key, with expiry, not-before and issued-at against clock.now, and the issuer and audience the call names, each optional; with as:, an Object of the application's, the claims it names instead, read as json.read reads them, a registered one as JwtClaims holds it; it reads the clock, so actions call it invalid_signature, expired, not_yet_valid, malformed, wrong_audience, wrong_issuer when issuer: is given, and mismatch when as: is none
crypto.read_certificate(der) Certificate, read from its DER Bytes as its PEM's DER is: a key set's x5c after encoding.from_base64, or a file's bytes. It needs no key, so a function may call it malformed none
crypto.verify_totp(code, secret) Bool for a OneTimeCode against a TotpSecret, RFC 6238 with one step either side. Waits for keys held as data none

A clinic seals each clinical note to its own record, hashes its staff's passwords, checks the webhooks its payment provider sends with the secret they share, and signs the receipts it issues. The keys are settings, each given as a call's key: and read nowhere else:

example
config {    notes_key: EncryptionKey    payments_webhook_secret: SigningKey    receipt_key: PrivateKey}
example
type ClinicalNote = Text {    max_length: 4000}type NoteId = Text {}type SealedNote = Bytes {}type StaffPassword = Password {    min_length: 12    max_length: 128}type WebhookBody = Text {    max_length: 100000}type WebhookSignature = Bytes {}type Receipt = Text {    max_length: 2000}type ReceiptSignature = Bytes {}type Genuine = Bool {}type NoteUnreadable = Failure {    kind: FailureKind.invalid_value}/// A clinical note sealed to its own record, so it can't be moved to/// another.action seal_note(note: ClinicalNote, record: NoteId) -> SealedNote {    return SealedNote(crypto.encrypt(note, key: config.notes_key, context: record))}/// A sealed note's text: another record's id, an altered note or another/// key can't open it.function open_note(sealed_note: SealedNote, record: NoteId) fails NoteUnreadable -> ClinicalNote {    var note_bytes: Bytes = attempt crypto.decrypt(        sealed_note,        key: config.notes_key,        context: record    ) {        unreadable: NoteUnreadable    }    var note_text: Text = attempt encoding.from_utf8(note_bytes) {        malformed: NoteUnreadable    }    return attempt ClinicalNote(note_text) {        max_length: NoteUnreadable    }}/// The hash a member of staff's password is stored as.action stored_password(staff_password: StaffPassword) -> PasswordHash {    return crypto.hash_password(staff_password)}/// Whether a password signs in; `correct_outdated` asks for a fresh hash.function sign_in_check(staff_password: StaffPassword, stored_hash: PasswordHash) -> PasswordCheck {    return crypto.verify_password(staff_password, stored_hash)}/// The signature on a webhook the clinic sends to the payment provider.function webhook_signature(body: WebhookBody) -> WebhookSignature {    return WebhookSignature(crypto.hmac(body, key: config.payments_webhook_secret))}/// Whether a webhook came from the payment provider, which shares the/// secret.function from_provider(body: WebhookBody, provider_signature: WebhookSignature) -> Genuine {    return Genuine(        crypto.verify_hmac(body, provider_signature, key: config.payments_webhook_secret)    )}/// A receipt's signature, which anyone with the clinic's public key can/// check.function receipt_signature(receipt: Receipt) -> ReceiptSignature {    return ReceiptSignature(crypto.sign(receipt, key: config.receipt_key))}/// The clinic's public key, which it publishes so anyone can check its/// receipts.function clinic_public_key() -> PublicKey {    return crypto.public_key(key: config.receipt_key)}/// Whether a receipt was signed with the private half of `issuer_key`.function receipt_from(    receipt: Receipt,    claimed_signature: ReceiptSignature,    issuer_key: PublicKey) -> Genuine {    return Genuine(crypto.verify_signature(receipt, claimed_signature, issuer_key))}

A test run makes its own throwaway keys, and its nonces and salts come from the test's seed, as random's values do. The older hash is the same password at 4 MiB and 3 passes, and the signature is RFC 8032's second test vector, the message r under its key:

example
test "a sealed note opens only for its own record" {    var sealed_copy: SealedNote = seal_note("Allergic to penicillin.", "note-1")    var read_back: ClinicalNote = attempt open_note(sealed_copy, "note-1")    assert read_back == "Allergic to penicillin."    assert open_note(sealed_copy, "note-2") fails NoteUnreadable    assert seal_note("Allergic to penicillin.", "note-1") != sealed_copy}test "a password signs in, and an older hash asks for a fresh one" {    var stored_hash: PasswordHash = stored_password("correct horse battery staple")    assert sign_in_check("correct horse battery staple", stored_hash) == PasswordCheck.correct    assert sign_in_check("correct horse battery stapler", stored_hash) == PasswordCheck.wrong    assert sign_in_check(        "correct horse battery staple",        "$argon2id$v=19$m=4096,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$VTsSHiMxkq48zEo8LUSymjr7JjaRSgsxGX2Z8HQE9Cw"    ) == PasswordCheck.correct_outdated}test "a webhook is checked against its signature" {    var body: WebhookBody = "{\"paid\": true}"    assert from_provider(body, webhook_signature(body))    assert not from_provider("{\"paid\": false}", webhook_signature(body))}test "a receipt's signature checks with its issuer's public key" {    var rfc_signature_bytes: Bytes = attempt encoding.from_hex(        "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"    ) {        malformed: NoteUnreadable    }    var rfc_signature: ReceiptSignature = ReceiptSignature(rfc_signature_bytes)    assert receipt_from("r", rfc_signature, "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw")    assert not receipt_from("R", rfc_signature, "PUAXw-hDiVqStwqnTRt-vJyYLM8uxJaMwM1V8Sr0Zgw")    assert text.length(encoding.to_hex(receipt_signature("r"))) == 128}test "what the clinic signs checks with the key it publishes" {    var paid_signature: ReceiptSignature = receipt_signature("Paid in full.")    assert receipt_from("Paid in full.", paid_signature, clinic_public_key())    assert not receipt_from("Paid in part.", paid_signature, clinic_public_key())}

One construction for each job

A release that changes a construction keeps reading the old format, by the sealed value's version byte or the password hash's PHC string, and correct_outdated tells your app to hash a password again when its owner signs in. Identity leaves checking a password to your application, which is why it needs these operations.

Keys are settings that code never reads

Code never reads a secret (Secrets), and a key is no exception. A key is an opaque value, held in a setting:

  • an EncryptionKey setting (32 random bytes) for encrypt and decrypt;
  • a SigningKey setting (Signing keys) for hmac;
  • a PrivateKey setting (an Ed25519 key's 32 bytes) for sign and public_key.

You write each key by name, key: config.<name>, and nowhere else. That makes a crypto operation's key: one of the places the compiler owns, alongside those Secrets lists. A key given by position, or read anywhere else, is still configuration.secret_flow, whose message names the one place the key goes. A key: given anything but a setting, such as a parameter or text, is type.mismatch.

Reading a key anywhere but a key: is configuration.secret_flow, even to take a fingerprint of it:

example
type KeyFingerprint = Bytes {}config {    notes_key: EncryptionKey}/// A fingerprint of the notes key, to tell in a log which key is in use.function key_fingerprint() -> KeyFingerprint {    return KeyFingerprint(hash.sha256(config.notes_key))}
text
error: `config.notes_key` is a key --> main.jadpo:9:39  |9 |     return KeyFingerprint(hash.sha256(config.notes_key))  |                                       ^^^^^^^^^^^^^^^^ a key, read here  reason: An `EncryptionKey` is opaque: code never reads, prints, stores, compares or passes on a key, so the one place it goes is a `crypto` call's `key:`, which the compiler owns.  next: Give `config.notes_key` only as a `crypto` call's `key:`, written `key: config.notes_key` (agent)  rule: configuration.secret_flow  help: https://jadpo.dev/docs/errors/configuration.secret_flowcheck failed: 1 error, 1 file

An EncryptionKey rotates as a SigningKey does, through a NAME_PREVIOUS setting. A PrivateKey reads no _PREVIOUS: whoever checks a signature holds the public key, so rotating one is the verifiers' job.

An EncryptionKey or PrivateKey setting's environment variable holds its 32 bytes as unpadded Base64url, 43 characters. Anything else is refused at startup, by the check before the app starts (Checking values before the app starts). hmac and verify_hmac use a SigningKey's text bytes directly as the HMAC key, so an outside system that shares the secret can check what your app signs.

A key is never a value

EncryptionKey, PrivateKey and SigningKey are built-in secret types. Until keys held as data arrive (Language gaps), only a setting holds one. So a key has no text, no JSON form (for json.write and json.read) and no storage: the secret rule wins over the contract's "never". Code never makes a key, and building a key type from a value is type.constructor_input.

Each executor gives the operation one opaque key value, which prints as <key>. It comes from the source the plan decides for the call: the setting now, and an encrypted field once keys held as data arrive. Only the runtime turns it into the key it holds (compiler-runtime §2.5, "A key as a value").

The sealed format

crypto.encrypt writes version 1 of the sealed format: a version byte, an algorithm byte (1, for XChaCha20-Poly1305), the key's identity (4 bytes), the 24-byte random nonce, the ciphertext and its 16-byte tag. The associated data is the first six bytes and the call's context: as UTF-8, so the header, the context and the ciphertext are all authenticated.

The key's identity is the first four bytes of HMAC-SHA-256 of a fixed label under the key, which says nothing about the key. decrypt tries each of the current and the previous key whose identity the value carries, current first. Anything else is unreadable, never saying which: a value too short, another version or algorithm, an altered byte, or another context or key. An empty value seals and opens.

Password hashes

crypto.hash_password uses OWASP's first choice of Argon2id parameters: 19,456 KiB, 2 passes, 1 lane, version 19, a 16-byte salt from the invocation's random bytes and a 32-byte hash. It writes the reference PHC string ($argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>, in standard Base64 without padding). A hash checks correct_outdated when its memory, passes, lanes or hash length differs from the release's.

A PasswordHash reads only Argon2id at version 19, in its one spelling, with bounded costs: at most 2 GiB (RFC 9106's costliest recommendation), 16 passes and 16 lanes, a salt of 8 to 64 bytes and a hash of 16 to 64. So a hash from outside can't make a sign-in allocate without bound. verify_password reads the hash again and gives wrong for anything else, never a fault. A PasswordHash is stored as its PHC string and never leaves the server (type.password_hash_exposed).

PasswordCheck is a standard enum, written and stored as its variant's name.

Signatures

A PublicKey is an Ed25519 key's 32 bytes as unpadded Base64url, the same spelling as a PrivateKey setting's, and its construction checks the spelling. Signing is pure Ed25519, with no prehash and no context. verify_signature verifies strictly, by RFC 8032's checks, refusing small-order keys and non-canonical signatures. So a key that isn't a point on the curve, or a signature of another length, gives false.

verify_hmac takes the same time whichever key matches: it computes the MACs of both keys whatever the first gives, and compares each in constant time.

Keys and nonces in tests

A test run makes 32 random bytes, as Base64url, for each EncryptionKey and PrivateKey, as it makes a signing key's, while a nonce and a salt come from the test's seed. A deployable takes both from the host's generator, so no key and nonce repeat.

Digests

A digest is Bytes, and encoding writes it as text: encoding.to_hex for an ETag or a content key.

There is no MD5 or SHA-1, since both are broken. An outside system that still asks for one is reached through a package (Integrations). That includes TOTP, whose RFC 6238 default is HMAC-SHA-1 and whose per-person secret is a key held as data (Language gaps).

Encrypted fields

notes: ClinicalNote { encrypted: true } on an entity stores the field sealed with the application's field_encryption_key:, and decrypts it on load. The field is kept out of unique rules, indexes, filters, ordering, search and logs, as the table on Built-in services says of field encryption. There is one key for the application, never one per person (Encrypted fields).

encrypt, decrypt, hash_password, verify_password, hmac, verify_hmac, sign, verify_signature and public_key are built, each checked against its standard's known answers and built for WebAssembly. None reads randomness itself, and the cipher, Argon2 and Ed25519 wipe their own copies of a key when dropped. Encrypted fields aren't built yet (the roadmap).

Markdown for agents: /docs/hashing-and-cryptography.md.Built from Jadpo main at a707efb.