@sebastienrousseau/crypto-sdk 0.0.8 → 0.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,34 +80,71 @@ import { CryptoClient } from "@sebastienrousseau/crypto-sdk";
80
80
 
81
81
  const client = new CryptoClient({
82
82
  baseUrl: "http://localhost:3000",
83
+ apiKey: "<the server's CRYPTO_API_KEY>",
83
84
  });
84
85
 
85
86
  const { data } = await client.hash({ algorithm: "sha256", data: "hello" });
86
87
  console.log(data.digest);
87
88
  ```
88
89
 
89
- `CryptoClient` accepts two optional authentication mechanisms:
90
+ `CryptoClient` accepts two authentication mechanisms. The server refuses
91
+ unauthenticated requests unless it runs with `ALLOW_ANONYMOUS=1`, and each
92
+ route needs a scope (`crypto:sign`, `crypto:keys`, ...); an API key holds
93
+ `crypto:admin`, which covers every scope except `crypto:keys:export`.
90
94
 
91
95
  | Option | Header sent | Description |
92
96
  | :------- | :------------------------------ | :--------------- |
93
97
  | `apiKey` | `x-api-key: <value>` | Static API key |
94
98
  | `token` | `Authorization: Bearer <value>` | JWT bearer token |
95
99
 
96
- Failed requests throw a `CryptoApiError`:
100
+ ### Server-held keys
101
+
102
+ Private keys never cross the API. Key-generation methods return a `keyId`
103
+ with the public key, and the server keeps the private key; methods that
104
+ need it take the `keyId`. A key is usable only by the principal that
105
+ generated it.
106
+
107
+ ```ts
108
+ const { data: key } = await client.generateKeyPair({ algorithm: "ed25519" });
109
+ const { data: sig } = await client.sign({ keyId: key.keyId, message: "hi" });
110
+ const { data: check } = await client.verify({
111
+ publicKey: key.publicKey,
112
+ message: "hi",
113
+ signature: sig.signature,
114
+ });
115
+ console.log(check.valid); // true
116
+ ```
117
+
118
+ `exportKey({ keyId })` returns the private parts, and only to a principal
119
+ whose token grants `crypto:keys:export` explicitly.
120
+
121
+ ### Errors
122
+
123
+ Failed requests throw a `CryptoApiError` whose `body` is the server's
124
+ [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem
125
+ (`application/problem+json`):
97
126
 
98
127
  ```ts
99
128
  import { CryptoClient, CryptoApiError } from "@sebastienrousseau/crypto-sdk";
100
129
 
101
130
  try {
102
- await client.hash({ algorithm: "invalid", data: "test" });
131
+ await client.sign({ keyId: "k_AAAAAAAAAAAAAAAAAAAAAA", message: "x" });
103
132
  } catch (err) {
104
133
  if (err instanceof CryptoApiError) {
105
- console.error(err.status); // HTTP status code (e.g. 400)
106
- console.error(err.body.error); // Error message from the server
134
+ console.error(err.status); // 404
135
+ console.error(err.body.type); // "urn:crypto-service:problem:key-not-found"
136
+ console.error(err.body.detail); // "Key not found"
137
+ console.error(err.body.code); // "KEY_NOT_FOUND"
107
138
  }
108
139
  }
