@sebastienrousseau/crypto-middleware 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 +16 -7
- package/dist/common.d.ts +8 -2
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +76 -30
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +34 -41
- package/dist/fastify.d.ts.map +1 -1
- package/dist/fastify.js +56 -68
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -18
package/README.md
CHANGED
|
@@ -142,16 +142,23 @@ lets you scope protection to specific endpoints.
|
|
|
142
142
|
|
|
143
143
|
### Configuration
|
|
144
144
|
|
|
145
|
-
| Option
|
|
146
|
-
|
|
|
147
|
-
| `key`
|
|
148
|
-
| `routes`
|
|
149
|
-
| `operations`
|
|
150
|
-
| `hmacKey`
|
|
151
|
-
| `jwtSecret`
|
|
145
|
+
| Option | Type | Required | Description |
|
|
146
|
+
| :------------ | :--------- | :------- | :------------------------------------------------ |
|
|
147
|
+
| `key` | `string` | \* | Hex-encoded 256-bit key for encryption/decryption |
|
|
148
|
+
| `routes` | `string[]` | No | Glob patterns for routes to apply middleware to |
|
|
149
|
+
| `operations` | `string[]` | No | Operations to perform (see below) |
|
|
150
|
+
| `hmacKey` | `string` | \* | Hex-encoded HMAC key for signature verification |
|
|
151
|
+
| `jwtSecret` | `string` | \* | Secret for HS256 JWT verification |
|
|
152
|
+
| `jwtIssuer` | `string` | No | Required `iss` claim for `verify-jwt` |
|
|
153
|
+
| `jwtAudience` | `string` | No | Required `aud` value for `verify-jwt` |
|
|
152
154
|
|
|
153
155
|
\* Required when the corresponding operation is enabled.
|
|
154
156
|
|
|
157
|
+
`verify-jwt` accepts only HS256 (any other `alg`, including `none`, is
|
|
158
|
+
rejected), requires a numeric `exp` claim, checks `nbf` when present, and
|
|
159
|
+
checks `iss` and `aud` when `jwtIssuer` / `jwtAudience` are set. Set both
|
|
160
|
+
when the secret is shared with any other service.
|
|
161
|
+
|
|
155
162
|
### Operations
|
|
156
163
|
|
|
157
164
|
| Operation | Description |
|
|
@@ -240,6 +247,8 @@ All 18 packages in the Crypto Service workspace maintain a **100% coverage floor
|
|
|
240
247
|
|
|
241
248
|
Report vulnerabilities privately via [GitHub Security Advisories](https://github.com/sebastienrousseau/crypto-service/security/advisories) or according to [`SECURITY.md`](../../SECURITY.md). Never report security issues publicly.
|
|
242
249
|
|
|
250
|
+
The middleware verifies JWTs and HMAC signatures but does not rate-limit them. Register a rate limiter in the host application (for example [`@fastify/rate-limit`](https://github.com/fastify/fastify-rate-limit) or [`express-rate-limit`](https://github.com/express-rate-limit/express-rate-limit)) in front of the routes it protects, as `crypto-server` does.
|
|
251
|
+
|
|
243
252
|
Cryptographic operations use the `@noble/*` libraries, Node.js `crypto` and OpenPGP.js. `@noble/post-quantum` has not been independently audited and does not guarantee constant-time execution, and no module in this suite is FIPS 140-3 validated. Key zeroization is limited: JavaScript strings and garbage-collected buffers cannot be reliably wiped. See [`SECURITY.md`](../../SECURITY.md).
|
|
244
253
|
|
|
245
254
|
<p align="right"><a href="#contents">Back to Top</a></p>
|
package/dist/common.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { JwtPayload } from "./types";
|
|
1
|
+
import { JwtPayload, MiddlewareConfig } from "./types";
|
|
2
2
|
export declare function encryptPayload(key: string, data: unknown): string;
|
|
3
3
|
export declare function decryptPayload(key: string, sealed: string): unknown;
|
|
4
4
|
export declare function verifyHmacSignature(hmacKey: string, body: string, signature: string): boolean;
|
|
5
|
-
export
|
|
5
|
+
export interface JwtVerifyOptions {
|
|
6
|
+
issuer?: string | undefined;
|
|
7
|
+
audience?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function verifyJwt(jwtSecret: string, token: string, options?: JwtVerifyOptions): JwtPayload;
|
|
10
|
+
export declare function verifyBearerJwt(config: MiddlewareConfig, authorization: string | undefined): JwtPayload;
|
|
11
|
+
export declare function verifyRequestSignature(config: MiddlewareConfig, headers: Record<string, string | string[] | undefined>, body: unknown): void;
|
|
6
12
|
export declare function matchRoute(path: string, routes: string[]): boolean;
|
|
7
13
|
//# sourceMappingURL=common.d.ts.map
|
package/dist/common.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAyB,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAyB,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgB9E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAIjE;AAgBD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAYnE;AAsBD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CA4BT;AAWD,MAAM,WAAW,gBAAgB;IAK/B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAM5B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAiHD,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,gBAAqB,GAC7B,UAAU,CAcZ;AAUD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,gBAAgB,EACxB,aAAa,EAAE,MAAM,GAAG,SAAS,GAChC,UAAU,CAcZ;AAUD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,EACtD,IAAI,EAAE,OAAO,GACZ,IAAI,CAcN;AAoBD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAYlE"}
|
package/dist/common.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.encryptPayload = encryptPayload;
|
|
|
4
4
|
exports.decryptPayload = decryptPayload;
|
|
5
5
|
exports.verifyHmacSignature = verifyHmacSignature;
|
|
6
6
|
exports.verifyJwt = verifyJwt;
|
|
7
|
+
exports.verifyBearerJwt = verifyBearerJwt;
|
|
8
|
+
exports.verifyRequestSignature = verifyRequestSignature;
|
|
7
9
|
exports.matchRoute = matchRoute;
|
|
8
10
|
const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
|
|
9
11
|
const types_1 = require("./types");
|
|
@@ -42,26 +44,32 @@ function base64urlDecode(input) {
|
|
|
42
44
|
const padded = input + "=".repeat((4 - (input.length % 4)) % 4);
|
|
43
45
|
return Buffer.from(padded, "base64").toString("utf8");
|
|
44
46
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
function reject(message, code) {
|
|
48
|
+
throw new types_1.CryptoMiddlewareError(message, 401, code);
|
|
49
|
+
}
|
|
50
|
+
function splitToken(token) {
|
|
51
|
+
if (!token)
|
|
52
|
+
reject("Missing JWT token", "MISSING_TOKEN");
|
|
49
53
|
const parts = token.split(".");
|
|
50
54
|
if (parts.length !== 3) {
|
|
51
|
-
|
|
55
|
+
reject("Malformed JWT: expected 3 parts", "MALFORMED_TOKEN");
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
return parts;
|
|
58
|
+
}
|
|
59
|
+
function decodeObject(segment, name) {
|
|
60
|
+
let value;
|
|
55
61
|
try {
|
|
56
|
-
|
|
62
|
+
value = JSON.parse(base64urlDecode(segment));
|
|
57
63
|
}
|
|
58
64
|
catch {
|
|
59
|
-
|
|
65
|
+
value = undefined;
|
|
60
66
|
}
|
|
61
|
-
if (
|
|
62
|
-
|
|
67
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
68
|
+
reject(`Malformed JWT ${name}`, "MALFORMED_TOKEN");
|
|
63
69
|
}
|
|
64
|
-
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function checkSignature(jwtSecret, signingInput, signatureB64) {
|
|
65
73
|
const keyHex = Buffer.from(jwtSecret, "utf8").toString("hex");
|
|
66
74
|
const computed = (0, crypto_lib_1.computeHmac)({
|
|
67
75
|
algorithm: "sha256",
|
|
@@ -69,33 +77,71 @@ function verifyJwt(jwtSecret, token) {
|
|
|
69
77
|
data: signingInput,
|
|
70
78
|
});
|
|
71
79
|
const sigPadded = signatureB64 + "=".repeat((4 - (signatureB64.length % 4)) % 4);
|
|
72
|
-
const
|
|
80
|
+
const signatureBytes = Buffer.from(sigPadded, "base64");
|
|
73
81
|
const computedBytes = Buffer.from(computed.mac, "hex");
|
|
74
|
-
const signatureBytes = Buffer.from(sigHex, "hex");
|
|
75
82
|
if (!(0, crypto_lib_1.timingSafeEqual)(computedBytes, signatureBytes)) {
|
|
76
|
-
|
|
83
|
+
reject("Invalid JWT signature", "INVALID_TOKEN");
|
|
77
84
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
}
|
|
86
|
+
function checkTimes(payload) {
|
|
87
|
+
const now = Math.floor(Date.now() / 1000);
|
|
88
|
+
if (typeof payload.exp !== "number") {
|
|
89
|
+
reject("JWT has no numeric exp claim", "MISSING_EXPIRATION");
|
|
81
90
|
}
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
if (now >= payload.exp)
|
|
92
|
+
reject("JWT has expired", "TOKEN_EXPIRED");
|
|
93
|
+
if (payload.nbf === undefined)
|
|
94
|
+
return;
|
|
95
|
+
if (typeof payload.nbf !== "number") {
|
|
96
|
+
reject("JWT nbf claim is not a number", "MALFORMED_TOKEN");
|
|
84
97
|
}
|
|
85
|
-
if (payload.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
if (now < payload.nbf)
|
|
99
|
+
reject("JWT is not yet valid", "TOKEN_NOT_YET_VALID");
|
|
100
|
+
}
|
|
101
|
+
function checkIssuerAudience(payload, options) {
|
|
102
|
+
if (options.issuer !== undefined && payload.iss !== options.issuer) {
|
|
103
|
+
reject("JWT issuer is not accepted", "INVALID_ISSUER");
|
|
90
104
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
if (options.audience === undefined)
|
|
106
|
+
return;
|
|
107
|
+
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
108
|
+
if (!aud.includes(options.audience)) {
|
|
109
|
+
reject("JWT audience is not accepted", "INVALID_AUDIENCE");
|
|
96
110
|
}
|
|
111
|
+
}
|
|
112
|
+
function verifyJwt(jwtSecret, token, options = {}) {
|
|
113
|
+
const [headerB64, payloadB64, signatureB64] = splitToken(token);
|
|
114
|
+
const header = decodeObject(headerB64, "header");
|
|
115
|
+
if (header["alg"] !== "HS256") {
|
|
116
|
+
reject(`Unsupported JWT algorithm: ${String(header["alg"])}. Only HS256 is supported.`, "UNSUPPORTED_ALGORITHM");
|
|
117
|
+
}
|
|
118
|
+
checkSignature(jwtSecret, `${headerB64}.${payloadB64}`, signatureB64);
|
|
119
|
+
const payload = decodeObject(payloadB64, "payload");
|
|
120
|
+
checkTimes(payload);
|
|
121
|
+
checkIssuerAudience(payload, options);
|
|
97
122
|
return payload;
|
|
98
123
|
}
|
|
124
|
+
function verifyBearerJwt(config, authorization) {
|
|
125
|
+
if (!config.jwtSecret) {
|
|
126
|
+
throw new types_1.CryptoMiddlewareError("jwtSecret is required for verify-jwt operation", 500, "MISSING_CONFIG");
|
|
127
|
+
}
|
|
128
|
+
const header = authorization ?? "";
|
|
129
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
130
|
+
return verifyJwt(config.jwtSecret, token, {
|
|
131
|
+
issuer: config.jwtIssuer,
|
|
132
|
+
audience: config.jwtAudience,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function verifyRequestSignature(config, headers, body) {
|
|
136
|
+
if (!config.hmacKey) {
|
|
137
|
+
throw new types_1.CryptoMiddlewareError("hmacKey is required for verify-signature operation", 500, "MISSING_CONFIG");
|
|
138
|
+
}
|
|
139
|
+
const signature = headers["x-signature"] ??
|
|
140
|
+
headers["x-hub-signature-256"] ??
|
|
141
|
+
"";
|
|
142
|
+
const rawBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
143
|
+
verifyHmacSignature(config.hmacKey, rawBody, signature);
|
|
144
|
+
}
|
|
99
145
|
function matchRoute(path, routes) {
|
|
100
146
|
if (routes.length === 0)
|
|
101
147
|
return true;
|
package/dist/express.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAyB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAyB,MAAM,SAAS,CAAC;AAwFlE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,IAK3D,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,IAAI,CAqBR"}
|
package/dist/express.js
CHANGED
|
@@ -3,6 +3,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createCryptoMiddleware = createCryptoMiddleware;
|
|
4
4
|
const types_1 = require("./types");
|
|
5
5
|
const common_1 = require("./common");
|
|
6
|
+
function requireKey(config, operation) {
|
|
7
|
+
if (!config.key) {
|
|
8
|
+
throw new types_1.CryptoMiddlewareError(`key is required for ${operation} operation`, 500, "MISSING_CONFIG");
|
|
9
|
+
}
|
|
10
|
+
return config.key;
|
|
11
|
+
}
|
|
12
|
+
function decryptBody(config, req) {
|
|
13
|
+
const key = requireKey(config, "decrypt-request");
|
|
14
|
+
if (req.body && typeof req.body === "object" && "encrypted" in req.body) {
|
|
15
|
+
req.body = (0, common_1.decryptPayload)(key, req.body.encrypted);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function encryptJsonResponses(config, res) {
|
|
19
|
+
const key = requireKey(config, "encrypt-response");
|
|
20
|
+
const originalJson = res.json.bind(res);
|
|
21
|
+
res.json = function encryptedJson(body) {
|
|
22
|
+
const sealed = (0, common_1.encryptPayload)(key, body);
|
|
23
|
+
return originalJson({ encrypted: sealed });
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function applyOperations(config, operations, req, res) {
|
|
27
|
+
if (operations.includes("verify-jwt")) {
|
|
28
|
+
req.jwtPayload = (0, common_1.verifyBearerJwt)(config, req.headers.authorization);
|
|
29
|
+
}
|
|
30
|
+
if (operations.includes("verify-signature")) {
|
|
31
|
+
(0, common_1.verifyRequestSignature)(config, req.headers, req.body);
|
|
32
|
+
}
|
|
33
|
+
if (operations.includes("decrypt-request"))
|
|
34
|
+
decryptBody(config, req);
|
|
35
|
+
if (operations.includes("encrypt-response")) {
|
|
36
|
+
encryptJsonResponses(config, res);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
6
39
|
function createCryptoMiddleware(config) {
|
|
7
40
|
const operations = config.operations ?? [];
|
|
8
41
|
const routes = config.routes ?? [];
|
|
@@ -12,47 +45,7 @@ function createCryptoMiddleware(config) {
|
|
|
12
45
|
return;
|
|
13
46
|
}
|
|
14
47
|
try {
|
|
15
|
-
|
|
16
|
-
if (!config.jwtSecret) {
|
|
17
|
-
throw new types_1.CryptoMiddlewareError("jwtSecret is required for verify-jwt operation", 500, "MISSING_CONFIG");
|
|
18
|
-
}
|
|
19
|
-
const authHeader = req.headers.authorization ?? "";
|
|
20
|
-
const token = authHeader.startsWith("Bearer ")
|
|
21
|
-
? authHeader.slice(7)
|
|
22
|
-
: "";
|
|
23
|
-
const payload = (0, common_1.verifyJwt)(config.jwtSecret, token);
|
|
24
|
-
req.jwtPayload = payload;
|
|
25
|
-
}
|
|
26
|
-
if (operations.includes("verify-signature")) {
|
|
27
|
-
if (!config.hmacKey) {
|
|
28
|
-
throw new types_1.CryptoMiddlewareError("hmacKey is required for verify-signature operation", 500, "MISSING_CONFIG");
|
|
29
|
-
}
|
|
30
|
-
const signature = req.headers["x-signature"] ??
|
|
31
|
-
req.headers["x-hub-signature-256"] ??
|
|
32
|
-
"";
|
|
33
|
-
const rawBody = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
34
|
-
(0, common_1.verifyHmacSignature)(config.hmacKey, rawBody, signature);
|
|
35
|
-
}
|
|
36
|
-
if (operations.includes("decrypt-request")) {
|
|
37
|
-
if (!config.key) {
|
|
38
|
-
throw new types_1.CryptoMiddlewareError("key is required for decrypt-request operation", 500, "MISSING_CONFIG");
|
|
39
|
-
}
|
|
40
|
-
if (req.body &&
|
|
41
|
-
typeof req.body === "object" &&
|
|
42
|
-
"encrypted" in req.body) {
|
|
43
|
-
req.body = (0, common_1.decryptPayload)(config.key, req.body.encrypted);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (operations.includes("encrypt-response")) {
|
|
47
|
-
if (!config.key) {
|
|
48
|
-
throw new types_1.CryptoMiddlewareError("key is required for encrypt-response operation", 500, "MISSING_CONFIG");
|
|
49
|
-
}
|
|
50
|
-
const originalJson = res.json.bind(res);
|
|
51
|
-
res.json = function encryptedJson(body) {
|
|
52
|
-
const sealed = (0, common_1.encryptPayload)(config.key, body);
|
|
53
|
-
return originalJson({ encrypted: sealed });
|
|
54
|
-
};
|
|
55
|
-
}
|
|
48
|
+
applyOperations(config, operations, req, res);
|
|
56
49
|
next();
|
|
57
50
|
}
|
|
58
51
|
catch (err) {
|
package/dist/fastify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAyB,MAAM,SAAS,CAAC;AA8HlE,iBAAe,gBAAgB,CAC7B,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAGD,eAAO,MAAM,YAAY,yBAGvB,CAAC"}
|
package/dist/fastify.js
CHANGED
|
@@ -7,94 +7,82 @@ exports.cryptoPlugin = void 0;
|
|
|
7
7
|
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
|
|
8
8
|
const types_1 = require("./types");
|
|
9
9
|
const common_1 = require("./common");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
function sendMiddlewareError(reply, err) {
|
|
11
|
+
if (err instanceof types_1.CryptoMiddlewareError) {
|
|
12
|
+
reply.code(err.statusCode).send({
|
|
13
|
+
error: err.message,
|
|
14
|
+
code: err.code,
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
function registerVerification(fastify, opts, operations, routes) {
|
|
13
21
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
14
22
|
if (!(0, common_1.matchRoute)(request.url, routes))
|
|
15
23
|
return;
|
|
16
24
|
try {
|
|
17
25
|
if (operations.includes("verify-jwt")) {
|
|
18
|
-
|
|
19
|
-
throw new types_1.CryptoMiddlewareError("jwtSecret is required for verify-jwt operation", 500, "MISSING_CONFIG");
|
|
20
|
-
}
|
|
21
|
-
const authHeader = request.headers.authorization ?? "";
|
|
22
|
-
const token = authHeader.startsWith("Bearer ")
|
|
23
|
-
? authHeader.slice(7)
|
|
24
|
-
: "";
|
|
25
|
-
const payload = (0, common_1.verifyJwt)(opts.jwtSecret, token);
|
|
26
|
+
const payload = (0, common_1.verifyBearerJwt)(opts, request.headers.authorization);
|
|
26
27
|
request.jwtPayload = payload;
|
|
27
28
|
}
|
|
28
29
|
if (operations.includes("verify-signature")) {
|
|
29
|
-
|
|
30
|
-
throw new types_1.CryptoMiddlewareError("hmacKey is required for verify-signature operation", 500, "MISSING_CONFIG");
|
|
31
|
-
}
|
|
32
|
-
const signature = request.headers["x-signature"] ??
|
|
33
|
-
request.headers["x-hub-signature-256"] ??
|
|
34
|
-
"";
|
|
35
|
-
const rawBody = typeof request.body === "string"
|
|
36
|
-
? request.body
|
|
37
|
-
: JSON.stringify(request.body);
|
|
38
|
-
(0, common_1.verifyHmacSignature)(opts.hmacKey, rawBody, signature);
|
|
30
|
+
(0, common_1.verifyRequestSignature)(opts, request.headers, request.body);
|
|
39
31
|
}
|
|
40
32
|
}
|
|
41
33
|
catch (err) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
sendMiddlewareError(reply, err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function registerDecryption(fastify, opts, routes) {
|
|
39
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
40
|
+
if (!(0, common_1.matchRoute)(request.url, routes))
|
|
41
|
+
return;
|
|
42
|
+
if (!opts.key) {
|
|
43
|
+
reply.code(500).send({
|
|
44
|
+
error: "key is required for decrypt-request operation",
|
|
45
|
+
code: "MISSING_CONFIG",
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const body = request.body;
|
|
51
|
+
if (body && typeof body === "object" && "encrypted" in body) {
|
|
52
|
+
request.body = (0, common_1.decryptPayload)(opts.key, body.encrypted);
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
sendMiddlewareError(reply, err);
|
|
50
57
|
}
|
|
51
58
|
});
|
|
59
|
+
}
|
|
60
|
+
function registerEncryption(fastify, opts, routes) {
|
|
61
|
+
fastify.addHook("preSerialization", async (request, _reply, payload) => {
|
|
62
|
+
if (!(0, common_1.matchRoute)(request.url, routes))
|
|
63
|
+
return payload;
|
|
64
|
+
if (!opts.key)
|
|
65
|
+
return payload;
|
|
66
|
+
if (payload !== null && typeof payload === "object") {
|
|
67
|
+
const sealed = (0, common_1.encryptPayload)(opts.key, payload);
|
|
68
|
+
return { encrypted: sealed };
|
|
69
|
+
}
|
|
70
|
+
return payload;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function cryptoPluginImpl(fastify, opts) {
|
|
74
|
+
const operations = opts.operations ?? [];
|
|
75
|
+
const routes = opts.routes ?? [];
|
|
76
|
+
registerVerification(fastify, opts, operations, routes);
|
|
52
77
|
if (operations.includes("decrypt-request")) {
|
|
53
|
-
fastify
|
|
54
|
-
if (!(0, common_1.matchRoute)(request.url, routes))
|
|
55
|
-
return;
|
|
56
|
-
if (!opts.key) {
|
|
57
|
-
reply.code(500).send({
|
|
58
|
-
error: "key is required for decrypt-request operation",
|
|
59
|
-
code: "MISSING_CONFIG",
|
|
60
|
-
});
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const body = request.body;
|
|
65
|
-
if (body && typeof body === "object" && "encrypted" in body) {
|
|
66
|
-
request.body =
|
|
67
|
-
(0, common_1.decryptPayload)(opts.key, body.encrypted);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
if (err instanceof types_1.CryptoMiddlewareError) {
|
|
72
|
-
reply.code(err.statusCode).send({
|
|
73
|
-
error: err.message,
|
|
74
|
-
code: err.code,
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
});
|
|
78
|
+
registerDecryption(fastify, opts, routes);
|
|
81
79
|
}
|
|
82
80
|
if (operations.includes("encrypt-response")) {
|
|
83
|
-
fastify
|
|
84
|
-
if (!(0, common_1.matchRoute)(request.url, routes))
|
|
85
|
-
return payload;
|
|
86
|
-
if (!opts.key)
|
|
87
|
-
return payload;
|
|
88
|
-
if (payload !== null && typeof payload === "object") {
|
|
89
|
-
const sealed = (0, common_1.encryptPayload)(opts.key, payload);
|
|
90
|
-
return { encrypted: sealed };
|
|
91
|
-
}
|
|
92
|
-
return payload;
|
|
93
|
-
});
|
|
81
|
+
registerEncryption(fastify, opts, routes);
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
exports.cryptoPlugin = (0, fastify_plugin_1.default)(cryptoPluginImpl, {
|
|
97
85
|
name: "crypto-middleware",
|
|
98
|
-
fastify: "4.x",
|
|
86
|
+
fastify: "4.x || 5.x",
|
|
99
87
|
});
|
|
100
88
|
//# sourceMappingURL=fastify.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { MiddlewareConfig, JwtPayload } from "./types";
|
|
2
2
|
export { CryptoMiddlewareError } from "./types";
|
|
3
|
+
export type { JwtVerifyOptions } from "./common";
|
|
3
4
|
export { encryptPayload, decryptPayload, verifyHmacSignature, verifyJwt, matchRoute, } from "./common";
|
|
4
5
|
export { createCryptoMiddleware } from "./express";
|
|
5
6
|
export { cryptoPlugin } from "./fastify";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,OAAO,EACL,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface MiddlewareConfig {
|
|
|
4
4
|
operations?: Array<"decrypt-request" | "encrypt-response" | "verify-signature" | "verify-jwt">;
|
|
5
5
|
hmacKey?: string;
|
|
6
6
|
jwtSecret?: string;
|
|
7
|
+
jwtIssuer?: string;
|
|
8
|
+
jwtAudience?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface JwtPayload {
|
|
9
11
|
sub?: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,gBAAgB;IAE/B,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,UAAU,CAAC,EAAE,KAAK,CAChB,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,YAAY,CAC3E,CAAC;IAEF,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,gBAAgB;IAE/B,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,UAAU,CAAC,EAAE,KAAK,CAChB,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,YAAY,CAC3E,CAAC;IAEF,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAcD,MAAM,WAAW,UAAU;IAEzB,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAKxB,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAUD,qBAAa,qBAAsB,SAAQ,KAAK;IAE9C,SAAgB,UAAU,EAAE,MAAM,CAAC;IAEnC,SAAgB,IAAI,EAAE,MAAM,CAAC;gBAGjB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAM9D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebastienrousseau/crypto-middleware",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Plug-and-play cryptographic middleware for Express and Fastify -- auto-decrypt requests, verify signatures, and encrypt responses.",
|
|
5
5
|
"author": "Sebastien Rousseau <sebastienrousseau@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -16,28 +16,30 @@
|
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=22.0.0"
|
|
18
18
|
},
|
|
19
|
+
"type": "commonjs",
|
|
19
20
|
"main": "./dist/index.js",
|
|
20
21
|
"types": "./dist/index.d.ts",
|
|
21
|
-
"sideEffects": false,
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
24
24
|
"types": "./dist/index.d.ts",
|
|
25
25
|
"require": "./dist/index.js",
|
|
26
|
-
"
|
|
27
|
-
}
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
28
29
|
},
|
|
30
|
+
"sideEffects": false,
|
|
29
31
|
"files": [
|
|
30
32
|
"dist/**/*.js",
|
|
31
33
|
"dist/**/*.d.ts",
|
|
32
34
|
"dist/**/*.d.ts.map"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"
|
|
37
|
+
"fastify-plugin": "^6.0.0",
|
|
38
|
+
"@sebastienrousseau/crypto-lib": "0.0.9"
|
|
36
39
|
},
|
|
37
40
|
"peerDependencies": {
|
|
38
41
|
"express": "^5.2.1",
|
|
39
|
-
"fastify": "^4.0.0"
|
|
40
|
-
"fastify-plugin": "^4.0.0"
|
|
42
|
+
"fastify": "^4.0.0 || ^5.0.0"
|
|
41
43
|
},
|
|
42
44
|
"peerDependenciesMeta": {
|
|
43
45
|
"express": {
|
|
@@ -45,25 +47,21 @@
|
|
|
45
47
|
},
|
|
46
48
|
"fastify": {
|
|
47
49
|
"optional": true
|
|
48
|
-
},
|
|
49
|
-
"fastify-plugin": {
|
|
50
|
-
"optional": true
|
|
51
50
|
}
|
|
52
51
|
},
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@sebastienrousseau/mocha-config": "^0.0.5",
|
|
55
|
-
"@types/chai": "^
|
|
54
|
+
"@types/chai": "^5.2.3",
|
|
56
55
|
"@types/chai-as-promised": "^8.0.2",
|
|
57
56
|
"@types/express": "^5.0.6",
|
|
58
|
-
"@types/mocha": "^10.0.
|
|
57
|
+
"@types/mocha": "^10.0.10",
|
|
59
58
|
"@types/node": "^26.6.3",
|
|
60
59
|
"c8": "^12.0.0",
|
|
61
|
-
"chai": "^
|
|
62
|
-
"chai-as-promised": "^
|
|
60
|
+
"chai": "^6.3.0",
|
|
61
|
+
"chai-as-promised": "^8.0.2",
|
|
63
62
|
"express": "^5.2.1",
|
|
64
63
|
"fastify": "^5.12.5",
|
|
65
|
-
"
|
|
66
|
-
"mocha": "^10.8.2",
|
|
64
|
+
"mocha": "^12.0.3",
|
|
67
65
|
"rimraf": "^6.1.3",
|
|
68
66
|
"ts-node": "^10.9.2",
|
|
69
67
|
"typedoc": "^0.28.20",
|
|
@@ -88,8 +86,8 @@
|
|
|
88
86
|
"clean": "rimraf ./coverage ./dist ./docs",
|
|
89
87
|
"test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --reporter=lcov --reporter=text mocha --config=.mocharc.cjs",
|
|
90
88
|
"docs": "typedoc",
|
|
91
|
-
"lint": "eslint
|
|
92
|
-
"lint:fix": "eslint --cache --fix
|
|
89
|
+
"lint": "eslint src __tests__",
|
|
90
|
+
"lint:fix": "eslint --cache --fix src __tests__",
|
|
93
91
|
"format": "prettier --write src/**/*.ts",
|
|
94
92
|
"format:check": "prettier --check \"src/**/*.ts\""
|
|
95
93
|
}
|