Sub-skills

Crypto in depth

For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill crypto`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/crypto.md verbatim with curl; don't summarize them).
Run `cubek check` after every edit until it is clean.

Hashes, MACs, passwords (bcrypt, argon2id imports), signatures (ES256, Ed25519), ECDH, HKDF and encryption (AES-GCM, XChaCha20-Poly1305): native code in crypto and encoding from "cubek" (no WebCrypto, no npm crypto on the server). Keys and nonces are Bytes; key pairs are base64url strings.

crypto.hash("sha256" | "sha512" | "sha1" | "md5", data)  → hex (sha1/md5: legacy formats only)
crypto.hmac("sha256" | "sha512" | "sha1", key, data)     → hex (sha1: TOTP)
crypto.blake2b(data, { length?: 1..64 (64), key?: ≤ 64 B }) → Bytes (libsodium generichash)
crypto.randomBytes(n) → Bytes · crypto.randomUUID() · crypto.randomInt(max) (0 ≤ n < max)
crypto.timingSafeEqual(a, b)                             → boolean (compare MACs, tokens)
crypto.password.hash(pw)                                 → "$scrypt$ln=14,r=8,p=1$..." (~50 ms)
crypto.password.verify(pw, hash)                         → boolean; also imported bcrypt $2a$/$2b$/$2y$
                                                           (cost ≤ 12) and argon2id (m ≤ 64 MiB, t ≤ 4, p ≤ 4)
                                                           8 hashes/verifies per request or task
crypto.aesGcm.encrypt(key, iv, data, aad?)               → ciphertext‖tag (16/32-byte key; 12-byte iv, never reused)
crypto.aesGcm.decrypt(key, iv, data, aad?)               → Bytes | null (null: not authentic)
crypto.xchacha20poly1305.encrypt(key, nonce, data, aad?) → ciphertext‖tag (32-byte key; 24-byte nonce, random is safe)
crypto.xchacha20poly1305.decrypt(key, nonce, data, aad?) → Bytes | null
crypto.xchacha20(key, nonce, data, { counter?: 0 })    → the plain stream, no tag (authenticate it yourself)
crypto.hkdf(ikm, salt, info, length)                     → HKDF-SHA256 Bytes (1..8160)
crypto.generateKeyPair("P-256" | "Ed25519")              → { privateKey, publicKey } (base64url raw)
crypto.sign(alg, privateKey, data)                       → base64url signature (P-256 = ES256, r‖s as in JWS)
crypto.verify(alg, publicKey, data, signature)           → boolean
crypto.ecdh(privateKey, publicKey)                       → 32-byte P-256 shared secret (feed hkdf)
crypto.toJwk(alg, publicKey, privateKey?) / crypto.fromJwk(jwk) → { alg, publicKey, privateKey | null }
encoding.base64 | base64url | hex: encode(string | Bytes) → string, decode(string) → Bytes
encoding.utf8.encode(s) → Bytes, decode(b) → string · encoding.concat([string | Bytes]) → Bytes
encoding.bytes.toArray(b) → number[] · encoding.bytes.fromArray([0..255]) → Bytes
Bytes: b.length, b[i] (0..255, read-only), b.slice(start?, end?)

P-256 keys: private 32 B, public 65 B uncompressed; Ed25519: 32 B each. Data arguments take a string (UTF-8) or Bytes. Secrets come from secrets.get(...) as base64url: encoding.base64url.decode(...).

TOTP (RFC 6238)

// src/lib/totp.ts
import { crypto, encoding, type Bytes } from "cubek"

// The secret is the raw key (base32-decode an authenticator's secret once, store Bytes).
export function totp(secret: Bytes, unixSeconds: number, digits: number = 6): string {
  const step = Math.floor(unixSeconds / 30)
  const counter = encoding.bytes.fromArray([0, 0, 0, 0, (step >>> 24) & 255, (step >>> 16) & 255, (step >>> 8) & 255, step & 255])
  const h = encoding.hex.decode(crypto.hmac("sha1", secret, counter))
  const o = h[h.length - 1] & 15
  const n = (((h[o] & 127) << 24) | (h[o + 1] << 16) | (h[o + 2] << 8) | h[o + 3]) % 10 ** digits
  return String(n).padStart(digits, "0")
}

// Accept the previous and next step too (clock drift).
export function checkTotp(secret: Bytes, code: string, unixSeconds: number): boolean {
  for (const d of [-30, 0, 30]) if (crypto.timingSafeEqual(totp(secret, unixSeconds + d), code)) return true
  return false
}

Encrypting a field

A key in a secret (cubek secrets set FIELD_KEY $(openssl rand -base64 32 | tr '+/' '-_' | tr -d =)), the nonce stored with the ciphertext:

// src/lib/seal.ts
import { crypto, encoding, secrets, type Bytes } from "cubek"

function key(): Bytes {
  const k = secrets.get("FIELD_KEY")
  if (k === null) throw new Error("FIELD_KEY is not set")
  return encoding.base64url.decode(k)
}

// base64url(nonce ‖ ciphertext ‖ tag); `aad` binds it to its row.
export function seal(plain: string, aad: string): string {
  const nonce = crypto.randomBytes(24)
  return encoding.base64url.encode(encoding.concat([nonce, crypto.xchacha20poly1305.encrypt(key(), nonce, plain, aad)]))
}

export function unseal(box: string, aad: string): string | null {
  const b = encoding.base64url.decode(box)
  const p = crypto.xchacha20poly1305.decrypt(key(), b.slice(0, 24), b.slice(24), aad)
  return p === null ? null : encoding.utf8.decode(p)
}

Imported password hashes

Users moved from PHP, Laravel, Node or Rails keep their bcrypt or argon2id hashes: verify checks them; rehash on the next sign-in to drop the old format.

// src/lib/login.ts
import { crypto } from "cubek"

export function checkPassword(pw: string, stored: string): { ok: boolean; rehash: string | null } {
  const ok = crypto.password.verify(pw, stored)
  return { ok, rehash: ok && !stored.startsWith("$scrypt$") ? crypto.password.hash(pw) : null }
}

PASETO v4.local

Encrypted tokens (blake2b + xchacha20); passes the official test vectors. The key: 32 random bytes in a secret.

// src/lib/paseto.ts
import { crypto, encoding, type Bytes } from "cubek"

const H = "v4.local."

// PAE (pre-authentication encoding): count and lengths as 64-bit little-endian.
function le64(n: number): Bytes {
  return encoding.bytes.fromArray([n & 255, (n >>> 8) & 255, (n >>> 16) & 255, (n >>> 24) & 255, 0, 0, 0, 0])
}
function pae(parts: Bytes[]): Bytes {
  const out: Bytes[] = [le64(parts.length)]
  for (const p of parts) out.push(le64(p.length), p)
  return encoding.concat(out)
}
function keys(key: Bytes, n: Bytes): { ek: Bytes; n2: Bytes; ak: Bytes } {
  const tmp = crypto.blake2b(encoding.concat(["paseto-encryption-key", n]), { key, length: 56 })
  return { ek: tmp.slice(0, 32), n2: tmp.slice(32), ak: crypto.blake2b(encoding.concat(["paseto-auth-key-for-aead", n]), { key, length: 32 }) }
}

// nonce: crypto.randomBytes(32) (fixed only in test vectors).
export function seal(key: Bytes, message: string, footer: string, implicit: string, nonce: Bytes): string {
  const k = keys(key, nonce)
  const c = crypto.xchacha20(k.ek, k.n2, message)
  const t = crypto.blake2b(pae([encoding.utf8.encode(H), nonce, c, encoding.utf8.encode(footer), encoding.utf8.encode(implicit)]), { key: k.ak, length: 32 })
  return H + encoding.base64url.encode(encoding.concat([nonce, c, t])) + (footer === "" ? "" : "." + encoding.base64url.encode(footer))
}

// The message, or null (wrong key, tampered, another footer or implicit assertion).
export function open(key: Bytes, token: string, implicit: string): string | null {
  if (!token.startsWith(H)) return null
  const parts = token.slice(H.length).split(".")
  const raw = encoding.base64url.decode(parts[0])
  if (raw.length < 64 || parts.length > 2) return null
  const footer = parts.length === 2 ? encoding.base64url.decode(parts[1]) : encoding.utf8.encode("")
  const n = raw.slice(0, 32)
  const c = raw.slice(32, raw.length - 32)
  const k = keys(key, n)
  const t = crypto.blake2b(pae([encoding.utf8.encode(H), n, c, footer, encoding.utf8.encode(implicit)]), { key: k.ak, length: 32 })
  if (!crypto.timingSafeEqual(t, raw.slice(raw.length - 32))) return null
  return encoding.utf8.decode(crypto.xchacha20(k.ek, k.n2, c))
}

JWT (HS256 and ES256)

// src/lib/jwt.ts
import { crypto, encoding } from "cubek"

const b64 = (s: string) => encoding.base64url.encode(s)

export function signHs256(claims: Record<string, string | number>, secret: string): string {
  const body = `${b64(JSON.stringify({ alg: "HS256", typ: "JWT" }))}.${b64(JSON.stringify(claims))}`
  return `${body}.${encoding.base64url.encode(encoding.hex.decode(crypto.hmac("sha256", secret, body)))}`
}

export function signEs256(claims: Record<string, string | number>, privateKey: string): string {
  const body = `${b64(JSON.stringify({ alg: "ES256", typ: "JWT" }))}.${b64(JSON.stringify(claims))}`
  return `${body}.${crypto.sign("P-256", privateKey, body)}`
}

Verify: recompute (HS256) and crypto.timingSafeEqual, or crypto.verify("P-256", publicKey, body, sig); then check exp. Web Push (VAPID, aes128gcm) is built in: push.md.