Cryptography
cryptoSeal data, sign and verify, hash, HMAC and make random tokens with modern algorithms only, on WebCrypto with no dependency.
keysFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
crypto gives you everyday building blocks: seal and open, sign and verify, hash, HMAC, derive a key and make tokens, with modern algorithms only, on WebCrypto. For records with per-user erasure, use keys.
At a glance
crypto:
fips: falseimport { crypto } from "mainsdk";
const resetToken = crypto.token(); // 32 random bytes, URL-safe
const stored = await crypto.hashHex(resetToken); // keep the hash, never the tokenlib/notes.test.ts
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { crypto } from "mainsdk";
describe("crypto", () => {
it("opens only with the same key and aad", async () => {
const key = crypto.generateKey();
const box = await crypto.seal(key, "hello", { aad: "note:1" });
await assert.rejects(crypto.open(key, box, { aad: "note:2" }), { code: "MAIN-CRYPTO-0003" });
assert.equal(new TextDecoder().decode(await crypto.open(key, box, { aad: "note:1" })), "hello");
});
});Calls
crypto.keys.customer(org, { kms })cryptoApi(object: every crypto helper under one name)crypto.token(bytes?)crypto.sign(privateKey, data)crypto.verify(publicKey, { data, signature })crypto.seal(key, data, { aad? })crypto.open(key, sealed, { aad? })crypto.generateKey()crypto.deriveKey(secret, info)crypto.randomBytes(n)crypto.hash(data, alg?)crypto.hashHex(data, alg?)crypto.hmac(key, data, alg?)crypto.verifyHmac(key, data, tag, alg?)crypto.generateSigningKeyPair()crypto.equal(a, b)crypto.b64u(bytes)crypto.fromB64u(text)crypto.toHex(bytes)
Drivers
4 drivers. Select one to see its line in main.yaml.
crypto:
driver: builtinChange this one line to swap providers; your calls stay the same.Built-in and local drivers integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-CRYPTO-0002 | Checkmainsdk check, before deploy | A Main 27 crypto rule, in the family that refuses MD5, SHA-1 and DES |
MAIN-CRYPTO-0013 | Checkmainsdk check, before deploy | crypto.fips is on but crypto.module does not name the validated module, its version and certificate number |
MAIN-CRYPTO-0001 | RuntimeError responses and logs | See the spec |
MAIN-CRYPTO-0003 | RuntimeError responses and logs | See the spec |
MAIN-CRYPTO-0010 | RuntimeError responses and logs | See the spec |
MAIN-CRYPTO-0011 | RuntimeError responses and logs | See the spec |
MAIN-CRYPTO-0012 | RuntimeError responses and logs | See the spec |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Field encryption, signing and tokens | Phase 1 | 0.3 | Not specified yet |
| Customer-managed keys | Phase 3 | 1.2 | hsm-keys#customer-keys |
| Post-quantum signatures | Phase 5 | 1.6 | post-quantum#pq-signatures |
| Hybrid post-quantum key exchange | Phase 5 | 1.6 | post-quantum#hybrid-kex |
Quick start
There is nothing to declare in main.yaml. Import crypto and call it.
app/reset/token.ts
import { crypto } from "mainsdk";
const token = crypto.token(); // 32 random bytes, 43 URL-safe characters
const stored = await crypto.hashHex(token); // keep this, never the tokenuse mainsdk::prelude::*;
let app = App::test();
let pair = app.crypto().keypair().await?;
let sig = app.crypto().sign(&pair.seed, b"hello").await?;
assert!(app.crypto().verify(&pair.public_key, b"hello", &sig).await?);Configuration
None today. crypto has no main.yaml section, and the algorithms are not a free choice: only modern ones are offered. A crypto.driver for a key service such as Vault Transit or a cloud KMS is planned. Until then use keys, which has those drivers.
What you get
The calls are free, run offline on WebCrypto on Node and edge runtimes, and need no dependency. Only modern algorithms are offered, so a weak one cannot be picked by accident. You hold the keys and pass them in; nothing is stored or logged. Reach for it for a single operation such as a token, a hash or a signature. The spec is keys (AC8 to AC13).
Examples
Password reset token
import { crypto } from "mainsdk";
export async function startReset() {
const token = crypto.token();
const stored = await crypto.hashHex(token);
// save `stored` with an expiry, then send `token` to the person
return { token, stored };
}
export async function checkReset(token: string, stored: string) {
return crypto.equal(await crypto.hashHex(token), stored); // constant time
}Store only the hash of the token, with an expiry, never the token itself. crypto.token(bytes) takes 16 or more.
Seal and open data
You hold the key. For keys held in a KMS, with per-user erasure, use keys.
import { crypto } from "mainsdk";
const key = crypto.generateKey(); // 32 bytes; keep it in your secrets driver
const box = await crypto.seal(key, "hello", { aad: "note:1" });
const plain = await crypto.open(key, box, { aad: "note:1" }); // Uint8Arrayaad binds the box to a context: opening it with another aad, another key or changed bytes fails with MAIN-CRYPTO-0003. A key that is not 32 bytes fails with MAIN-CRYPTO-0001.
Sign and verify
import { crypto } from "mainsdk";
const pair = await crypto.generateSigningKeyPair(); // ed25519: PKCS#8 private key, 32-byte public key
const signature = await crypto.sign(pair.privateKey, "payload"); // 64 bytes
const ok = await crypto.verify(pair.publicKey, { data: "payload", signature }); // trueverify returns false for a changed message or another key. To keep the private key in a key service, so it never reaches your code, see keys.sign() (planned).
Hash, HMAC and derive a key
import { crypto } from "mainsdk";
const digest = await crypto.hashHex("abc"); // SHA-256; "sha384" and "sha512" are allowed
const secret = crypto.token(); // in an app, read this from your secrets driver
const tag = await crypto.hmac(secret, "body");
const good = await crypto.verifyHmac(secret, "body", tag); // false on a mismatch, never throws
const subkey = await crypto.deriveKey("a-long-enough-secret", "exports"); // HKDF-SHA-256, 32 bytesAn HMAC key needs 16 bytes or more. MD5, SHA-1 and DES are refused with MAIN-CRYPTO-0002; there is no option to pick them.
The other helpers are crypto.randomBytes(n), crypto.b64u(), crypto.fromB64u() and crypto.toHex().
Use the helpers without the app
cryptoApi is an object, not a function (the catalog lists it as cryptoApi(...)). It is what crypto forwards to: every helper above under one name. Import it when you need these helpers where no app is running, such as a script or a library.
import { cryptoApi } from "mainsdk";
const digest = await cryptoApi.hashHex("abc");Planned
- Password hashing with Argon2id. Not in the code yet. Do not use
crypto.hash()for passwords: SHA-2 is fast by design. - Field encryption with a data label. Not in the code yet. Until it lands, encrypt a value with
keys.encrypt(), which wraps a data key per value. - Drivers (
libsodium,vault-transit,cloud-kms) behind the same calls. - Customer-held keys with
crypto.keys.customer()(phase 3, spechsm-keys); post-quantum signatures and hybrid key exchange (phase 5, specpost-quantum).
Errors and checks
| Code | When | Fix |
|---|---|---|
MAIN-CRYPTO-0001 | A key is the wrong size, an input is not valid base64url, an HMAC key is under 16 bytes, or a token or byte count is out of range (400) | Use a 32-byte key from crypto.generateKey() and a long random secret from crypto.token() |
MAIN-CRYPTO-0002 | A weak hash such as MD5 or SHA-1 is asked for (400) | Use sha256, sha384 or sha512 |
MAIN-CRYPTO-0003 | A sealed box cannot be opened: wrong key, wrong aad, changed bytes or too short (400) | Open with the key and aad that sealed it, on unchanged bytes |
MAIN-CRYPTO-0010 | The platform has no implementation of the post-quantum algorithm asked for | Pick a supported algorithm or runtime (spec post-quantum) |
MAIN-CRYPTO-0011 | FIPS mode is on and the call asked for an algorithm that is not approved | Use an approved algorithm, or turn crypto.fips off |
MAIN-CRYPTO-0012 | FIPS mode is on and the platform has no FIPS-validated crypto provider | Run on a validated provider |
MAIN-CRYPTO-0013 | crypto.fips is on but crypto.module does not name the validated module (check) | Add crypto.module with name, version and certificate |
mainsdk check carries the same rule in source as MAIN-CRYPTO-0002; the rules belong to Main 27.
Swapping drivers
crypto has one driver today, the built-in WebCrypto one, so there is nothing to swap. Drivers for libsodium, vault-transit and cloud-kms are planned behind the same calls. Until then, use keys when the key must live in a key service. See Swap a driver.
Testing
crypto is pure and offline, so tests call it directly.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { crypto } from "mainsdk";
describe("crypto", () => {
it("opens only with the same key and aad", async () => {
const key = crypto.generateKey();
const box = await crypto.seal(key, "hello", { aad: "note:1" });
await assert.rejects(crypto.open(key, box, { aad: "note:2" }), { code: "MAIN-CRYPTO-0003" });
assert.equal(new TextDecoder().decode(await crypto.open(key, box, { aad: "note:1" })), "hello");
});
});Security and privacy
- Modern algorithms only: AES-256-GCM, ed25519, SHA-2 and HKDF-SHA-256.
- Every
sealuses a fresh random nonce. Comparison withcrypto.equal()takes time that depends only on length. - Keys are bytes you pass in; nothing is logged or stored by
crypto.
FAQ
When do I use crypto and when keys? keys encrypts records with their own data keys, wrapped by a KMS, with per-user keys for erasure. crypto is for single operations such as a hash, an HMAC or a signature, with keys you hold.
Can I choose SHA-1 or bcrypt? No. Only modern algorithms are offered.
Next
Was this page helpful?