crypto
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...
crypto — applied cryptography adapter pack
Real, modern cryptography for the Leumas ecosystem. The original half is built entirely on Node's native node:crypto with no npm dependency at all; the container lane added for encrypt.leumas.tech is the adapter face of @leumas/crypt, the shared engine that also runs in a browser.
This is the "do actual crypto" pack: encrypt sensitive data, generate keypairs, sign & verify payloads, mint and validate JSON Web Tokens, derive keys from passwords, and generate secure secrets — plus encrypt any file with any suite, any KDF and every cost knob exposed.
[critical]encryptanddecryptare frozen. Seeded chatbot functioncalls depend on their exact argument and result shapes, and the failure mode of changing them is silent — an agent keeps calling the old shape and gets a result that no longer means what it did. The parameterised lane isencryptData/decryptData, sitting beside them.
Tools
| Tool | What it does |
|---|---|
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). |
decrypt | Reverses encrypt. Needs the password + the salt/iv/authTag/ciphertext. Throws on wrong password or tampered data (GCM is authenticated). |
generateKeyPair | Generate an rsa (default 2048-bit), ec (default P-256), ed25519, or ed448 keypair. Returns PEM publicKey + privateKey. |
publicKeyFromPrivate | Derive/export the PEM public key from a private key. |
sign | Digital signature of data with a PEM private key (RSA/EC use a hash, default sha256; Ed keys sign raw). |
verify | Verify a signature against data with the matching public key → { valid }. |
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"). |
jwtVerify | Verify a JWT signature + exp/nbf. Throws status:401 on invalid/expired. Supports an algorithms allow-list. |
jwtDecode | Decode header + payload without verifying (inspection only — never trust for auth). |
hmac | Keyed MAC of arbitrary data (raw or encoded key, choose hash + output encoding). |
hash | One-shot cryptographic hash of arbitrary data/bytes (sha256 default). |
pbkdf2 | PBKDF2 key derivation (iterations + salt). Auto-generates + returns a salt if none given. |
scrypt | scrypt memory-hard key derivation (N/r/p). Auto-generates + returns a salt if none given. |
randomBytes | CSPRNG bytes in the chosen encoding (base64 default). |
randomToken | URL-safe random token (base64url) for API keys, nonces, reset tokens. |
uuid | An RFC-4122 v4 UUID from the CSPRNG. |
timingSafeEqual | Constant-time comparison of two secrets (resists timing attacks). |
listHashAlgorithms | Hash algorithms available on this Node build. |
The container lane — @leumas/crypt
| Tool | What it does |
|---|---|
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. |
decryptData | Open a LEUC1 container — or a legacy LEUENC file, which is still read and reported as format: 'leuenc'. |
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. |
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. |
sealEnvelope | Hybrid public-key encryption: a random content key wrapped with RSA-OAEP or ECDH-P256. No passphrase anywhere. |
openEnvelope | Open an envelope with the matching private key. |
classical | Caesar, ROT13, Atbash, Vigenère. Not encryption, and every result says so. |
breakCaesar | Breaks Caesar with no key, and returns all 26 shifts scored — the size of the keyspace is the lesson. |
Usage
Every tool takes ONE args object (an HTTP POST body maps 1:1). Through the registry: registry.run('crypto', '<tool>', args).
import crypto from './index.js';
const { adapters } = crypto;
// Encrypt then decrypt
const box = adapters.encrypt({ text: 'top secret', password: 'hunter2' });
const { text } = adapters.decrypt({ password: 'hunter2', ...box });
// text === 'top secret'
// Keypair + sign + verify
const { publicKey, privateKey } = adapters.generateKeyPair({ type: 'ec' });
const { signature } = adapters.sign({ data: 'invoice#42', privateKey });
const { valid } = adapters.verify({ data: 'invoice#42', signature, publicKey }); // valid === true
// JWT (HS256)
const { token } = adapters.jwtSign({ payload: { sub: 'u_1', role: 'admin' }, secret: 's3cr3t', expiresIn: '2h' });
const { payload } = adapters.jwtVerify({ token, secret: 's3cr3t' });
// Key derivation + secure token
const kdf = adapters.pbkdf2({ password: 'hunter2' }); // returns salt + key
const { token: apiKey } = adapters.randomToken({ length: 32 });
// The container lane — a file, a chosen suite, a chosen KDF cost
const { container } = await adapters.encryptData({
data: pngBytesBase64, dataEncoding: 'base64',
password: 'hunter2',
cipher: 'chacha20-poly1305',
kdf: 'argon2id', kdfParams: { memoryCost: 131072, timeCost: 4 },
});
adapters.inspectContainer({ container }); // suite + costs, no passphrase needed
const { data } = await adapters.decryptData({ container, password: 'hunter2', encoding: 'base64' });
DRY boundary — what does NOT live here
This pack is deliberately scoped so it does not overlap its neighbours:
a-textowns string→string primitives on text:MD5Hash,SHA256Hash,SHA512Hash,
a hex-digest HMAC, Base64Encode/Base64Decode, and classical ciphers (CaesarCipher, Rot13, VigenereEncode/Decode). Those are deterministic display/obfuscation helpers with no key management. Reach for a-text when you just want the hex hash of a string.
a-file-actionsowns AES encryption of files on disk, at fixed parameters, writing the
older LEUENC format. This pack reads those files and never writes them: LEUENC records no algorithm and no KDF cost, which is fine for a fixed backup job and fatal once the parameters are the user's to choose.
@leumas/cryptowns the LEUC1 format, the suite/KDF catalogue and the browser lane. This
adapter imports it and does not fork it — the same engine runs in the tab and on the server, and a cross-lane test compares their bytes.
crypto(this pack) owns real applied cryptography on data and keys: password-based
AES-256-GCM of arbitrary text (authenticated, salted, random-IV), RSA/EC/Ed keypairs, digital signatures, JWTs, KDFs (PBKDF2/scrypt), and CSPRNG token/byte/compare primitives.
The small overlaps are intentional and distinct: crypto.hmac/crypto.hash are the general data/bytes primitives (any key encoding, any output encoding) used to build signing flows, whereas a-text's HMAC/*Hash are hex-only convenience wrappers for text. Use a-text for quick text digests; use this pack when you're actually securing data, keys, or tokens.
Notes
- Binary outputs default to base64 (base64url for
randomTokenand JWT segments). All results are
plain JSON-serializable objects.
encryptuses scrypt (N=16384, r=8, p=1) → AES-256-GCM; the returnedsalt/iv/authTag
make each ciphertext self-describing for decrypt.
- JWT algorithms:
HS256/384/512,RS256/384/512,PS256/384/512,ES256/384/512. ES* use
IEEE-P1363 (r‖s) signatures per the JWT spec.
- Binary crosses this boundary as base64, because an adapter result is JSON-serialisable by
contract. Pass encoding: 'base64' to decryptData for anything that is not text — utf8 on binary yields replacement characters and loses the file, silently. The HTTP lane at /api/encrypt streams real bytes instead and has no such cost.
argon2idis server-only and its native binding (@node-rs/argon2) is an optional
dependency: a missing binary degrades that one KDF at call time, with a sentence naming it, rather than taking the module down at import.
integrity: falseon a non-AEAD suite is a teaching mode, not a configuration. It produces a
file that decrypts silently wrong when modified, which is the only demonstration on the site of why authentication is not optional.