{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/adapter-crypto",
  "slug": "adapters/adapter-crypto",
  "kind": "capabilities",
  "bucket": "package",
  "title": "crypto — applied cryptography adapter pack",
  "name": "crypto",
  "eyebrow": "applied cryptography adapter pack",
  "chip": null,
  "summary": "Crypto adapter — applied cryptography over node:crypto, plus the parameterised LEUC1 container lane from @leumas/crypt. Loaded by the adapter registry (POST /api/adapters/crypto/<fn>), and doubles as...",
  "keywords": [
    "crypto",
    "cryptography",
    "encrypt",
    "decrypt",
    "scrypt",
    "crypto api",
    "leumas crypto",
    "salt",
    "aes",
    "chacha20",
    "argon2",
    "pbkdf2",
    "leuc1",
    "container",
    "envelope",
    "classical cipher"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# crypto — applied cryptography adapter pack\n\nReal, modern cryptography for the Leumas ecosystem. The original half is built entirely on Node's\nnative `node:crypto` with no npm dependency at all; the **container lane** added for\n[encrypt.leumas.tech](https://encrypt.leumas.tech) is the adapter face of `@leumas/crypt`, the\nshared engine that also runs in a browser.\n\nThis is the \"do actual crypto\" pack: encrypt sensitive data, generate keypairs, sign & verify\npayloads, mint and validate JSON Web Tokens, derive keys from passwords, and generate secure\nsecrets — plus encrypt any file with any suite, any KDF and every cost knob exposed.\n\n> [critical] **`encrypt` and `decrypt` are frozen.** Seeded chatbot functioncalls depend on their exact\n> argument and result shapes, and the failure mode of changing them is silent — an agent keeps\n> calling the old shape and gets a result that no longer means what it did. The parameterised lane\n> is `encryptData`/`decryptData`, sitting beside them.\n\n## Tools\n\n| Tool | What it does |\n|---|---|\n| `encrypt` | AES-256-GCM authenticated encryption of text with a password. Derives a 32-byte key via scrypt from the password + a random salt, encrypts under a random IV. Returns `{ salt, iv, authTag, ciphertext }` (all base64). |\n| `decrypt` | Reverses `encrypt`. Needs the password + the `salt/iv/authTag/ciphertext`. Throws on wrong password or tampered data (GCM is authenticated). |\n| `generateKeyPair` | Generate an `rsa` (default 2048-bit), `ec` (default P-256), `ed25519`, or `ed448` keypair. Returns PEM `publicKey` + `privateKey`. |\n| `publicKeyFromPrivate` | Derive/export the PEM public key from a private key. |\n| `sign` | Digital signature of `data` with a PEM private key (RSA/EC use a hash, default sha256; Ed keys sign raw). |\n| `verify` | Verify a signature against `data` with the matching public key → `{ valid }`. |\n| `jwtSign` | Sign a JWT. HS* use a shared `secret`; RS*/PS*/ES* use a PEM `privateKey`. Auto-adds `iat`/`exp` (`expiresIn` accepts `3600`, `\"15m\"`, `\"2h\"`, `\"7d\"`). |\n| `jwtVerify` | Verify a JWT signature + `exp`/`nbf`. Throws `status:401` on invalid/expired. Supports an `algorithms` allow-list. |\n| `jwtDecode` | Decode header + payload **without** verifying (inspection only — never trust for auth). |\n| `hmac` | Keyed MAC of arbitrary data (raw or encoded key, choose hash + output encoding). |\n| `hash` | One-shot cryptographic hash of arbitrary data/bytes (sha256 default). |\n| `pbkdf2` | PBKDF2 key derivation (iterations + salt). Auto-generates + returns a salt if none given. |\n| `scrypt` | scrypt memory-hard key derivation (`N`/`r`/`p`). Auto-generates + returns a salt if none given. |\n| `randomBytes` | CSPRNG bytes in the chosen encoding (base64 default). |\n| `randomToken` | URL-safe random token (base64url) for API keys, nonces, reset tokens. |\n| `uuid` | An RFC-4122 v4 UUID from the CSPRNG. |\n| `timingSafeEqual` | Constant-time comparison of two secrets (resists timing attacks). |\n| `listHashAlgorithms` | Hash algorithms available on this Node build. |\n\n### The container lane — `@leumas/crypt`\n\n| Tool | What it does |\n|---|---|\n| `encryptData` | Encrypt bytes into a **LEUC1** container with a chosen suite (AES-128/192/256 GCM · CBC · CTR, ChaCha20-Poly1305) and KDF (scrypt · PBKDF2 · Argon2id), every cost knob exposed. The parameters are recorded IN the file, so it opens with the passphrase alone. |\n| `decryptData` | Open a LEUC1 container — or a legacy `LEUENC` file, which is still read and reported as `format: 'leuenc'`. |\n| `inspectContainer` | Read what a container says about itself **with no passphrase**: suite, KDF and costs, whether it is authenticated, whether it needs AAD, and its overhead. |\n| `listCipherSuites` | The whole catalogue with each knob's type and range, and — given a `lane` — which entries that lane can actually run, **keeping the ones it cannot** with a note saying why. |\n| `sealEnvelope` | Hybrid public-key encryption: a random content key wrapped with RSA-OAEP or ECDH-P256. No passphrase anywhere. |\n| `openEnvelope` | Open an envelope with the matching private key. |\n| `classical` | Caesar, ROT13, Atbash, Vigenère. **Not encryption**, and every result says so. |\n| `breakCaesar` | Breaks Caesar with no key, and returns all 26 shifts scored — the size of the keyspace is the lesson. |\n\n## Usage\n\nEvery tool takes ONE args object (an HTTP POST body maps 1:1). Through the registry:\n`registry.run('crypto', '<tool>', args)`.\n\n```js\nimport crypto from './index.js';\nconst { adapters } = crypto;\n\n// Encrypt then decrypt\nconst box = adapters.encrypt({ text: 'top secret', password: 'hunter2' });\nconst { text } = adapters.decrypt({ password: 'hunter2', ...box });\n// text === 'top secret'\n\n// Keypair + sign + verify\nconst { publicKey, privateKey } = adapters.generateKeyPair({ type: 'ec' });\nconst { signature } = adapters.sign({ data: 'invoice#42', privateKey });\nconst { valid } = adapters.verify({ data: 'invoice#42', signature, publicKey }); // valid === true\n\n// JWT (HS256)\nconst { token } = adapters.jwtSign({ payload: { sub: 'u_1', role: 'admin' }, secret: 's3cr3t', expiresIn: '2h' });\nconst { payload } = adapters.jwtVerify({ token, secret: 's3cr3t' });\n\n// Key derivation + secure token\nconst kdf = adapters.pbkdf2({ password: 'hunter2' }); // returns salt + key\nconst { token: apiKey } = adapters.randomToken({ length: 32 });\n\n// The container lane — a file, a chosen suite, a chosen KDF cost\nconst { container } = await adapters.encryptData({\n  data: pngBytesBase64, dataEncoding: 'base64',\n  password: 'hunter2',\n  cipher: 'chacha20-poly1305',\n  kdf: 'argon2id', kdfParams: { memoryCost: 131072, timeCost: 4 },\n});\nadapters.inspectContainer({ container });   // suite + costs, no passphrase needed\nconst { data } = await adapters.decryptData({ container, password: 'hunter2', encoding: 'base64' });\n```\n\n## DRY boundary — what does NOT live here\n\nThis pack is deliberately scoped so it does not overlap its neighbours:\n\n- **`a-text`** owns string→string primitives on **text**: `MD5Hash`, `SHA256Hash`, `SHA512Hash`,\n  a hex-digest `HMAC`, `Base64Encode`/`Base64Decode`, and classical ciphers (`CaesarCipher`,\n  `Rot13`, `VigenereEncode`/`Decode`). Those are deterministic display/obfuscation helpers with no\n  key management. Reach for a-text when you just want the hex hash of a string.\n- **`a-file-actions`** owns AES encryption of **files on disk**, at fixed parameters, writing the\n  older `LEUENC` format. This pack **reads** those files and never writes them: `LEUENC` records no\n  algorithm and no KDF cost, which is fine for a fixed backup job and fatal once the parameters are\n  the user's to choose.\n- **`@leumas/crypt`** owns the LEUC1 format, the suite/KDF catalogue and the browser lane. This\n  adapter **imports** it and does not fork it — the same engine runs in the tab and on the server,\n  and a cross-lane test compares their bytes.\n- **`crypto` (this pack)** owns **real applied cryptography on data and keys**: password-based\n  AES-256-GCM of arbitrary text (authenticated, salted, random-IV), RSA/EC/Ed keypairs, digital\n  signatures, JWTs, KDFs (PBKDF2/scrypt), and CSPRNG token/byte/compare primitives.\n\nThe small overlaps are intentional and distinct: `crypto.hmac`/`crypto.hash` are the general\ndata/bytes primitives (any key encoding, any output encoding) used to build signing flows, whereas\na-text's `HMAC`/`*Hash` are hex-only convenience wrappers for text. Use a-text for quick text\ndigests; use this pack when you're actually securing data, keys, or tokens.\n\n## Notes\n\n- Binary outputs default to base64 (base64url for `randomToken` and JWT segments). All results are\n  plain JSON-serializable objects.\n- `encrypt` uses scrypt (`N=16384, r=8, p=1`) → AES-256-GCM; the returned `salt`/`iv`/`authTag`\n  make each ciphertext self-describing for `decrypt`.\n- JWT algorithms: `HS256/384/512`, `RS256/384/512`, `PS256/384/512`, `ES256/384/512`. ES* use\n  IEEE-P1363 (r‖s) signatures per the JWT spec.\n- **Binary crosses this boundary as base64**, because an adapter result is JSON-serialisable by\n  contract. Pass `encoding: 'base64'` to `decryptData` for anything that is not text — `utf8` on\n  binary yields replacement characters and loses the file, silently. The HTTP lane at\n  `/api/encrypt` streams real bytes instead and has no such cost.\n- **`argon2id` is server-only** and its native binding (`@node-rs/argon2`) is an *optional*\n  dependency: a missing binary degrades that one KDF at call time, with a sentence naming it, rather\n  than taking the module down at import.\n- **`integrity: false`** on a non-AEAD suite is a teaching mode, not a configuration. It produces a\n  file that decrypts silently *wrong* when modified, which is the only demonstration on the site of\n  why authentication is not optional.\n",
  "source": {
    "path": "shared/engines/adapters/domain/crypto/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 9231,
    "hash": "7062a2c36f13390413ce00f411d5369b8c89f2cf"
  },
  "urls": {
    "html": "/p/adapters/adapter-crypto",
    "json": "/docs/adapters/adapter-crypto.json",
    "md": "/docs/adapters/adapter-crypto.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": null
}
