@sebastienrousseau/crypto-server 0.0.8 → 0.0.9

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.
Files changed (80) hide show
  1. package/README.md +159 -56
  2. package/dist/@types/types.d.ts +1 -2
  3. package/dist/@types/types.d.ts.map +1 -1
  4. package/dist/config/auth-policy.d.ts +18 -0
  5. package/dist/config/auth-policy.d.ts.map +1 -1
  6. package/dist/config/auth-policy.js +109 -0
  7. package/dist/enterprise/metering.d.ts.map +1 -1
  8. package/dist/enterprise/metering.js +2 -4
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/lib/auth.d.ts +4 -1
  12. package/dist/lib/auth.d.ts.map +1 -1
  13. package/dist/lib/auth.js +87 -40
  14. package/dist/lib/kdf-runner.d.ts +18 -0
  15. package/dist/lib/kdf-runner.d.ts.map +1 -0
  16. package/dist/lib/kdf-runner.js +27 -0
  17. package/dist/lib/key-store.d.ts +30 -0
  18. package/dist/lib/key-store.d.ts.map +1 -0
  19. package/dist/lib/key-store.js +126 -0
  20. package/dist/lib/problem.d.ts +66 -0
  21. package/dist/lib/problem.d.ts.map +1 -0
  22. package/dist/lib/problem.js +152 -0
  23. package/dist/routes/v1/decrypt.d.ts.map +1 -1
  24. package/dist/routes/v1/decrypt.js +20 -15
  25. package/dist/routes/v1/encrypt.d.ts.map +1 -1
  26. package/dist/routes/v1/encrypt.js +12 -13
  27. package/dist/routes/v1/generate.d.ts.map +1 -1
  28. package/dist/routes/v1/generate.js +22 -25
  29. package/dist/routes/v1/revoke.d.ts.map +1 -1
  30. package/dist/routes/v1/revoke.js +6 -11
  31. package/dist/routes/v1/verify.d.ts.map +1 -1
  32. package/dist/routes/v1/verify.js +6 -11
  33. package/dist/routes/v2/algorithms.js +1 -1
  34. package/dist/routes/v2/compliance.js +1 -1
  35. package/dist/routes/v2/encrypt.d.ts.map +1 -1
  36. package/dist/routes/v2/encrypt.js +1 -1
  37. package/dist/routes/v2/hash.d.ts.map +1 -1
  38. package/dist/routes/v2/hash.js +1 -1
  39. package/dist/routes/v2/kdf.d.ts +5 -0
  40. package/dist/routes/v2/kdf.d.ts.map +1 -1
  41. package/dist/routes/v2/kdf.js +27 -12
  42. package/dist/routes/v2/key-wrap.d.ts.map +1 -1
  43. package/dist/routes/v2/key-wrap.js +9 -3
  44. package/dist/routes/v2/keys.d.ts.map +1 -1
  45. package/dist/routes/v2/keys.js +39 -26
  46. package/dist/routes/v2/mac.d.ts.map +1 -1
  47. package/dist/routes/v2/mac.js +9 -3
  48. package/dist/routes/v2/multi-recipient.d.ts.map +1 -1
  49. package/dist/routes/v2/multi-recipient.js +12 -11
  50. package/dist/routes/v2/password-encrypt.d.ts.map +1 -1
  51. package/dist/routes/v2/password-encrypt.js +13 -46
  52. package/dist/routes/v2/password.d.ts +4 -0
  53. package/dist/routes/v2/password.d.ts.map +1 -1
  54. package/dist/routes/v2/password.js +28 -53
  55. package/dist/routes/v2/pq-hash-sign.d.ts.map +1 -1
  56. package/dist/routes/v2/pq-hash-sign.js +30 -24
  57. package/dist/routes/v2/pq-sign.d.ts.map +1 -1
  58. package/dist/routes/v2/pq-sign.js +30 -24
  59. package/dist/routes/v2/pq.d.ts.map +1 -1
  60. package/dist/routes/v2/pq.js +57 -38
  61. package/dist/routes/v2/sealedbox.d.ts.map +1 -1
  62. package/dist/routes/v2/sealedbox.js +32 -14
  63. package/dist/routes/v2/secretbox.d.ts.map +1 -1
  64. package/dist/routes/v2/secretbox.js +10 -3
  65. package/dist/routes/v2/signing.d.ts.map +1 -1
  66. package/dist/routes/v2/signing.js +16 -13
  67. package/dist/routes/v2/stream.d.ts.map +1 -1
  68. package/dist/routes/v2/stream.js +29 -24
  69. package/dist/server.d.ts.map +1 -1
  70. package/dist/server.js +15 -5
  71. package/dist/utils/keys.d.ts +17 -0
  72. package/dist/utils/keys.d.ts.map +1 -0
  73. package/dist/utils/keys.js +36 -0
  74. package/dist/utils/route-helpers.d.ts +1 -5
  75. package/dist/utils/route-helpers.d.ts.map +1 -1
  76. package/dist/utils/route-helpers.js +16 -10
  77. package/dist/utils/validation.d.ts +1 -0
  78. package/dist/utils/validation.d.ts.map +1 -1
  79. package/dist/utils/validation.js +17 -9
  80. package/package.json +20 -17
