#Crypto and Secrets
alepha/crypto holds the cryptographic helpers every other module uses:
password hashing, hashes and HMAC, AES-256-GCM encryption, and secure random
values. It also owns APP_SECRET, the key that signs tokens and cookies.
#CryptoProvider
Inject CryptoProvider instead of calling node:crypto or Math.random()
directly, so a test can substitute it.
1import { $inject } from "alepha"; 2import { CryptoProvider } from "alepha/crypto"; 3 4class AccountService { 5 crypto = $inject(CryptoProvider); 6 7 async register(password: string) { 8 const hash = await this.crypto.hashPassword(password); // "salt:key", scrypt 9 const ok = await this.crypto.verifyPassword(password, hash); // true10 11 const code = this.crypto.randomCode(6); // "042917", digits only12 const token = this.crypto.randomText(32); // base64url, 32 chars13 14 return { hash, ok, code, token };15 }16}
| Method | Notes |
|---|---|
hashPassword(password) |
scrypt with a random 16-byte salt, stored as salt:key in hex |
verifyPassword(password, stored) |
constant-time; returns false on a malformed hash instead of throwing |
hash(data, algorithm = "sha256") |
hex digest |
hmac(data, secret, algorithm = "sha256") |
hex digest; verifyHmac(data, signature, secret) compares in constant time |
encrypt(plaintext, key) / decrypt(...) |
AES-256-GCM, output iv:tag:ciphertext in hex, key hashed to 32 bytes |
equals(a, b) |
constant-time string comparison |
randomUUID() / randomUUIDv7(timestampMs) |
v7 takes the timestamp, see below |
randomText(length) |
base64url, cut to length |
randomCode(length) |
zero-padded digits, for one-time codes |
randomInt(maxExclusive) |
uniform integer in [0, maxExclusive), the stand-in for Math.random() |
randomUUIDv7 takes the timestamp rather than reading a clock, so pass
DateTimeProvider.nowMillis() and generated ids follow travel() in tests.
Ids generated in the same millisecond still sort in generation order.
1import { $inject } from "alepha"; 2import { CryptoProvider } from "alepha/crypto"; 3import { DateTimeProvider } from "alepha/datetime"; 4 5class IdService { 6 crypto = $inject(CryptoProvider); 7 dateTime = $inject(DateTimeProvider); 8 9 next() {10 return this.crypto.randomUUIDv7(this.dateTime.nowMillis());11 }12}
#In the browser
The browser build of alepha/crypto exports a Web Crypto implementation under
the same CryptoProvider name. Two differences: hash, hmac, verifyHmac,
encrypt and decrypt return promises, and hashPassword /
verifyPassword throw, since password hashing is server-only. Code shared by
both sides should await those calls.
Both providers also expose deriveKeyFromPassphrase, encryptWithPassphrase
and decryptWithPassphrase (PBKDF2 over Web Crypto), meant for client-side
encryption with a user's passphrase.
#APP_SECRET
SecretProvider reads the application secret used to sign JWTs, encrypt
cookies, and sign links such as unsubscribe URLs. It is created only when
something injects it, so an app that signs nothing never needs one.
| Variable | Meaning |
|---|---|
APP_SECRET |
the secret itself; an empty value counts as unset |
APP_SECRET_FILE |
a file holding it, read on boot and generated on first boot (mode 0600) when it is absent |
- Production refuses the default. Unset, the secret falls back to a public constant: outside production that logs a warning, in production the app refuses to boot, since anyone could forge tokens.
APP_SECRETwins over the file. The file is read only when no explicit secret is set.- No file on serverless.
APP_SECRET_FILEon Cloudflare Workers throws: there is no writable file system, so setAPP_SECRETas a secret. - Rotating it invalidates every session token, signed cookie and signed link issued with the old value.
APP_SECRET_FILE is what lets a self-hosted image start with a plain
docker run: point it at a mounted volume and each install mints its own
secret.