109
140
  ```
110
141
 
142
+ `body` has `type`, `title`, `status`, `detail` and `instance`, plus
143
+ `code` for crypto-lib and key-store errors and `errors` (`{ field, message }`
144
+ per failed check) for validation failures. A response without a problem
145
+ body (for example from a proxy) becomes an `about:blank` problem for its
146
+ status code.
147
+
111
148
  <p align="right"><a href="#contents">Back to Top</a></p>
112
149
 
113
150
  ---
@@ -150,41 +187,54 @@ signature operations.
150
187
 
151
188
  ## API Reference
152
189
 
153
- Every method returns `Promise<ApiResponse<T>>` where
154
- `ApiResponse<T>` is `{ data: T }`.
155
-
156
- | Method | Endpoint | Description |
157
- | :----------------------- | :------------------------------- | :---------------------------------------- |
158
- | `hash(body)` | `POST /v2/hash` | Compute a cryptographic hash |
159
- | `encrypt(body)` | `POST /v2/encrypt` | Encrypt plaintext |
160
- | `decrypt(body)` | `POST /v2/decrypt` | Decrypt ciphertext |
161
- | `sign(body)` | `POST /v2/sign` | Sign a message |
162
- | `verify(body)` | `POST /v2/verify` | Verify a signature |
163
- | `kdf(body)` | `POST /v2/kdf` | Derive a key |
164
- | `mac(body)` | `POST /v2/hmac` | Compute a MAC |
165
- | `macVerify(body)` | `POST /v2/hmac/verify` | Verify a MAC |
166
- | `passwordHash(body)` | `POST /v2/password/hash` | Hash a password |
167
- | `passwordVerify(body)` | `POST /v2/password/verify` | Verify a password hash |
168
- | `passwordEncrypt(body)` | `POST /v2/password/encrypt` | Encrypt data with a password |
169
- | `passwordDecrypt(body)` | `POST /v2/password/decrypt` | Decrypt password-encrypted data |
170
- | `generateKeyPair(body?)` | `POST /v2/keys/generate` | Generate a key pair |
171
- | `keyWrap(body)` | `POST /v2/keys/wrap` | Wrap a key |
172
- | `keyUnwrap(body)` | `POST /v2/keys/unwrap` | Unwrap a wrapped key |
173
- | `secretboxSeal(body)` | `POST /v2/secretbox/seal` | Seal plaintext with a symmetric key |
174
- | `secretboxOpen(body)` | `POST /v2/secretbox/open` | Open a sealed secretbox |
175
- | `sealedboxSeal(body)` | `POST /v2/sealedbox/seal` | Seal plaintext for a recipient public key |
176
- | `sealedboxOpen(body)` | `POST /v2/sealedbox/open` | Open a sealed box |
177
- | `pqGenerateKeyPair()` | `POST /v2/pq/hybrid/keygen` | Generate a hybrid key pair |
178
- | `pqEncapsulate(body)` | `POST /v2/pq/hybrid/encapsulate` | Encapsulate a shared secret |
179
- | `pqDecapsulate(body)` | `POST /v2/pq/hybrid/decapsulate` | Decapsulate a shared secret |
180
- | `pqSignKeygen(body)` | `POST /v2/pq/dsa/keygen` | Generate an ML-DSA key pair |
181
- | `pqSign(body)` | `POST /v2/pq/dsa/sign` | Sign with ML-DSA |
182
- | `pqVerify(body)` | `POST /v2/pq/dsa/verify` | Verify an ML-DSA signature |
183
- | `pqHashSignKeygen(body)` | `POST /v2/pq/hash-sign/keygen` | Generate an SLH-DSA key pair |
184
- | `pqHashSign(body)` | `POST /v2/pq/hash-sign/sign` | Sign with SLH-DSA |
185
- | `pqHashVerify(body)` | `POST /v2/pq/hash-sign/verify` | Verify an SLH-DSA signature |
186
- | `algorithms()` | `GET /v2/algorithms` | List all supported algorithms |
187
- | `health()` | `GET /health` | Server health check |
190
+ Methods return `Promise<ApiResponse<T>>` where `ApiResponse<T>` is
191
+ `{ data: T }`, except `getCbom()` (the CycloneDX document itself) and
192
+ `health()`. Methods marked _keyId_ use a server-held key; key-generation
193
+ methods return its `keyId` and public key, never the private key.
194
+
195
+ | Method | Endpoint | Description |
196
+ | :----------------------- | :------------------------------- | :--------------------------------------------- |
197
+ | `hash(body)` | `POST /v2/hash` | Compute a cryptographic hash |
198
+ | `encrypt(body)` | `POST /v2/encrypt` | Encrypt with XChaCha20-Poly1305 |
199
+ | `decrypt(body)` | `POST /v2/decrypt` | Decrypt ciphertext |
200
+ | `generateKeyPair(body?)` | `POST /v2/keys/generate` | Generate a server-held key pair |
201
+ | `exportKey(body)` | `POST /v2/keys/export` | Export a key's private parts (_keyId_) |
202
+ | `sign(body)` | `POST /v2/sign` | Sign with Ed25519 (_keyId_) |
203
+ | `verify(body)` | `POST /v2/verify` | Verify an Ed25519 signature |
204
+ | `kdf(body)` | `POST /v2/kdf` | Derive a key |
205
+ | `mac(body)` | `POST /v2/hmac` | Compute an HMAC |
206
+ | `macVerify(body)` | `POST /v2/hmac/verify` | Verify an HMAC |
207
+ | `passwordHash(body)` | `POST /v2/password/hash` | Hash a password with Argon2id |
208
+ | `passwordVerify(body)` | `POST /v2/password/verify` | Verify a password hash |
209
+ | `passwordEncrypt(body)` | `POST /v2/password/encrypt` | Encrypt data with a password |
210
+ | `passwordDecrypt(body)` | `POST /v2/password/decrypt` | Decrypt password-encrypted data |
211
+ | `keyWrap(body)` | `POST /v2/keys/wrap` | Wrap a key |
212
+ | `keyUnwrap(body)` | `POST /v2/keys/unwrap` | Unwrap a wrapped key |
213
+ | `secretboxSeal(body)` | `POST /v2/secretbox/seal` | Seal plaintext with a symmetric key |
214
+ | `secretboxOpen(body)` | `POST /v2/secretbox/open` | Open a sealed secretbox |
215
+ | `sealedboxSeal(body)` | `POST /v2/sealedbox/seal` | Seal plaintext for an X25519 public key |
216
+ | `sealedboxOpen(body)` | `POST /v2/sealedbox/open` | Open a sealed box (_keyId_) |
217
+ | `sealedboxSealPq(body)` | `POST /v2/sealedbox/seal-pq` | Seal plaintext for a hybrid public key pair |
218
+ | `sealedboxOpenPq(body)` | `POST /v2/sealedbox/open-pq` | Open a hybrid sealed box (_keyId_) |
219
+ | `mlKemGenerateKeyPair()` | `POST /v2/pq/keygen` | Generate a server-held ML-KEM-768 key pair |
220
+ | `mlKemEncapsulate(body)` | `POST /v2/pq/encapsulate` | Encapsulate to an ML-KEM-768 public key |
221
+ | `mlKemDecapsulate(body)` | `POST /v2/pq/decapsulate` | Decapsulate (_keyId_) |
222
+ | `pqGenerateKeyPair()` | `POST /v2/pq/hybrid/keygen` | Generate a server-held hybrid key pair |
223
+ | `pqEncapsulate(body)` | `POST /v2/pq/hybrid/encapsulate` | Encapsulate a hybrid shared secret |
224
+ | `pqDecapsulate(body)` | `POST /v2/pq/hybrid/decapsulate` | Decapsulate a hybrid shared secret (_keyId_) |
225
+ | `pqSignKeygen(body)` | `POST /v2/pq/dsa/keygen` | Generate a server-held ML-DSA key pair |
226
+ | `pqSign(body)` | `POST /v2/pq/dsa/sign` | Sign with ML-DSA (_keyId_; level from the key) |
227
+ | `pqVerify(body)` | `POST /v2/pq/dsa/verify` | Verify an ML-DSA signature |
228
+ | `pqHashSignKeygen(body)` | `POST /v2/pq/slh-dsa/keygen` | Generate a server-held SLH-DSA key pair |
229
+ | `pqHashSign(body)` | `POST /v2/pq/slh-dsa/sign` | Sign with SLH-DSA (_keyId_; variant from key) |
230
+ | `pqHashVerify(body)` | `POST /v2/pq/slh-dsa/verify` | Verify an SLH-DSA signature |
231
+ | `getDoraCompliance()` | `GET /v2/compliance/dora` | DORA self-assessment |
232
+ | `getCbom()` | `GET /v2/compliance/cbom` | CycloneDX 1.6 CBOM |
233
+ | `algorithms()` | `GET /v2/algorithms` | List all supported algorithms |
234
+ | `health()` | `GET /health` | Server health check |
235
+
236
+ The stream (`/v2/stream/*`), multi-recipient (`/v2/multi-recipient/encrypt`)
237
+ and v1 OpenPGP routes have no SDK methods yet.
188
238
 
189
239
  <p align="right"><a href="#contents">Back to Top</a></p>
190
240
 
@@ -192,28 +242,34 @@ Every method returns `Promise<ApiResponse<T>>` where
192
242
 
193
243
  All examples are self-contained TypeScript files in the `examples/`
194
244
  directory. Each requires the crypto-server running on
195
- `http://localhost:3000` (override via `CRYPTO_SERVER_URL`).
245
+ `http://localhost:3000` (override via `CRYPTO_SERVER_URL`) and sends
246
+ `CRYPTO_API_KEY` (or the JWT in `CRYPTO_TOKEN`) when set.
247
+ `pnpm run examples:check` type-checks them against the SDK.
196
248
 
197
249
  ```bash
198
- npx ts-node examples/<name>.ts
250
+ CRYPTO_API_KEY=<key> npx ts-node examples/<name>.ts
199
251
  ```
200
252
 
201
- | Category | Example | Purpose |
202
- | :----------- | :-------------------------------------- | :------------------------------------- |
203
- | Algorithms | [algorithms.ts](examples/algorithms.ts) | List all supported algorithms |
204
- | Encryption | [encrypt.ts](examples/encrypt.ts) | AES-256-GCM encrypt and decrypt |
205
- | Hashing | [hash.ts](examples/hash.ts) | Compute SHA-256 and BLAKE2b digests |
206
- | KDF | [kdf.ts](examples/kdf.ts) | Key derivation with HKDF-SHA256 |
207
- | Key Gen | [keygen.ts](examples/keygen.ts) | Generate key pairs |
208
- | Key Wrap | [keywrap.ts](examples/keywrap.ts) | AES key wrapping and unwrapping |
209
- | MAC | [mac.ts](examples/mac.ts) | HMAC-SHA256 compute and verify |
210
- | Passwords | [password.ts](examples/password.ts) | Argon2 hashing and password encryption |
211
- | PQ KEM | [pqkem.ts](examples/pqkem.ts) | Hybrid X25519 + ML-KEM key exchange |
212
- | PQ Sign | [pqsign.ts](examples/pqsign.ts) | ML-DSA post-quantum signing |
213
- | PQ Hash Sign | [pqhashsign.ts](examples/pqhashsign.ts) | SLH-DSA post-quantum signing |
214
- | Sealed Box | [sealedbox.ts](examples/sealedbox.ts) | Anonymous public-key encryption |
215
- | Secretbox | [secretbox.ts](examples/secretbox.ts) | Symmetric authenticated encryption |
216
- | Signing | [sign.ts](examples/sign.ts) | Ed25519 signing and verification |
253
+ | Category | Example | Purpose |
254
+ | :----------- | :-------------------------------------- | :--------------------------------------------- |
255
+ | Algorithms | [algorithms.ts](examples/algorithms.ts) | List all supported algorithms |
256
+ | Encryption | [encrypt.ts](examples/encrypt.ts) | XChaCha20-Poly1305 encrypt and decrypt |
257
+ | Hashing | [hash.ts](examples/hash.ts) | Compute SHA-256, SHA-512 and BLAKE2b digests |
258
+ | KDF | [kdf.ts](examples/kdf.ts) | Key derivation with HKDF-SHA256 |
259
+ | Key Gen | [keygen.ts](examples/keygen.ts) | Generate server-held key pairs |
260
+ | Key Wrap | [keywrap.ts](examples/keywrap.ts) | AES key wrapping and unwrapping |
261
+ | MAC | [mac.ts](examples/mac.ts) | HMAC-SHA256 compute and verify |
262
+ | Passwords | [password.ts](examples/password.ts) | Argon2 hashing and password encryption |
263
+ | PQ KEM | [pqkem.ts](examples/pqkem.ts) | Hybrid X25519 + ML-KEM key exchange by keyId |
264
+ | PQ Sign | [pqsign.ts](examples/pqsign.ts) | ML-DSA post-quantum signing by keyId |
265
+ | PQ Hash Sign | [pqhashsign.ts](examples/pqhashsign.ts) | SLH-DSA post-quantum signing by keyId |
266
+ | Sealed Box | [sealedbox.ts](examples/sealedbox.ts) | Anonymous public-key encryption, open by keyId |
267
+ | Secretbox | [secretbox.ts](examples/secretbox.ts) | Symmetric authenticated encryption |
268
+ | Signing | [sign.ts](examples/sign.ts) | Ed25519 signing by keyId and verification |
269
+
270
+ `__tests__/contract.test.ts` runs the SDK against the real crypto-server
271
+ in process (its `fetch` forwards to Fastify's `inject`), so a drift
272
+ between a method and its route fails the test suite.
217
273
 
218
274
  <p align="right"><a href="#contents">Back to Top</a></p>
219
275
 
@@ -0,0 +1,8 @@
1
+ import type { ApiError } from "./types";
2
+ export declare class CryptoApiError extends Error {
3
+ readonly status: number;
4
+ readonly body: ApiError;
5
+ constructor(status: number, body: ApiError);
6
+ }
7
+ export declare function readProblem(res: Response): Promise<ApiError>;
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAiBxC,qBAAa,cAAe,SAAQ,KAAK;IAEvC,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B,SAAgB,IAAI,EAAE,QAAQ,CAAC;gBAEnB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ;CAM3C;AAiBD,wBAAsB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CASlE"}
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CryptoApiError = void 0;
4
+ exports.readProblem = readProblem;
5
+ class CryptoApiError extends Error {
6
+ status;
7
+ body;
8
+ constructor(status, body) {
9
+ super(`API Error ${status}: ${body.detail}`);
10
+ this.name = "CryptoApiError";
11
+ this.status = status;
12
+ this.body = body;
13
+ }
14
+ }
15
+ exports.CryptoApiError = CryptoApiError;
16
+ function isProblem(body) {
17
+ return (typeof body === "object" &&
18
+ body !== null &&
19
+ typeof body.detail === "string");
20
+ }
21
+ async function readProblem(res) {
22
+ const body = await res.json().catch(() => undefined);
23
+ if (isProblem(body))
24
+ return body;
25
+ return {
26
+ type: "about:blank",
27
+ title: res.statusText || "HTTP error",
28
+ status: res.status,
29
+ detail: `The server answered ${res.status} without a problem body`,
30
+ };
31
+ }
32
+ //# sourceMappingURL=errors.js.map