@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.
- package/README.md +159 -56
- package/dist/@types/types.d.ts +1 -2
- package/dist/@types/types.d.ts.map +1 -1
- package/dist/config/auth-policy.d.ts +18 -0
- package/dist/config/auth-policy.d.ts.map +1 -1
- package/dist/config/auth-policy.js +109 -0
- package/dist/enterprise/metering.d.ts.map +1 -1
- package/dist/enterprise/metering.js +2 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/auth.d.ts +4 -1
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +87 -40
- package/dist/lib/kdf-runner.d.ts +18 -0
- package/dist/lib/kdf-runner.d.ts.map +1 -0
- package/dist/lib/kdf-runner.js +27 -0
- package/dist/lib/key-store.d.ts +30 -0
- package/dist/lib/key-store.d.ts.map +1 -0
- package/dist/lib/key-store.js +126 -0
- package/dist/lib/problem.d.ts +66 -0
- package/dist/lib/problem.d.ts.map +1 -0
- package/dist/lib/problem.js +152 -0
- package/dist/routes/v1/decrypt.d.ts.map +1 -1
- package/dist/routes/v1/decrypt.js +20 -15
- package/dist/routes/v1/encrypt.d.ts.map +1 -1
- package/dist/routes/v1/encrypt.js +12 -13
- package/dist/routes/v1/generate.d.ts.map +1 -1
- package/dist/routes/v1/generate.js +22 -25
- package/dist/routes/v1/revoke.d.ts.map +1 -1
- package/dist/routes/v1/revoke.js +6 -11
- package/dist/routes/v1/verify.d.ts.map +1 -1
- package/dist/routes/v1/verify.js +6 -11
- package/dist/routes/v2/algorithms.js +1 -1
- package/dist/routes/v2/compliance.js +1 -1
- package/dist/routes/v2/encrypt.d.ts.map +1 -1
- package/dist/routes/v2/encrypt.js +1 -1
- package/dist/routes/v2/hash.d.ts.map +1 -1
- package/dist/routes/v2/hash.js +1 -1
- package/dist/routes/v2/kdf.d.ts +5 -0
- package/dist/routes/v2/kdf.d.ts.map +1 -1
- package/dist/routes/v2/kdf.js +27 -12
- package/dist/routes/v2/key-wrap.d.ts.map +1 -1
- package/dist/routes/v2/key-wrap.js +9 -3
- package/dist/routes/v2/keys.d.ts.map +1 -1
- package/dist/routes/v2/keys.js +39 -26
- package/dist/routes/v2/mac.d.ts.map +1 -1
- package/dist/routes/v2/mac.js +9 -3
- package/dist/routes/v2/multi-recipient.d.ts.map +1 -1
- package/dist/routes/v2/multi-recipient.js +12 -11
- package/dist/routes/v2/password-encrypt.d.ts.map +1 -1
- package/dist/routes/v2/password-encrypt.js +13 -46
- package/dist/routes/v2/password.d.ts +4 -0
- package/dist/routes/v2/password.d.ts.map +1 -1
- package/dist/routes/v2/password.js +28 -53
- package/dist/routes/v2/pq-hash-sign.d.ts.map +1 -1
- package/dist/routes/v2/pq-hash-sign.js +30 -24
- package/dist/routes/v2/pq-sign.d.ts.map +1 -1
- package/dist/routes/v2/pq-sign.js +30 -24
- package/dist/routes/v2/pq.d.ts.map +1 -1
- package/dist/routes/v2/pq.js +57 -38
- package/dist/routes/v2/sealedbox.d.ts.map +1 -1
- package/dist/routes/v2/sealedbox.js +32 -14
- package/dist/routes/v2/secretbox.d.ts.map +1 -1
- package/dist/routes/v2/secretbox.js +10 -3
- package/dist/routes/v2/signing.d.ts.map +1 -1
- package/dist/routes/v2/signing.js +16 -13
- package/dist/routes/v2/stream.d.ts.map +1 -1
- package/dist/routes/v2/stream.js +29 -24
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -5
- package/dist/utils/keys.d.ts +17 -0
- package/dist/utils/keys.d.ts.map +1 -0
- package/dist/utils/keys.js +36 -0
- package/dist/utils/route-helpers.d.ts +1 -5
- package/dist/utils/route-helpers.d.ts.map +1 -1
- package/dist/utils/route-helpers.js +16 -10
- package/dist/utils/validation.d.ts +1 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +17 -9
- 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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|