@sebastienrousseau/crypto-server 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.
Files changed (102) 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 +126 -0
  7. package/dist/config/constants.d.ts.map +1 -1
  8. package/dist/config/constants.js +7 -1
  9. package/dist/enterprise/metering.d.ts.map +1 -1
  10. package/dist/enterprise/metering.js +2 -4
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/lib/auth.d.ts +4 -1
  14. package/dist/lib/auth.d.ts.map +1 -1
  15. package/dist/lib/auth.js +87 -40
  16. package/dist/lib/kdf-runner.d.ts +18 -0
  17. package/dist/lib/kdf-runner.d.ts.map +1 -0
  18. package/dist/lib/kdf-runner.js +27 -0
  19. package/dist/lib/key-store.d.ts +37 -0
  20. package/dist/lib/key-store.d.ts.map +1 -0
  21. package/dist/lib/key-store.js +223 -0
  22. package/dist/lib/opaque-store.d.ts +34 -0
  23. package/dist/lib/opaque-store.d.ts.map +1 -0
  24. package/dist/lib/opaque-store.js +72 -0
  25. package/dist/lib/problem.d.ts +67 -0
  26. package/dist/lib/problem.d.ts.map +1 -0
  27. package/dist/lib/problem.js +162 -0
  28. package/dist/routes/v1/decrypt.d.ts.map +1 -1
  29. package/dist/routes/v1/decrypt.js +24 -16
  30. package/dist/routes/v1/encrypt.d.ts.map +1 -1
  31. package/dist/routes/v1/encrypt.js +12 -13
  32. package/dist/routes/v1/generate.d.ts.map +1 -1
  33. package/dist/routes/v1/generate.js +22 -25
  34. package/dist/routes/v1/revoke.d.ts.map +1 -1
  35. package/dist/routes/v1/revoke.js +6 -11
  36. package/dist/routes/v1/verify.d.ts.map +1 -1
  37. package/dist/routes/v1/verify.js +10 -12
  38. package/dist/routes/v2/algorithms.js +1 -1
  39. package/dist/routes/v2/compliance.js +1 -1
  40. package/dist/routes/v2/encrypt.d.ts.map +1 -1
  41. package/dist/routes/v2/encrypt.js +1 -1
  42. package/dist/routes/v2/hash.d.ts.map +1 -1
  43. package/dist/routes/v2/hash.js +1 -1
  44. package/dist/routes/v2/hpke.d.ts +3 -0
  45. package/dist/routes/v2/hpke.d.ts.map +1 -0
  46. package/dist/routes/v2/hpke.js +168 -0
  47. package/dist/routes/v2/index.d.ts.map +1 -1
  48. package/dist/routes/v2/index.js +6 -0
  49. package/dist/routes/v2/kdf.d.ts +5 -0
  50. package/dist/routes/v2/kdf.d.ts.map +1 -1
  51. package/dist/routes/v2/kdf.js +27 -12
  52. package/dist/routes/v2/key-wrap.d.ts.map +1 -1
  53. package/dist/routes/v2/key-wrap.js +9 -3
  54. package/dist/routes/v2/keys.d.ts.map +1 -1
  55. package/dist/routes/v2/keys.js +39 -26
  56. package/dist/routes/v2/kms.d.ts +7 -0
  57. package/dist/routes/v2/kms.d.ts.map +1 -0
  58. package/dist/routes/v2/kms.js +310 -0
  59. package/dist/routes/v2/mac.d.ts.map +1 -1
  60. package/dist/routes/v2/mac.js +10 -3
  61. package/dist/routes/v2/multi-recipient.d.ts.map +1 -1
  62. package/dist/routes/v2/multi-recipient.js +12 -11
  63. package/dist/routes/v2/opaque.d.ts +4 -0
  64. package/dist/routes/v2/opaque.d.ts.map +1 -0
  65. package/dist/routes/v2/opaque.js +174 -0
  66. package/dist/routes/v2/password-encrypt.d.ts.map +1 -1
  67. package/dist/routes/v2/password-encrypt.js +13 -46
  68. package/dist/routes/v2/password.d.ts +4 -0
  69. package/dist/routes/v2/password.d.ts.map +1 -1
  70. package/dist/routes/v2/password.js +28 -53
  71. package/dist/routes/v2/pq-hash-sign.d.ts.map +1 -1
  72. package/dist/routes/v2/pq-hash-sign.js +33 -24
  73. package/dist/routes/v2/pq-sign.d.ts.map +1 -1
  74. package/dist/routes/v2/pq-sign.js +31 -24
  75. package/dist/routes/v2/pq.d.ts.map +1 -1
  76. package/dist/routes/v2/pq.js +57 -38
  77. package/dist/routes/v2/sealedbox.d.ts.map +1 -1
  78. package/dist/routes/v2/sealedbox.js +32 -14
  79. package/dist/routes/v2/secretbox.d.ts.map +1 -1
  80. package/dist/routes/v2/secretbox.js +10 -3
  81. package/dist/routes/v2/signing.d.ts.map +1 -1
  82. package/dist/routes/v2/signing.js +17 -13
  83. package/dist/routes/v2/stream-schemas.d.ts +296 -0
  84. package/dist/routes/v2/stream-schemas.d.ts.map +1 -0
  85. package/dist/routes/v2/stream-schemas.js +207 -0
  86. package/dist/routes/v2/stream.d.ts.map +1 -1
  87. package/dist/routes/v2/stream.js +144 -151
  88. package/dist/server.d.ts.map +1 -1
  89. package/dist/server.js +19 -5
  90. package/dist/utils/keys.d.ts +17 -0
  91. package/dist/utils/keys.d.ts.map +1 -0
  92. package/dist/utils/keys.js +36 -0
  93. package/dist/utils/route-helpers.d.ts +1 -5
  94. package/dist/utils/route-helpers.d.ts.map +1 -1
  95. package/dist/utils/route-helpers.js +16 -10
  96. package/dist/utils/trace.d.ts +12 -0
  97. package/dist/utils/trace.d.ts.map +1 -0
  98. package/dist/utils/trace.js +62 -0
  99. package/dist/utils/validation.d.ts +1 -0
  100. package/dist/utils/validation.d.ts.map +1 -1
  101. package/dist/utils/validation.js +17 -9
  102. package/package.json +22 -18
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,37 @@
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 interface EncryptedPartsPayload {
19
+ readonly ciphertext: string;
20
+ readonly iv: string;
21
+ readonly tag: string;
22
+ readonly alg: "aes-256-gcm";
23
+ }
24
+ export declare class KeyStore {
25
+ private readonly dir;
26
+ private readonly capacity;
27
+ private readonly keys;
28
+ private readonly storageKey;
29
+ constructor(dir: string | undefined, capacity?: number, storageKey?: string | Uint8Array);
30
+ put(key: NewKey): Promise<StoredKey>;
31
+ get(keyId: string, owner: string, algorithms?: readonly string[]): Promise<StoredKey>;
32
+ private lookup;
33
+ private remember;
34
+ private file;
35
+ }
36
+ export declare function keyStoreFromEnv(): KeyStore;
37
+ //# 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":"AAmCA,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;AAGD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;CAC7B;AAmFD,qBAAa,QAAQ;IAajB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAb3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IACrD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;gBAW7B,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,QAAQ,GAAE,MAA6B,EACxD,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU;IAM5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAgDpC,GAAG,CACP,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,SAAS,CAAC;YAcP,MAAM;IAwDpB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;CAGb;AAGD,wBAAgB,eAAe,IAAI,QAAQ,CAM1C"}
@@ -0,0 +1,223 @@
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
+ const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
42
+ exports.KEY_ID_PATTERN = "^k_[A-Za-z0-9_-]{22}$";
43
+ const KEY_ID_RE = new RegExp(exports.KEY_ID_PATTERN);
44
+ exports.DEFAULT_KEY_CAPACITY = 10_000;
45
+ class KeyStoreError extends Error {
46
+ statusCode;
47
+ code;
48
+ constructor(message, statusCode, code) {
49
+ super(message);
50
+ this.name = "KeyStoreError";
51
+ this.statusCode = statusCode;
52
+ this.code = code;
53
+ }
54
+ }
55
+ exports.KeyStoreError = KeyStoreError;
56
+ function resolveStorageKey(key) {
57
+ if (!key)
58
+ return undefined;
59
+ if (typeof key !== "string") {
60
+ if (key.length === 32)
61
+ return Buffer.from(key);
62
+ return (0, crypto_1.createHash)("sha256").update(key).digest();
63
+ }
64
+ const trimmed = key.trim();
65
+ if (!trimmed)
66
+ return undefined;
67
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
68
+ return Buffer.from(trimmed, "hex");
69
+ }
70
+ return (0, crypto_1.createHash)("sha256").update(trimmed, "utf8").digest();
71
+ }
72
+ function encryptPrivateParts(privateParts, keyId, owner, key) {
73
+ const iv = (0, crypto_1.randomBytes)(12);
74
+ const cipher = (0, crypto_1.createCipheriv)("aes-256-gcm", key, iv);
75
+ cipher.setAAD(Buffer.from(`${keyId}:${owner}`, "utf8"));
76
+ const plaintext = Buffer.from(JSON.stringify(privateParts), "utf8");
77
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
78
+ const tag = cipher.getAuthTag();
79
+ (0, crypto_lib_1.wipeMemory)(plaintext);
80
+ return {
81
+ ciphertext: ciphertext.toString("base64"),
82
+ iv: iv.toString("base64"),
83
+ tag: tag.toString("base64"),
84
+ alg: "aes-256-gcm",
85
+ };
86
+ }
87
+ function decryptPrivateParts(encrypted, keyId, owner, key) {
88
+ if (encrypted.alg !== "aes-256-gcm") {
89
+ throw new KeyStoreError(`Unsupported encryption algorithm: ${encrypted.alg}`, 500, "UNSUPPORTED_STORAGE_ALGORITHM");
90
+ }
91
+ try {
92
+ const iv = Buffer.from(encrypted.iv, "base64");
93
+ const tag = Buffer.from(encrypted.tag, "base64");
94
+ const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
95
+ const decipher = (0, crypto_1.createDecipheriv)("aes-256-gcm", key, iv);
96
+ decipher.setAAD(Buffer.from(`${keyId}:${owner}`, "utf8"));
97
+ decipher.setAuthTag(tag);
98
+ const decrypted = Buffer.concat([
99
+ decipher.update(ciphertext),
100
+ decipher.final(),
101
+ ]);
102
+ const parsed = JSON.parse(decrypted.toString("utf8"));
103
+ (0, crypto_lib_1.wipeMemory)(decrypted);
104
+ return parsed;
105
+ }
106
+ catch {
107
+ throw new KeyStoreError("Failed to decrypt key material: invalid key or corrupted data", 500, "KEY_INTEGRITY_FAILED");
108
+ }
109
+ }
110
+ const notFound = () => new KeyStoreError("Key not found", 404, "KEY_NOT_FOUND");
111
+ class KeyStore {
112
+ dir;
113
+ capacity;
114
+ keys = new Map();
115
+ storageKey;
116
+ constructor(dir, capacity = exports.DEFAULT_KEY_CAPACITY, storageKey) {
117
+ this.dir = dir;
118
+ this.capacity = capacity;
119
+ this.storageKey = resolveStorageKey(storageKey);
120
+ }
121
+ async put(key) {
122
+ if (!this.dir && this.keys.size >= this.capacity) {
123
+ throw new KeyStoreError("Key store is full; set CRYPTO_KEY_OUT_DIR to persist keys", 503, "KEY_STORE_FULL");
124
+ }
125
+ const stored = Object.freeze({
126
+ ...key,
127
+ keyId: `k_${(0, crypto_1.randomBytes)(16).toString("base64url")}`,
128
+ createdAt: new Date().toISOString(),
129
+ });
130
+ if (this.dir) {
131
+ const diskRecord = {
132
+ keyId: stored.keyId,
133
+ algorithm: stored.algorithm,
134
+ owner: stored.owner,
135
+ createdAt: stored.createdAt,
136
+ publicParts: stored.publicParts,
137
+ };
138
+ if (this.storageKey) {
139
+ diskRecord.encryptedPrivateParts = encryptPrivateParts(stored.privateParts, stored.keyId, stored.owner, this.storageKey);
140
+ }
141
+ else {
142
+ diskRecord.privateParts = stored.privateParts;
143
+ }
144
+ await (0, promises_1.writeFile)(this.file(stored.keyId), JSON.stringify(diskRecord), {
145
+ encoding: "utf8",
146
+ mode: 0o600,
147
+ flag: "wx",
148
+ });
149
+ }
150
+ this.remember(stored);
151
+ return stored;
152
+ }
153
+ async get(keyId, owner, algorithms) {
154
+ const key = await this.lookup(keyId);
155
+ if (!key || key.owner !== owner)
156
+ throw notFound();
157
+ if (algorithms && !algorithms.includes(key.algorithm)) {
158
+ throw new KeyStoreError(`Key ${keyId} is a ${key.algorithm} key; this operation needs ${algorithms.join(" or ")}`, 400, "KEY_ALGORITHM_MISMATCH");
159
+ }
160
+ return key;
161
+ }
162
+ async lookup(keyId) {
163
+ if (!KEY_ID_RE.test(keyId))
164
+ return undefined;
165
+ const cached = this.keys.get(keyId);
166
+ if (cached || !this.dir)
167
+ return cached;
168
+ let raw;
169
+ try {
170
+ raw = await (0, promises_1.readFile)(this.file(keyId), "utf8");
171
+ }
172
+ catch {
173
+ return undefined;
174
+ }
175
+ let record;
176
+ try {
177
+ record = JSON.parse(raw);
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ if (record.keyId !== keyId)
183
+ return undefined;
184
+ let privateParts;
185
+ if (record.encryptedPrivateParts) {
186
+ if (!this.storageKey) {
187
+ throw new KeyStoreError("Key is encrypted at rest; storage key required", 500, "STORAGE_KEY_REQUIRED");
188
+ }
189
+ privateParts = decryptPrivateParts(record.encryptedPrivateParts, record.keyId, record.owner, this.storageKey);
190
+ }
191
+ else if (record.privateParts) {
192
+ privateParts = record.privateParts;
193
+ }
194
+ else {
195
+ throw new KeyStoreError("Key file contains no private key material", 500, "KEY_CORRUPTED");
196
+ }
197
+ const stored = Object.freeze({
198
+ keyId: record.keyId,
199
+ algorithm: record.algorithm,
200
+ owner: record.owner,
201
+ createdAt: record.createdAt,
202
+ publicParts: record.publicParts,
203
+ privateParts,
204
+ });
205
+ this.remember(stored);
206
+ return stored;
207
+ }
208
+ remember(key) {
209
+ this.keys.set(key.keyId, key);
210
+ if (this.keys.size > this.capacity) {
211
+ const oldest = this.keys.keys().next().value;
212
+ this.keys.delete(oldest);
213
+ }
214
+ }
215
+ file(keyId) {
216
+ return path.join(this.dir, `${keyId}.json`);
217
+ }
218
+ }
219
+ exports.KeyStore = KeyStore;
220
+ function keyStoreFromEnv() {
221
+ return new KeyStore(process.env["CRYPTO_KEY_OUT_DIR"] || undefined, exports.DEFAULT_KEY_CAPACITY, process.env["CRYPTO_KEY_STORAGE_KEY"] || undefined);
222
+ }
223
+ //# sourceMappingURL=key-store.js.map
@@ -0,0 +1,34 @@
1
+ import { protocols } from "@sebastienrousseau/crypto-lib";
2
+ type SuiteId = protocols.pake.SuiteId;
3
+ type ServerSetup = protocols.pake.ServerSetup;
4
+ type RegistrationRecord = protocols.pake.RegistrationRecord;
5
+ type ServerLoginState = protocols.pake.ServerLoginState;
6
+ export declare const DEFAULT_OPAQUE_SESSION_TTL_MS: number;
7
+ export interface OpaqueActiveSession {
8
+ readonly serverState: ServerLoginState;
9
+ readonly credentialIdentifier: string;
10
+ readonly suite: SuiteId;
11
+ readonly expiresAt: number;
12
+ }
13
+ export declare class OpaqueStore {
14
+ private readonly setups;
15
+ private readonly fakeRecords;
16
+ private readonly records;
17
+ private readonly sessions;
18
+ getSetup(suite?: SuiteId): ServerSetup;
19
+ getFakeRecord(suite?: SuiteId): RegistrationRecord;
20
+ setRecord(credentialIdentifier: string, record: RegistrationRecord): void;
21
+ getRecord(credentialIdentifier: string): RegistrationRecord | undefined;
22
+ hasRecord(credentialIdentifier: string): boolean;
23
+ createSession(credentialIdentifier: string, serverState: ServerLoginState, suite?: SuiteId, ttlMs?: number): string;
24
+ consumeSession(sessionId: string): OpaqueActiveSession | undefined;
25
+ cleanExpiredSessions(): void;
26
+ clear(): void;
27
+ }
28
+ declare module "fastify" {
29
+ interface FastifyInstance {
30
+ opaqueStore: OpaqueStore;
31
+ }
32
+ }
33
+ export {};
34
+ //# sourceMappingURL=opaque-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-store.d.ts","sourceRoot":"","sources":["../../src/lib/opaque-store.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAE1D,KAAK,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;AACtC,KAAK,WAAW,GAAG,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC;AAC9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,IAAI,CAAC,kBAAkB,CAAC;AAC5D,KAAK,gBAAgB,GAAG,SAAS,CAAC,IAAI,CAAC,gBAAgB,CAAC;AAKxD,eAAO,MAAM,6BAA6B,QAAgB,CAAC;AAG3D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAGD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0C;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IAMnE,QAAQ,CAAC,KAAK,GAAE,OAAuB,GAAG,WAAW;IAarD,aAAa,CAAC,KAAK,GAAE,OAAuB,GAAG,kBAAkB;IAUjE,SAAS,CAAC,oBAAoB,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAKzE,SAAS,CAAC,oBAAoB,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAKvE,SAAS,CAAC,oBAAoB,EAAE,MAAM,GAAG,OAAO;IAKhD,aAAa,CACX,oBAAoB,EAAE,MAAM,EAC5B,WAAW,EAAE,gBAAgB,EAC7B,KAAK,GAAE,OAAuB,EAC9B,KAAK,GAAE,MAAsC,GAC5C,MAAM;IAgBT,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IASlE,oBAAoB,IAAI,IAAI;IAU5B,KAAK,IAAI,IAAI;CAId;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QAEvB,WAAW,EAAE,WAAW,CAAC;KAC1B;CACF"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpaqueStore = exports.DEFAULT_OPAQUE_SESSION_TTL_MS = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
6
+ const { pake } = crypto_lib_1.protocols;
7
+ exports.DEFAULT_OPAQUE_SESSION_TTL_MS = 5 * 60 * 1000;
8
+ class OpaqueStore {
9
+ setups = new Map();
10
+ fakeRecords = new Map();
11
+ records = new Map();
12
+ sessions = new Map();
13
+ getSetup(suite = "P256-SHA256") {
14
+ let setup = this.setups.get(suite);
15
+ if (!setup) {
16
+ setup = pake.createServerSetup({ suite });
17
+ this.setups.set(suite, setup);
18
+ }
19
+ return setup;
20
+ }
21
+ getFakeRecord(suite = "P256-SHA256") {
22
+ let fake = this.fakeRecords.get(suite);
23
+ if (!fake) {
24
+ fake = pake.createFakeRecord({ suite });
25
+ this.fakeRecords.set(suite, fake);
26
+ }
27
+ return fake;
28
+ }
29
+ setRecord(credentialIdentifier, record) {
30
+ this.records.set(credentialIdentifier, record);
31
+ }
32
+ getRecord(credentialIdentifier) {
33
+ return this.records.get(credentialIdentifier);
34
+ }
35
+ hasRecord(credentialIdentifier) {
36
+ return this.records.has(credentialIdentifier);
37
+ }
38
+ createSession(credentialIdentifier, serverState, suite = "P256-SHA256", ttlMs = exports.DEFAULT_OPAQUE_SESSION_TTL_MS) {
39
+ this.cleanExpiredSessions();
40
+ const sessionId = (0, node_crypto_1.randomUUID)();
41
+ this.sessions.set(sessionId, {
42
+ serverState,
43
+ credentialIdentifier,
44
+ suite,
45
+ expiresAt: Date.now() + ttlMs,
46
+ });
47
+ return sessionId;
48
+ }
49
+ consumeSession(sessionId) {
50
+ const session = this.sessions.get(sessionId);
51
+ if (!session)
52
+ return undefined;
53
+ this.sessions.delete(sessionId);
54
+ if (Date.now() > session.expiresAt)
55
+ return undefined;
56
+ return session;
57
+ }
58
+ cleanExpiredSessions() {
59
+ const now = Date.now();
60
+ for (const [id, session] of this.sessions) {
61
+ if (now > session.expiresAt) {
62
+ this.sessions.delete(id);
63
+ }
64
+ }
65
+ }
66
+ clear() {
67
+ this.records.clear();
68
+ this.sessions.clear();
69
+ }
70
+ }
71
+ exports.OpaqueStore = OpaqueStore;
72
+ //# sourceMappingURL=opaque-store.js.map
@@ -0,0 +1,67 @@
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
+ conflict: "Conflict";
19
+ }>;
20
+ export type ProblemSlug = keyof typeof PROBLEM_TITLES;
21
+ export interface Problem {
22
+ type: string;
23
+ title: string;
24
+ status: number;
25
+ detail: string;
26
+ instance?: string;
27
+ [extension: string]: unknown;
28
+ }
29
+ export declare const PROBLEM_SCHEMA: {
30
+ readonly type: "object";
31
+ readonly properties: {
32
+ readonly type: {
33
+ readonly type: "string";
34
+ };
35
+ readonly title: {
36
+ readonly type: "string";
37
+ };
38
+ readonly status: {
39
+ readonly type: "integer";
40
+ };
41
+ readonly detail: {
42
+ readonly type: "string";
43
+ };
44
+ readonly instance: {
45
+ readonly type: "string";
46
+ };
47
+ readonly code: {
48
+ readonly type: "string";
49
+ };
50
+ readonly errors: {
51
+ readonly type: "array";
52
+ };
53
+ };
54
+ };
55
+ export declare function problem(status: number, slug: ProblemSlug, detail: string, extensions?: Record<string, unknown>, instance?: string): Problem;
56
+ export declare function sendProblem(reply: FastifyReply, status: number, slug: ProblemSlug, detail: string, extensions?: Record<string, unknown>): FastifyReply;
57
+ export declare function cryptoErrorStatus(code: string): number;
58
+ export declare function isCryptoError(error: unknown): error is CryptoError;
59
+ export interface ProblemSpec {
60
+ status: number;
61
+ slug: ProblemSlug;
62
+ detail: string;
63
+ extensions: Record<string, unknown>;
64
+ }
65
+ export declare function classifyThrown(error: unknown): ProblemSpec;
66
+ export declare function registerProblemHandlers(app: FastifyInstance): void;
67
+ //# 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;;;;;;;;;;;;;;EAczB,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;AAkBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAGD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAcD,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"}