---
title: "Cryptography"
description: "Seal data, sign and verify, hash, HMAC and make random tokens with modern algorithms only, on WebCrypto with no dependency."
section: "interfaces"
status: "Building for 0.3 · ships as a beta API"
spec: "keys"
canonical: "https://mainsdk.dev/docs/interfaces/crypto"
updated: "2026-10-28"
---

# Cryptography

> Seal data, sign and verify, hash, HMAC and make random tokens with modern algorithms only, on WebCrypto with no dependency.

Status: Building for 0.3 · ships as a beta API · Canonical: https://mainsdk.dev/docs/interfaces/crypto · Updated: 2026-10-28

`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`](https://mainsdk.dev/docs/interfaces/keys).

## At a glance

- Status: Building for 0.3 · ships as a beta API
- Phase: 1 (Core, public beta)
- Milestone: 0.3 Every interface
- Plan: free in your app (Apache-2.0)
- Spec: https://github.com/hubxero/mainsdk/blob/develop/docs/specs/keys.md
- Category: Security and privacy

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)`

In main.yaml:

```yaml
crypto:
  fips: false
```

Guarantees, whatever the driver:

- Modern algorithms only
- Keys never in code


## Drivers

Pick one in main.yaml with `crypto.driver`.

| Driver | Id in main.yaml | Default | Note |
| --- | --- | --- | --- |
| WebCrypto | `builtin` | yes |  |
| Cloud key services | `cloud-kms` |  |  |
| Vault Transit | `vault-transit` |  |  |
| libsodium | `libsodium` |  |  |

## Codes

| Code | Where | Meaning |
| --- | --- | --- |
| `MAIN-CRYPTO-0002` | check | A Main 27 crypto rule, in the family that refuses MD5, SHA-1 and DES |
| `MAIN-CRYPTO-0013` | check | `crypto.fips` is on but `crypto.module` does not name the validated module, its version and certificate number |
| `MAIN-CRYPTO-0001` | runtime |  |
| `MAIN-CRYPTO-0003` | runtime |  |
| `MAIN-CRYPTO-0010` | runtime |  |
| `MAIN-CRYPTO-0011` | runtime |  |
| `MAIN-CRYPTO-0012` | runtime |  |

## Features

| Feature | Phase | Milestone | Spec ticket |
| --- | --- | --- | --- |
| Field encryption, signing and tokens | 1 | 0.3 |  |
| Customer-managed keys | 3 | 1.2 | `hsm-keys#customer-keys` |
| Post-quantum signatures | 5 | 1.6 | `post-quantum#pq-signatures` |
| Hybrid post-quantum key exchange | 5 | 1.6 | `post-quantum#hybrid-kex` |

## MainSDK Cloud

Everything on this page is free in your app.


## Quick start

There is nothing to declare in `main.yaml`. Import `crypto` and call it.

TypeScript:

```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 token
```

Rust:

```rust
use 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`](https://mainsdk.dev/docs/interfaces/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

```ts title="app/reset/actions.ts"
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`](https://mainsdk.dev/docs/interfaces/keys).

```ts title="lib/notes.ts"
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" });   // Uint8Array
```

`aad` 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

```ts title="lib/sign.ts"
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 });   // true
```

`verify` 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

```ts title="lib/hash.ts"
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 bytes
```

An 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.

```ts title="scripts/digest.ts"
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, spec `hsm-keys`); post-quantum signatures and hybrid key exchange (phase 5, spec `post-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](https://mainsdk.dev/docs/concepts/main27).

## 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`](https://mainsdk.dev/docs/interfaces/keys) when the key must live in a key service. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## Testing

`crypto` is pure and offline, so tests call it directly.

```ts title="lib/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");
  });
});
```

## Security and privacy

- Modern algorithms only: AES-256-GCM, ed25519, SHA-2 and HKDF-SHA-256.
- Every `seal` uses a fresh random nonce. Comparison with `crypto.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

- [Encryption keys](https://mainsdk.dev/docs/interfaces/keys)
- [Secrets](https://mainsdk.dev/docs/interfaces/secrets)
- [Main 27](https://mainsdk.dev/docs/concepts/main27)
