@sebastienrousseau/crypto-server
v0.0.39
Published
Fastify REST API for cryptography as a service: post-quantum ML-KEM and ML-DSA, encryption, signing, hashing and KMS endpoints, with OpenAPI docs, API key or JWT auth, rate limiting and OpenTelemetry.
Maintainers
Readme
Contents
Getting started
- Install — installation via pnpm, npm, or yarn
- Requirements — runtime floor and environment prerequisites
- Quick Start — minimal working usage sample
The Crypto Service ecosystem
- The Crypto Service ecosystem — full 18-package suite overview
Package reference
- Reference & Usage — features, configuration, and capabilities
- Examples — runnable sample code
Operational
- Development — build, lint, format, and test targets
- Security — vulnerability disclosure and cryptographic invariants
- Documentation — TypeDoc API docs and ecosystem guides
- Stability guarantees — SemVer axis and release policy
- License
Install
pnpm add @sebastienrousseau/crypto-server
# or
npm install @sebastienrousseau/crypto-server
# or
yarn add @sebastienrousseau/crypto-serverRequirements
- Node.js:
^22.0.0or>=24.0.0(active and maintenance LTS releases) - Package Manager:
pnpm >=9(recommended) ornpm >=10 - TypeScript:
>=5.0(when compiling with TypeScript)
Quick Start
Start the server:
npx crypto-server
# or, from a clone of this repo:
pnpm --filter @sebastienrousseau/crypto-server startHash some data:
curl -s -X POST http://localhost:3000/v2/hash \
-H "Content-Type: application/json" \
-H "x-api-key: your-secret-api-key" \
-d '{"algorithm":"sha256","data":"Hello, world!"}' | jq{
"data": "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
}The Crypto Service ecosystem
Crypto Service provides a complete cryptography stack across 14 specialized packages:
| Package | Role | Description |
| :---------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| @sebastienrousseau/crypto-api | API docs | Postman collection types and a Postman-to-Markdown converter; the server's API contract is its OpenAPI document. |
| @sebastienrousseau/crypto-benchmarks | Benchmarks | Benchmarks comparing classical and post-quantum cryptography: ops/sec, latency and throughput. |
| @sebastienrousseau/crypto-cbom | CBOM | Cryptographic Bill of Materials generator and auditor (CycloneDX 1.6, SPDX 3.0). |
| @sebastienrousseau/crypto-cli | Terminal CLI | Command-line interface: hashing, key generation, encryption, signing, HPKE, Argon2id and CBOM scans, with an interactive menu. |
| @sebastienrousseau/crypto-edge | Edge Runtime | Edge-runtime cryptographic operations using the Web Crypto API, optimized for Cloudflare Workers, Vercel Edge, and Deno. |
| @sebastienrousseau/crypto-kms | Cloud KMS | Unified KMS interface: AWS KMS, Google Cloud KMS, Vault Transit and local providers; Azure is a stub. |
| @sebastienrousseau/crypto-lib | Core Library | Core library: post-quantum ML-KEM, ML-DSA and SLH-DSA, plus classical, modern and OpenPGP cryptography for TypeScript. |
| @sebastienrousseau/crypto-lsp | IDE Analysis | Language server that flags weak and quantum-vulnerable cryptography in the editor, with quick fixes. |
| @sebastienrousseau/crypto-mcp | AI Agents | Model Context Protocol server exposing cryptography tools to AI assistants and agents. |
| @sebastienrousseau/crypto-middleware | Middleware | Cryptographic middleware for Express and Fastify: decrypt requests, encrypt responses, verify HMAC signatures and JWTs. |
| @sebastienrousseau/crypto-prisma | ORM Adapter | Transparent field-level encryption extension for Prisma Client, using XChaCha20-Poly1305. |
| @sebastienrousseau/crypto-react | React Hooks | React hooks and context provider for client-side cryptographic operations with zero boilerplate. |
| @sebastienrousseau/crypto-sdk | Client SDK | A zero-dependency, typed HTTP client for the Crypto Service REST API, with full post-quantum support. |
| @sebastienrousseau/crypto-server (this package) | HTTP API | A hardened Fastify REST API for cryptographic operations, with rate limiting, OpenAPI schemas, and post-quantum endpoints. |
| @sebastienrousseau/crypto-testing | Test Support | Deterministic keys, fast mocks, and test fixtures for crypto-lib |
| @sebastienrousseau/crypto-typeorm | ORM Adapter | TypeORM column-level encryption with a single decorator, powered by crypto-lib. |
| @sebastienrousseau/crypto-vue | Vue Composables | Vue 3 composables for client-side cryptography |
| @sebastienrousseau/crypto-wasm | Acceleration | Placeholder for a WebAssembly accelerator; no WebAssembly module ships yet. |
Overview
Crypto Server is built on Fastify 4.x with a layered middleware stack:
Request
-> @fastify/helmet (security headers)
-> @fastify/cors
-> @fastify/rate-limit
-> @fastify/compress
-> Authentication (x-api-key / JWT Bearer)
-> Route handler
-> ResponseRoute versioning
| Prefix | Status | Notes |
| :---------------------------- | :------------- | :---------------------------------------------------------------------------- |
| /v1/* | Deprecated | Legacy PGP-based endpoints. Emit Deprecation, Sunset, and Link headers. |
| /v2/* | Current | Modern endpoints using @noble/* primitives and post-quantum algorithms. |
| /live, /ready, /metrics | Stable | Infrastructure probes (no auth required). |
API Routes
All v2 endpoints accept and return application/json. Authenticated
requests must include an x-api-key header (or
Authorization: Bearer <jwt>).
| Method | Path | Description |
| :----- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| POST | /v2/hash | Compute a cryptographic hash (SHA-2, SHA-3, BLAKE2b, BLAKE3) |
| POST | /v2/encrypt | AEAD encryption with XChaCha20-Poly1305 |
| POST | /v2/decrypt | AEAD decryption with XChaCha20-Poly1305 |
| POST | /v2/sign | Sign with a server-held Ed25519 key (keyId) |
| POST | /v2/verify | Verify a digital signature |
| POST | /v2/kdf | Derive a key (scrypt N=2^17 r=8, HKDF-SHA256, PBKDF2-SHA256 >= 600k) |
| POST | /v2/hmac | Compute an HMAC |
| POST | /v2/hmac/verify | Verify an HMAC in constant time |
| POST | /v2/password/hash | Hash a password with Argon2id (m >= 19 MiB, t >= 2) |
| POST | /v2/password/verify | Verify a password against an Argon2id hash |
| POST | /v2/password/encrypt | Encrypt with password (Argon2id + XChaCha20-Poly1305) |
| POST | /v2/password/decrypt | Decrypt with password |
| POST | /v2/keys/generate | Generate a server-held key pair; returns keyId + public key |
| POST | /v2/keys/export | Export a private key (crypto:keys:export scope only) |
| POST | /v2/keys/delete | Delete one of the caller's keys, freeing quota |
| POST | /v2/keys/wrap | Wrap a key with AES-KW or AES-KWP |
| POST | /v2/keys/unwrap | Unwrap a key |
| POST | /v2/secretbox/seal | Encrypt with XChaCha20-Poly1305 (secretbox) |
| POST | /v2/secretbox/open | Decrypt a secretbox ciphertext |
| POST | /v2/sealedbox/seal | Anonymous public-key encryption (X25519) |
| POST | /v2/sealedbox/open | Decrypt a sealed box with a server-held X25519 key (keyId) |
| POST | /v2/sealedbox/seal-pq | Post-quantum sealed box (X25519 + ML-KEM-768) |
| POST | /v2/sealedbox/open-pq | Decrypt a PQ sealed box with a server-held hybrid key |
| POST | /v2/multi-recipient/encrypt | Encrypt for multiple recipients |
| POST | /v2/pq/keygen | Generate an ML-KEM-768 key pair (FIPS 203) |
| POST | /v2/pq/encapsulate | Encapsulate a shared secret with ML-KEM-768 |
| POST | /v2/pq/decapsulate | Decapsulate with a server-held ML-KEM-768 key (keyId) |
| POST | /v2/pq/hybrid/keygen | Generate a hybrid X25519 + ML-KEM-768 key pair |
| POST | /v2/pq/hybrid/encapsulate | Hybrid encapsulation |
| POST | /v2/pq/hybrid/decapsulate | Hybrid decapsulation with a server-held key (keyId) |
| POST | /v2/pq/dsa/keygen | Generate an ML-DSA key pair (FIPS 204) |
| POST | /v2/pq/dsa/sign | Sign with a server-held ML-DSA key (keyId) |
| POST | /v2/pq/dsa/verify | Verify an ML-DSA signature |
| POST | /v2/pq/slh-dsa/keygen | Generate an SLH-DSA key pair (FIPS 205) |
| POST | /v2/pq/slh-dsa/sign | Sign with a server-held SLH-DSA key (keyId) |
| POST | /v2/pq/slh-dsa/verify | Verify an SLH-DSA signature |
| GET | /v2/algorithms | List all supported algorithms |
| GET | /live | Liveness probe (Kubernetes) |
| GET | /ready | Readiness probe: 503 while the server drains for shutdown or cannot write to CRYPTO_KEY_OUT_DIR |
| GET | /metrics | Prometheus metrics: process memory and uptime, and http_requests_total and http_request_duration_seconds by method, route and status |
Errors
Every error response is an RFC 9457 problem, served as
application/problem+json:
{
"type": "urn:crypto-service:problem:forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Missing required scope: crypto:encrypt",
"instance": "/v2/encrypt"
}type is urn:crypto-service:problem:<slug>, one of
validation-failed (400, with an errors array of { field, message }),
invalid-input (400), request-error (other 4xx), unauthorized (401),
forbidden (403), not-found (404), key-not-found (404),
key-algorithm-mismatch (400), payload-too-large (413),
rate-limited (429, with Retry-After), key-store-full (503) and
internal-error (500). A crypto-lib CryptoError or key-store error
adds its code. A 500 never carries the internal message or stack, and
instance is the request path without its query string.
Work factors
/v2/kdf and /v2/password/hash enforce the OWASP Password Storage
Cheat Sheet floors for new keys and hashes: scrypt N = 2^17 with r = 8
(crypto-lib caps N at 2^17), PBKDF2-HMAC-SHA256 with at least 600,000
iterations, and Argon2id with at least 19 MiB of memory and two passes.
A lower value gets 400. /v2/password/verify still accepts the
parameters of existing hashes. Scrypt, PBKDF2 and Argon2 (including
/v2/password/encrypt and /decrypt) run on two worker threads, so a
derivation does not block other requests or the health probes; further
derivations queue.
Key custody
The API never takes a private key and never returns one, with one
exception below. Key-generation routes (/v2/keys/generate,
/v2/pq/keygen, /v2/pq/hybrid/keygen, /v2/pq/dsa/keygen,
/v2/pq/slh-dsa/keygen) keep the private key on the server and return
a keyId with the public key. Signing, sealed-box opening and
decapsulation routes take that keyId. A key belongs to the principal
(token sub) that generated it; for anyone else it does not exist
(404).
Keys live in memory (at most 10,000 without persistence, and 1,000 per
principal by default). Set CRYPTO_KEY_OUT_DIR to also write each key
to <keyId>.json there (mode 0600, never overwritten) so keys survive
a restart. With CRYPTO_KEY_STORAGE_KEY the private parts are encrypted
and the whole record authenticated; without it they are written in
plaintext, which a production server refuses unless
CRYPTO_KEY_ALLOW_PLAINTEXT=1. POST /v2/keys/delete removes one of
the caller's keys.
POST /v2/keys/export returns a key's private parts. It needs the
crypto:keys:export scope, granted explicitly in a JWT: crypto:admin
does not include it, so API-key and anonymous principals cannot export.
The v1 routes use the server's own OpenPGP key pair from
CRYPTO_KEY_DIR: /v1/decrypt decrypts messages sent to it, and
/v1/encrypt signs with it when sign is true.
Symmetric keys (/v2/encrypt, /v2/secretbox/*, /v2/hmac, the
key-wrap KEK) are still supplied by the client.
Authentication
The server supports two authentication modes:
- API Key -- set
CRYPTO_API_KEYand pass it as thex-api-keyheader. - JWT Bearer -- set
JWT_SECRETand passAuthorization: Bearer <token>.
If neither variable is set, every request except the probes and
/docs gets 401, unless ALLOW_ANONYMOUS=1 is set for local
development or behind an authenticating gateway.
# API key
curl -H "x-api-key: your-secret-api-key" ...
# JWT Bearer
curl -H "Authorization: Bearer eyJhbGciOi..." ...A JWT must be HS256 and carry sub, exp and iat. Its sub must be
a non-empty string other than api-key and anonymous (the built-in
principals), because server-held keys belong to the subject. It is refused once it
is older than JWT_MAX_AGE seconds (default 3600) or if it declares a
longer lifetime (exp - iat). When JWT_ISSUER / JWT_AUDIENCE are
set, its iss must equal the issuer and its aud must contain the
audience. In production (NODE_ENV=production) both are required
whenever JWT_SECRET is set, and the server refuses to start without
them.
Scopes
Every route requires one scope, defined in one table
(ROUTE_SCOPES in src/config/auth-policy.ts). A principal without it
gets 403. The server refuses to start if a route has no entry.
JWT principals carry their scopes in a scopes array claim; API-key
and anonymous principals hold crypto:admin, which satisfies every
scope except crypto:keys:export.
Anonymous access (ALLOW_ANONYMOUS=1) is single-tenant: every caller is
the same principal, holds crypto:admin, and can use every key the
server holds. Use it only for local development.
KMS keys
The /v2/kms/* routes use only the providers in KMS_PROVIDERS
(default local); a request for any other provider is refused with
400 before a provider is contacted. A key created with
/v2/kms/create-key belongs to the principal that created it: other
principals get 404 for it, as for a key that does not exist. A key
the API did not create (for example one made in the cloud console) has
no owner and can be used by crypto:admin only. With
CRYPTO_KEY_OUT_DIR set, owners are kept in kms-owners.json there
(mode 0600), so they survive a restart.
| Scope | Routes |
| :------------------- | :------------------------------------------------------------------------------------------------------------------- |
| crypto:encrypt | /v1/encrypt, /v2/encrypt, secretbox/sealedbox seal, password and multi-recipient encrypt, PQ encapsulate |
| crypto:decrypt | /v1/decrypt, /v2/decrypt, secretbox/sealedbox open, password decrypt, PQ decapsulate |
| crypto:sign | /v2/sign, /v2/stream/sign, ML-DSA and SLH-DSA sign, /v2/hmac |
| crypto:verify | /v1/verify, /v2/verify, /v2/stream/verify, /v2/stream/iso20022, ML-DSA and SLH-DSA verify, /v2/hmac/verify |
| crypto:hash | /v2/hash |
| crypto:kdf | /v2/kdf, /v2/password/hash, /v2/password/verify |
| crypto:keys | /v1/generate, /v2/keys/*, PQ, ML-DSA and SLH-DSA keygen |
| crypto:keys:export | /v2/keys/export; never implied by crypto:admin |
| crypto:admin | /v1/revoke (revokes the server's own key pair); satisfies every other scope except crypto:keys:export |
GET /, GET /v2/algorithms and GET /v2/compliance/* need
authentication but no scope.
Audit log
Every request that creates, exports, deletes or uses a server-held key,
calls a KMS route, or registers an OPAQUE credential writes one log line
with msg: "audit" after the response, refused requests included:
{
"reqId": "…",
"msg": "audit",
"audit": {
"action": "POST /v2/keys/export",
"principal": "alice",
"status": 404,
"outcome": "failure",
"keyId": "k_…"
}
}It names the key or KMS provider, never key material. Audit lines are
written at info, so a LOG_LEVEL above info drops them.
Configuration
The server checks every setting at startup and refuses to start, listing each problem, when one is invalid. Numeric settings take digits only: PORT=3000abc or RATE_LIMIT_MAX=0 is an error, not a fallback to the default.
| Variable | Default | Description |
| :----------------------------------------------------------------------- | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| PORT | 3000 | TCP port to listen on |
| HOST | localhost | Bind address |
| PROTOCOL | http | http or https |
| NODE_ENV | development | development, production, or test |
| LOG_LEVEL | info | error, warn, info, or debug, for every log line (one JSON logger, pino). Credentials and key fields are redacted; request lines carry reqId, taken from a well-formed x-request-id header (at most 128 of A-Za-z0-9._-) or generated, and echoed in the response |
| CRYPTO_API_KEY | -- | Static API key for x-api-key authentication |
| JWT_SECRET | -- | HMAC secret for HS256 JWT validation |
| JWT_MAX_AGE | 3600 | Maximum JWT lifetime in seconds |
| JWT_ISSUER | -- | Required JWT iss (required in production) |
| JWT_AUDIENCE | -- | Required JWT aud (required in production) |
| CORS_ORIGIN | -- | Comma-separated allowed origins (empty = disabled) |
| TRUSTED_PROXY_CIDRS | -- | Comma-separated trusted proxy CIDRs |
| CRYPTO_KEY_DIR | -- | Directory for key storage |
| CRYPTO_KEY_OUT_DIR | -- | Where generated keys are persisted (see Key custody) |
| SHUTDOWN_TIMEOUT_MS | 30000 | Graceful shutdown timeout in milliseconds |
| ALLOW_ANONYMOUS | -- | 1 allows unauthenticated requests |
| RATE_LIMIT_MAX | 10 | Requests per client per minute (probes are exempt) |
| CRYPTO_KEY_STORAGE_KEY | -- | Encrypts the private parts of persisted keys (AES-256-GCM, authenticated with the whole record): exactly 64 hex characters; a passphrase is refused. Required in production when CRYPTO_KEY_OUT_DIR is set |
| CRYPTO_KEY_ALLOW_PLAINTEXT | -- | 1 accepts plaintext key files: writing them in production, or reading old ones after CRYPTO_KEY_STORAGE_KEY was set |
| KEY_STORE_MAX_PER_PRINCIPAL | 1000 | Keys one principal may hold; more answer 429 until it deletes some |
| OTEL_EXPORTER_OTLP_ENDPOINT | -- | OpenTelemetry OTLP collector endpoint; without it (or with OTEL_SDK_DISABLED=true) OpenTelemetry does not start, in any environment |
| OTEL_SDK_DISABLED | -- | true keeps OpenTelemetry off even with an endpoint set |
| DOCS_ENABLED | 1 | 0 removes the API documentation (/docs and the OpenAPI document at /docs/json) |
| V1_ENABLED | 1 | 0 removes the deprecated v1 routes before their sunset (1 January 2027); see MIGRATION.md |
| KMS_PROVIDERS | local | Comma-separated KMS providers requests may use: local, aws, gcp, vault. Any other provider is refused with 400; an enabled provider with a missing setting, an unknown name or azure (a stub) stops the server from starting |
| AWS_REGION | -- | Required when aws is enabled; credentials come from the AWS default credential chain |
| GCP_PROJECT_ID, GCP_LOCATION_ID, GCP_KEY_RING_ID, GCP_AUTH_TOKEN | -- | Required when gcp is enabled; the OAuth2 access token is not refreshed by the server |
| VAULT_ADDR, VAULT_TOKEN | -- | Required when vault is enabled (Transit engine mounted at transit) |
Examples
All examples are self-contained TypeScript files in the examples/
directory. Each uses fetch to call the server. Run any example
with:
# Start the server in one terminal:
pnpm --filter @sebastienrousseau/crypto-server start
# Run an example in another:
npx ts-node examples/<name>.ts| Category | Example | Purpose | | :-------------- | :---------------------------------------------- | :--------------------------------------- | | Algorithms | algorithms.ts | List supported algorithms | | Encryption | encrypt.ts | Encrypt and decrypt via v2 endpoints | | Hashing | hash.ts | Hash data via POST /v2/hash | | HMAC | hmac.ts | HMAC compute and verify | | KDF | kdf.ts | Key derivation | | Key Generation | keygen.ts | Generate keys via POST /v2/keys/generate | | Key Wrap | keywrap.ts | AES key wrapping and unwrapping | | Passwords | password.ts | Password hash and verify | | PW Encrypt | pwencrypt.ts | Password-based encryption and decryption | | PQ KEM | pqkem.ts | Post-quantum KEM operations | | PQ Sign | pqsign.ts | Post-quantum ML-DSA signing | | PQ Hash Sign | pqhashsign.ts | Post-quantum SLH-DSA signing | | Probes | probes.ts | Health and readiness checks | | Multi-Recipient | multirecipient.ts | Multi-recipient encryption | | Sealed Box | sealedbox.ts | Sealed box operations | | Secretbox | secretbox.ts | Secretbox seal and open | | Signing | sign.ts | Sign and verify via v2 endpoints |
Development
pnpm --filter @sebastienrousseau/crypto-server run build
pnpm --filter @sebastienrousseau/crypto-server run test
pnpm --filter @sebastienrousseau/crypto-server run lint
pnpm --filter @sebastienrousseau/crypto-server run formatAll 18 packages in the Crypto Service workspace maintain a 100% coverage floor across statements, branches, functions, and lines.
Security
Report vulnerabilities privately via GitHub Security Advisories or according to SECURITY.md. Never report security issues publicly.
Cryptographic operations use the @noble/* libraries, Node.js crypto and OpenPGP.js. @noble/post-quantum has not been independently audited and does not guarantee constant-time execution, and no module in this suite is FIPS 140-3 validated. Key zeroization is limited: JavaScript strings and garbage-collected buffers cannot be reliably wiped. See SECURITY.md.
Documentation
Stability guarantees
Versions advance strictly one step at a time on the 0.0.x line (v0.0.1 → v0.0.2 → v0.0.3 ... → v0.0.999 → v0.1.0). Work for every release iteration begins on a dedicated feat/v<version> branch.
All 18 packages in the workspace move in lockstep. Public API signatures, cipher output formats, and serialization schemas are strictly versioned. Breaking changes to serialized formats or algorithm defaults are considered major breaking changes. Minimum toolchain upgrades (e.g. Node.js LTS floor) are governed by POLICIES.md.
License
Dual-licensed under Apache 2.0 or MIT, at your option.
Copyright (c) 2022-2026 Sebastien Rousseau and The Crypto Service Suite contributors.