package/dist/lib/auth.js CHANGED
@@ -33,13 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SCOPES = void 0;
36
+ exports.EXPLICIT_SCOPES = exports.SCOPES = void 0;
37
37
  exports.registerAuth = registerAuth;
38
+ exports.exceedsLifetime = exceedsLifetime;
38
39
  exports.authenticate = authenticate;
39
40
  exports.hasScope = hasScope;
40
41
  exports.requireScope = requireScope;
42
+ exports.authorizeRoute = authorizeRoute;
41
43
  const crypto_1 = require("crypto");
42
44
  const validation_1 = require("../utils/validation");
45
+ const problem_1 = require("./problem");
46
+ const auth_policy_1 = require("../config/auth-policy");
43
47
  exports.SCOPES = [
44
48
  "crypto:encrypt",
45
49
  "crypto:decrypt",
@@ -48,18 +52,70 @@ exports.SCOPES = [
48
52
  "crypto:hash",
49
53
  "crypto:kdf",
50
54
  "crypto:keys",
55
+ "crypto:keys:export",
51
56
  "crypto:admin",
52
57
  ];
58
+ exports.EXPLICIT_SCOPES = ["crypto:keys:export"];
59
+ const IAT_CLOCK_SKEW_SECONDS = 60;
53
60
  async function registerAuth(app) {
54
61
  const jwtSecret = process.env["JWT_SECRET"];
55
- if (jwtSecret) {
56
- const fastifyJwt = await Promise.resolve().then(() => __importStar(require("@fastify/jwt")));
57
- await app.register(fastifyJwt.default, {
58
- secret: jwtSecret,
59
- verify: { algorithms: ["HS256"] },
60
- });
62
+ if (!jwtSecret)
63
+ return;
64
+ const configError = (0, auth_policy_1.jwtConfigError)(process.env);
65
+ if (configError)
66
+ throw new Error(configError);
67
+ const policy = (0, auth_policy_1.jwtPolicy)(process.env);
68
+ const fastifyJwt = await Promise.resolve().then(() => __importStar(require("@fastify/jwt")));
69
+ await app.register(fastifyJwt.default, {
70
+ secret: jwtSecret,
71
+ verify: {
72
+ algorithms: ["HS256"],
73
+ requiredClaims: [
74
+ "exp",
75
+ "iat",
76
+ ...(policy.issuer ? ["iss"] : []),
77
+ ...(policy.audience ? ["aud"] : []),
78
+ ],
79
+ maxAge: policy.maxAgeSeconds * 1000,
80
+ ...(policy.issuer ? { allowedIss: policy.issuer } : {}),
81
+ ...(policy.audience ? { allowedAud: policy.audience } : {}),
82
+ },
83
+ });
84
+ }
85
+ function exceedsLifetime(payload, maxAgeSeconds, nowSeconds = Math.floor(Date.now() / 1000)) {
86
+ const { exp, iat } = payload;
87
+ if (typeof exp !== "number" || typeof iat !== "number")
88
+ return true;
89
+ return exp - iat > maxAgeSeconds || iat > nowSeconds + IAT_CLOCK_SKEW_SECONDS;
90
+ }
91
+ async function verifyBearer(request, reply) {
92
+ try {
93
+ const decoded = await request.jwtVerify();
94
+ const { maxAgeSeconds } = (0, auth_policy_1.jwtPolicy)(process.env);
95
+ if (exceedsLifetime(decoded, maxAgeSeconds)) {
96
+ throw new Error("token lifetime exceeds JWT_MAX_AGE");
97
+ }
98
+ return decoded;
99
+ }
100
+ catch {
101
+ (0, problem_1.sendProblem)(reply, 401, "unauthorized", "Invalid or expired JWT token");
102
+ return null;
61
103
  }
62
104
  }
105
+ function checkApiKey(request, reply, apiKey) {
106
+ const providedKey = request.headers["x-api-key"];
107
+ if (!providedKey || typeof providedKey !== "string") {
108
+ (0, problem_1.sendProblem)(reply, 401, "unauthorized", "Missing API key or Bearer token");
109
+ return null;
110
+ }
111
+ const a = Buffer.from(providedKey);
112
+ const b = Buffer.from(apiKey);
113
+ if (a.length !== b.length || !(0, crypto_1.timingSafeEqual)(a, b)) {
114
+ (0, problem_1.sendProblem)(reply, 401, "unauthorized", "Invalid API key");
115
+ return null;
116
+ }
117
+ return { sub: "api-key", scopes: ["crypto:admin"] };
118
+ }
63
119
  async function authenticate(request, reply) {
64
120
  const jwtSecret = process.env["JWT_SECRET"];
65
121
  const apiKey = process.env["CRYPTO_API_KEY"];
@@ -67,54 +123,45 @@ async function authenticate(request, reply) {
67
123
  if ((0, validation_1.anonymousAllowed)()) {
68
124
  return { sub: "anonymous", scopes: ["crypto:admin"] };
69
125
  }
70
- reply
71
- .status(401)
72
- .send({ error: "Unauthorized: authentication is not configured" });
126
+ (0, problem_1.sendProblem)(reply, 401, "unauthorized", "Authentication is not configured");
73
127
  return null;
74
128
  }
75
129
  const authHeader = request.headers["authorization"];
76
130
  if (authHeader?.startsWith("Bearer ") && jwtSecret) {
77
- try {
78
- const decoded = await request.jwtVerify();
79
- return decoded;
80
- }
81
- catch {
82
- reply.status(401).send({ error: "Invalid or expired JWT token" });
83
- return null;
84
- }
131
+ return verifyBearer(request, reply);
85
132
  }
86
133
  if (apiKey) {
87
- const providedKey = request.headers["x-api-key"];
88
- if (!providedKey || typeof providedKey !== "string") {
89
- reply
90
- .status(401)
91
- .send({ error: "Unauthorized: Missing API key or Bearer token" });
92
- return null;
93
- }
94
- const a = Buffer.from(providedKey);
95
- const b = Buffer.from(apiKey);
96
- if (a.length !== b.length || !(0, crypto_1.timingSafeEqual)(a, b)) {
97
- reply.status(401).send({ error: "Unauthorized: Invalid API key" });
98
- return null;
99
- }
100
- return { sub: "api-key", scopes: ["crypto:admin"] };
134
+ return checkApiKey(request, reply, apiKey);
101
135
  }
102
- reply
103
- .status(401)
104
- .send({ error: "Unauthorized: No valid credentials provided" });
136
+ (0, problem_1.sendProblem)(reply, 401, "unauthorized", "No valid credentials provided");
105
137
  return null;
106
138
  }
107
139
  function hasScope(payload, required) {
108
- return (payload.scopes.includes("crypto:admin") || payload.scopes.includes(required));
140
+ const scopes = Array.isArray(payload.scopes)
141
+ ? payload.scopes
142
+ : [];
143
+ if (scopes.includes(required))
144
+ return true;
145
+ return !exports.EXPLICIT_SCOPES.includes(required) && scopes.includes("crypto:admin");
109
146
  }
110
147
  function requireScope(payload, scope, reply) {
111
148
  if (!hasScope(payload, scope)) {
112
- reply.status(403).send({
113
- error: "Forbidden",
114
- message: `Missing required scope: ${scope}`,
115
- });
149
+ (0, problem_1.sendProblem)(reply, 403, "forbidden", `Missing required scope: ${scope}`);
116
150
  return false;
117
151
  }
118
152
  return true;
119
153
  }
154
+ function authorizeRoute(request, reply, payload) {
155
+ const url = request.routeOptions.url;
156
+ if (url === undefined)
157
+ return true;
158
+ const required = (0, auth_policy_1.routeRequirement)(request.method, url);
159
+ if (required === undefined) {
160
+ (0, problem_1.sendProblem)(reply, 403, "forbidden", "No access policy is defined for this route");
161
+ return false;
162
+ }
163
+ if (required === auth_policy_1.AUTHENTICATED)
164
+ return true;
165
+ return requireScope(payload, required, reply);
166
+ }
120
167
  //# sourceMappingURL=auth.js.map
@@ -0,0 +1,18 @@
1
+ export declare const KDF_WORKER_THREADS = 2;
2
+ declare const MODULES: {
3
+ readonly modern: string;
4
+ readonly highLevel: string;
5
+ };
6
+ export type KdfModule = keyof typeof MODULES;
7
+ export declare class KdfRunner {
8
+ private pool;
9
+ run<T>(module: KdfModule, functionName: string, ...args: unknown[]): Promise<T>;
10
+ close(): Promise<void>;
11
+ }
12
+ declare module "fastify" {
13
+ interface FastifyInstance {
14
+ kdf: KdfRunner;
15
+ }
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=kdf-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kdf-runner.d.ts","sourceRoot":"","sources":["../../src/lib/kdf-runner.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAQpC,QAAA,MAAM,OAAO;;;CAGH,CAAC;AAGX,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,OAAO,CAAC;AAM7C,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAyB;IAOrC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE;IAU5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QAEvB,GAAG,EAAE,SAAS,CAAC;KAChB;CACF"}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KdfRunner = exports.KDF_WORKER_THREADS = void 0;
4
+ const accel_1 = require("@sebastienrousseau/crypto-lib/accel");
5
+ exports.KDF_WORKER_THREADS = 2;
6
+ const MODULES = {
7
+ modern: require.resolve("@sebastienrousseau/crypto-lib/modern"),
8
+ highLevel: require.resolve("@sebastienrousseau/crypto-lib/high-level"),
9
+ };
10
+ class KdfRunner {
11
+ pool;
12
+ run(module, functionName, ...args) {
13
+ this.pool ??= new accel_1.WorkerPool({ size: exports.KDF_WORKER_THREADS });
14
+ return this.pool.execute({
15
+ modulePath: MODULES[module],
16
+ functionName,
17
+ args,
18
+ });
19
+ }
20
+ async close() {
21
+ const pool = this.pool;
22
+ this.pool = undefined;
23
+ await pool?.shutdown();
24
+ }
25
+ }
26
+ exports.KdfRunner = KdfRunner;
27
+ //# sourceMappingURL=kdf-runner.js.map
@@ -0,0 +1,30 @@
1
+ export type KeyParts = Readonly<Record<string, string>>;
2
+ export interface StoredKey {
3
+ readonly keyId: string;
4
+ readonly algorithm: string;
5
+ readonly owner: string;
6
+ readonly createdAt: string;
7
+ readonly publicParts: KeyParts;
8
+ readonly privateParts: KeyParts;
9
+ }
10
+ export type NewKey = Pick<StoredKey, "algorithm" | "owner" | "publicParts" | "privateParts">;
11
+ export declare const KEY_ID_PATTERN = "^k_[A-Za-z0-9_-]{22}$";
12
+ export declare const DEFAULT_KEY_CAPACITY = 10000;
13
+ export declare class KeyStoreError extends Error {
14
+ readonly statusCode: number;
15
+ readonly code: string;
16
+ constructor(message: string, statusCode: number, code: string);
17
+ }
18
+ export declare class KeyStore {
19
+ private readonly dir;
20
+ private readonly capacity;
21
+ private readonly keys;
22
+ constructor(dir: string | undefined, capacity?: number);
23
+ put(key: NewKey): Promise<StoredKey>;
24
+ get(keyId: string, owner: string, algorithms?: readonly string[]): Promise<StoredKey>;
25
+ private lookup;
26
+ private remember;
27
+ private file;
28
+ }
29
+ export declare function keyStoreFromEnv(): KeyStore;
30
+ //# sourceMappingURL=key-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-store.d.ts","sourceRoot":"","sources":["../../src/lib/key-store.ts"],"names":[],"mappings":"AA6BA,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAGxD,MAAM,WAAW,SAAS;IAExB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;IAE/B,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC;CACjC;AAGD,MAAM,MAAM,MAAM,GAAG,IAAI,CACvB,SAAS,EACT,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,cAAc,CACvD,CAAC;AAGF,eAAO,MAAM,cAAc,0BAA0B,CAAC;AAKtD,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAG3C,qBAAa,aAAc,SAAQ,KAAK;IAEtC,SAAgB,UAAU,EAAE,MAAM,CAAC;IAEnC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAM9D;AAOD,qBAAa,QAAQ;IAUjB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAV3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;gBASlC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,GAAE,MAA6B;IAIpD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IA6BpC,GAAG,CACP,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,SAAS,CAAC;YAcP,MAAM;IAiBpB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;CAGb;AAGD,wBAAgB,eAAe,IAAI,QAAQ,CAE1C"}
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.KeyStore = exports.KeyStoreError = exports.DEFAULT_KEY_CAPACITY = exports.KEY_ID_PATTERN = void 0;
37
+ exports.keyStoreFromEnv = keyStoreFromEnv;
38
+ const crypto_1 = require("crypto");
39
+ const promises_1 = require("fs/promises");
40
+ const path = __importStar(require("path"));
41
+ exports.KEY_ID_PATTERN = "^k_[A-Za-z0-9_-]{22}$";
42
+ const KEY_ID_RE = new RegExp(exports.KEY_ID_PATTERN);
43
+ exports.DEFAULT_KEY_CAPACITY = 10_000;
44
+ class KeyStoreError extends Error {
45
+ statusCode;
46
+ code;
47
+ constructor(message, statusCode, code) {
48
+ super(message);
49
+ this.name = "KeyStoreError";
50
+ this.statusCode = statusCode;
51
+ this.code = code;
52
+ }
53
+ }
54
+ exports.KeyStoreError = KeyStoreError;
55
+ const notFound = () => new KeyStoreError("Key not found", 404, "KEY_NOT_FOUND");
56
+ class KeyStore {
57
+ dir;
58
+ capacity;
59
+ keys = new Map();
60
+ constructor(dir, capacity = exports.DEFAULT_KEY_CAPACITY) {
61
+ this.dir = dir;
62
+ this.capacity = capacity;
63
+ }
64
+ async put(key) {
65
+ if (!this.dir && this.keys.size >= this.capacity) {
66
+ throw new KeyStoreError("Key store is full; set CRYPTO_KEY_OUT_DIR to persist keys", 503, "KEY_STORE_FULL");
67
+ }
68
+ const stored = Object.freeze({
69
+ ...key,
70
+ keyId: `k_${(0, crypto_1.randomBytes)(16).toString("base64url")}`,
71
+ createdAt: new Date().toISOString(),
72
+ });
73
+ if (this.dir) {
74
+ await (0, promises_1.writeFile)(this.file(stored.keyId), JSON.stringify(stored), {
75
+ encoding: "utf8",
76
+ mode: 0o600,
77
+ flag: "wx",
78
+ });
79
+ }
80
+ this.remember(stored);
81
+ return stored;
82
+ }
83
+ async get(keyId, owner, algorithms) {
84
+ const key = await this.lookup(keyId);
85
+ if (!key || key.owner !== owner)
86
+ throw notFound();
87
+ if (algorithms && !algorithms.includes(key.algorithm)) {
88
+ throw new KeyStoreError(`Key ${keyId} is a ${key.algorithm} key; this operation needs ${algorithms.join(" or ")}`, 400, "KEY_ALGORITHM_MISMATCH");
89
+ }
90
+ return key;
91
+ }
92
+ async lookup(keyId) {
93
+ if (!KEY_ID_RE.test(keyId))
94
+ return undefined;
95
+ const cached = this.keys.get(keyId);
96
+ if (cached || !this.dir)
97
+ return cached;
98
+ let raw;
99
+ try {
100
+ raw = await (0, promises_1.readFile)(this.file(keyId), "utf8");
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ const stored = Object.freeze(JSON.parse(raw));
106
+ if (stored.keyId !== keyId)
107
+ return undefined;
108
+ this.remember(stored);
109
+ return stored;
110
+ }
111
+ remember(key) {
112
+ this.keys.set(key.keyId, key);
113
+ if (this.keys.size > this.capacity) {
114
+ const oldest = this.keys.keys().next().value;
115
+ this.keys.delete(oldest);
116
+ }
117
+ }
118
+ file(keyId) {
119
+ return path.join(this.dir, `${keyId}.json`);
120
+ }
121
+ }
122
+ exports.KeyStore = KeyStore;
123
+ function keyStoreFromEnv() {
124
+ return new KeyStore(process.env["CRYPTO_KEY_OUT_DIR"] || undefined);
125
+ }
126
+ //# sourceMappingURL=key-store.js.map
@@ -0,0 +1,66 @@
1
+ import type { FastifyInstance, FastifyReply } from "fastify";
2
+ import { CryptoError } from "@sebastienrousseau/crypto-lib";
3
+ export declare const PROBLEM_CONTENT_TYPE = "application/problem+json";
4
+ export declare const PROBLEM_TYPE_PREFIX = "urn:crypto-service:problem:";
5
+ export declare const PROBLEM_TITLES: Readonly<{
6
+ "validation-failed": "Validation failed";
7
+ "invalid-input": "Invalid input";
8
+ unauthorized: "Unauthorized";
9
+ forbidden: "Forbidden";
10
+ "not-found": "Not Found";
11
+ "key-not-found": "Key not found";
12
+ "key-algorithm-mismatch": "Key algorithm mismatch";
13
+ "key-store-full": "Key store full";
14
+ "payload-too-large": "Payload Too Large";
15
+ "rate-limited": "Too Many Requests";
16
+ "request-error": "Bad Request";
17
+ "internal-error": "Internal Server Error";
18
+ }>;
19
+ export type ProblemSlug = keyof typeof PROBLEM_TITLES;
20
+ export interface Problem {
21
+ type: string;
22
+ title: string;
23
+ status: number;
24
+ detail: string;
25
+ instance?: string;
26
+ [extension: string]: unknown;
27
+ }
28
+ export declare const PROBLEM_SCHEMA: {
29
+ readonly type: "object";
30
+ readonly properties: {
31
+ readonly type: {
32
+ readonly type: "string";
33
+ };
34
+ readonly title: {
35
+ readonly type: "string";
36
+ };
37
+ readonly status: {
38
+ readonly type: "integer";
39
+ };
40
+ readonly detail: {
41
+ readonly type: "string";
42
+ };
43
+ readonly instance: {
44
+ readonly type: "string";
45
+ };
46
+ readonly code: {
47
+ readonly type: "string";
48
+ };
49
+ readonly errors: {
50
+ readonly type: "array";
51
+ };
52
+ };
53
+ };
54
+ export declare function problem(status: number, slug: ProblemSlug, detail: string, extensions?: Record<string, unknown>, instance?: string): Problem;
55
+ export declare function sendProblem(reply: FastifyReply, status: number, slug: ProblemSlug, detail: string, extensions?: Record<string, unknown>): FastifyReply;
56
+ export declare function cryptoErrorStatus(code: string): number;
57
+ export declare function isCryptoError(error: unknown): error is CryptoError;
58
+ export interface ProblemSpec {
59
+ status: number;
60
+ slug: ProblemSlug;
61
+ detail: string;
62
+ extensions: Record<string, unknown>;
63
+ }
64
+ export declare function classifyThrown(error: unknown): ProblemSpec;
65
+ export declare function registerProblemHandlers(app: FastifyInstance): void;
66
+ //# sourceMappingURL=problem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../../src/lib/problem.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAkB,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAI5D,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAG/D,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAGjE,eAAO,MAAM,cAAc;;;;;;;;;;;;;EAazB,CAAC;AAGH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC;AAGtD,MAAM,WAAW,OAAO;IAEtB,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;IAEd,MAAM,EAAE,MAAM,CAAC;IAEf,MAAM,EAAE,MAAM,CAAC;IAEf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAGD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;CAWjB,CAAC;AAGX,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACxC,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAST;AAOD,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,YAAY,CAGd;AAaD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAUD,MAAM,WAAW,WAAW;IAE1B,MAAM,EAAE,MAAM,CAAC;IAEf,IAAI,EAAE,WAAW,CAAC;IAElB,MAAM,EAAE,MAAM,CAAC;IAEf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAwFD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAU1D;AAOD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAoBlE"}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROBLEM_SCHEMA = exports.PROBLEM_TITLES = exports.PROBLEM_TYPE_PREFIX = exports.PROBLEM_CONTENT_TYPE = void 0;
4
+ exports.problem = problem;
5
+ exports.sendProblem = sendProblem;
6
+ exports.cryptoErrorStatus = cryptoErrorStatus;
7
+ exports.isCryptoError = isCryptoError;
8
+ exports.classifyThrown = classifyThrown;
9
+ exports.registerProblemHandlers = registerProblemHandlers;
10
+ const http_1 = require("http");
11
+ const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
12
+ const key_store_1 = require("./key-store");
13
+ exports.PROBLEM_CONTENT_TYPE = "application/problem+json";
14
+ exports.PROBLEM_TYPE_PREFIX = "urn:crypto-service:problem:";
15
+ exports.PROBLEM_TITLES = Object.freeze({
16
+ "validation-failed": "Validation failed",
17
+ "invalid-input": "Invalid input",
18
+ unauthorized: "Unauthorized",
19
+ forbidden: "Forbidden",
20
+ "not-found": "Not Found",
21
+ "key-not-found": "Key not found",
22
+ "key-algorithm-mismatch": "Key algorithm mismatch",
23
+ "key-store-full": "Key store full",
24
+ "payload-too-large": "Payload Too Large",
25
+ "rate-limited": "Too Many Requests",
26
+ "request-error": "Bad Request",
27
+ "internal-error": "Internal Server Error",
28
+ });
29
+ exports.PROBLEM_SCHEMA = {
30
+ type: "object",
31
+ properties: {
32
+ type: { type: "string" },
33
+ title: { type: "string" },
34
+ status: { type: "integer" },
35
+ detail: { type: "string" },
36
+ instance: { type: "string" },
37
+ code: { type: "string" },
38
+ errors: { type: "array" },
39
+ },
40
+ };
41
+ function problem(status, slug, detail, extensions = {}, instance) {
42
+ return {
43
+ type: `${exports.PROBLEM_TYPE_PREFIX}${slug}`,
44
+ title: exports.PROBLEM_TITLES[slug],
45
+ status,
46
+ detail,
47
+ ...(instance === undefined ? {} : { instance }),
48
+ ...extensions,
49
+ };
50
+ }
51
+ const pathOf = (request) => request?.url.split("?")[0];
52
+ function sendProblem(reply, status, slug, detail, extensions) {
53
+ const body = problem(status, slug, detail, extensions, pathOf(reply.request));
54
+ return reply.status(status).type(exports.PROBLEM_CONTENT_TYPE).send(body);
55
+ }
56
+ const CRYPTO_INPUT_CODES = new Set([
57
+ "INVALID_KEY",
58
+ "UNSUPPORTED_ALGORITHM",
59
+ "INVALID_HEX",
60
+ "INVALID_CIPHERTEXT",
61
+ "AUTH_FAILED",
62
+ "INVALID_INPUT",
63
+ ]);
64
+ function cryptoErrorStatus(code) {
65
+ return CRYPTO_INPUT_CODES.has(code) ? 400 : 500;
66
+ }
67
+ function isCryptoError(error) {
68
+ return error instanceof crypto_lib_1.CryptoError;
69
+ }
70
+ const KEY_STORE_SLUGS = {
71
+ KEY_NOT_FOUND: "key-not-found",
72
+ KEY_ALGORITHM_MISMATCH: "key-algorithm-mismatch",
73
+ KEY_STORE_FULL: "key-store-full",
74
+ };
75
+ function fieldOf(issue) {
76
+ const missing = issue.params.missingProperty;
77
+ return missing ? `${issue.instancePath}/${missing}` : issue.instancePath;
78
+ }
79
+ const internal = (extensions = {}) => ({
80
+ status: 500,
81
+ slug: "internal-error",
82
+ detail: "An unexpected error occurred",
83
+ extensions,
84
+ });
85
+ function validationProblem(error) {
86
+ const errors = error.validation.map((issue) => ({
87
+ field: fieldOf(issue),
88
+ message: issue.message ?? "is invalid",
89
+ }));
90
+ return {
91
+ status: 400,
92
+ slug: "validation-failed",
93
+ detail: error.message,
94
+ extensions: { errors },
95
+ };
96
+ }
97
+ function cryptoProblem(error) {
98
+ if (cryptoErrorStatus(error.code) === 500) {
99
+ return internal({ code: error.code });
100
+ }
101
+ return {
102
+ status: 400,
103
+ slug: "invalid-input",
104
+ detail: "Invalid input",
105
+ extensions: { code: error.code },
106
+ };
107
+ }
108
+ function keyStoreProblem(error) {
109
+ return {
110
+ status: error.statusCode,
111
+ slug: KEY_STORE_SLUGS[error.code] ?? "request-error",
112
+ detail: error.message,
113
+ extensions: { code: error.code },
114
+ };
115
+ }
116
+ function clientProblem(status, error) {
117
+ const slug = status === 429
118
+ ? "rate-limited"
119
+ : status === 413
120
+ ? "payload-too-large"
121
+ : "request-error";
122
+ return {
123
+ status,
124
+ slug,
125
+ detail: error.message ?? String(http_1.STATUS_CODES[status]),
126
+ extensions: {},
127
+ };
128
+ }
129
+ function classifyThrown(error) {
130
+ if (isCryptoError(error))
131
+ return cryptoProblem(error);
132
+ if (error instanceof key_store_1.KeyStoreError)
133
+ return keyStoreProblem(error);
134
+ const thrown = (error ?? {});
135
+ if (thrown.validation) {
136
+ return validationProblem(thrown);
137
+ }
138
+ const status = thrown.statusCode ?? 500;
139
+ if (status >= 400 && status < 500)
140
+ return clientProblem(status, thrown);
141
+ return internal();
142
+ }
143
+ function registerProblemHandlers(app) {
144
+ app.setErrorHandler((error, request, reply) => {
145
+ const spec = classifyThrown(error);
146
+ if (spec.status >= 500)
147
+ request.log.error(error, "Request failed");
148
+ return sendProblem(reply, spec.status, spec.slug, spec.detail, spec.extensions);
149
+ });
150
+ app.setNotFoundHandler((request, reply) => sendProblem(reply, 404, "not-found", `Route ${request.method} ${pathOf(request)} not found`));
151
+ }
152
+ //# sourceMappingURL=problem.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/decrypt.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;yBAkC/B,KAAK,eAAe,KAAG,IAAI;AAA3C,wBAgCE"}
1
+ {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../../src/routes/v1/decrypt.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;yBA2C/B,KAAK,eAAe,KAAG,IAAI;AAA3C,wBA+BE"}