badmfck-api-server 5.0.6 → 5.0.7

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.
@@ -1,7 +1,18 @@
1
1
  import { IInterceptor } from "../APIService";
2
2
  import { HTTPRequestVO } from "../structures/Interfaces";
3
3
  export declare class InterceptorPayAuth implements IInterceptor<any> {
4
- intercept(req: HTTPRequestVO, authorizations: {
4
+ private static readonly TS_PAST_SEC;
5
+ private static readonly TS_FUTURE_SEC;
6
+ private static readonly NONCE_TTL_MS;
7
+ private static readonly NONCE_MAX;
8
+ private nonceSeen;
9
+ private authorizationsPreset;
10
+ constructor(authorizations?: {
11
+ key: string;
12
+ kid: string;
13
+ }[]);
14
+ private purgeNonces;
15
+ intercept(req: HTTPRequestVO, authorizations?: {
5
16
  key: string;
6
17
  kid: string;
7
18
  }[]): Promise<any>;
@@ -17,6 +17,28 @@ const _TAuthParams = {
17
17
  sig: ""
18
18
  };
19
19
  class InterceptorPayAuth {
20
+ static TS_PAST_SEC = 300;
21
+ static TS_FUTURE_SEC = 60;
22
+ static NONCE_TTL_MS = (InterceptorPayAuth.TS_PAST_SEC + InterceptorPayAuth.TS_FUTURE_SEC) * 1000;
23
+ static NONCE_MAX = 100_000;
24
+ nonceSeen = new Map();
25
+ authorizationsPreset;
26
+ constructor(authorizations) {
27
+ this.authorizationsPreset = authorizations;
28
+ }
29
+ purgeNonces(nowMs) {
30
+ for (const [nonce, expiresAt] of this.nonceSeen) {
31
+ if (expiresAt > nowMs)
32
+ break;
33
+ this.nonceSeen.delete(nonce);
34
+ }
35
+ while (this.nonceSeen.size > InterceptorPayAuth.NONCE_MAX) {
36
+ const oldest = this.nonceSeen.keys().next().value;
37
+ if (oldest === undefined)
38
+ break;
39
+ this.nonceSeen.delete(oldest);
40
+ }
41
+ }
20
42
  async intercept(req, authorizations) {
21
43
  const authScheme = "payauth ";
22
44
  let headerData = req.headers["authorization"];
@@ -39,11 +61,13 @@ class InterceptorPayAuth {
39
61
  const params = {};
40
62
  headerData.substring(authScheme.length).trim().split(",").forEach(v => {
41
63
  v = v.trim();
42
- const tmp = v.split("=");
43
- if (tmp.length === 2) {
44
- const key = tmp[0].trim().toLowerCase();
45
- let value = tmp[1].trim();
46
- if (value.startsWith("'") && value.endsWith("'")) {
64
+ const eq = v.indexOf("=");
65
+ if (eq > 0) {
66
+ const key = v.substring(0, eq).trim().toLowerCase();
67
+ let value = v.substring(eq + 1).trim();
68
+ if (value.length > 1 &&
69
+ ((value.startsWith("'") && value.endsWith("'")) ||
70
+ (value.startsWith('"') && value.endsWith('"')))) {
47
71
  value = value.substring(1, value.length - 1);
48
72
  }
49
73
  params[key] = value;
@@ -52,15 +76,38 @@ class InterceptorPayAuth {
52
76
  const errors = await Validator_1.Validator.validateStructure(_TAuthParams, params);
53
77
  if (errors)
54
78
  throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "Malformed authorization header", stack: errors };
79
+ if (!/^\d{10}$/.test(params.ts) && !/^\d{13}$/.test(params.ts))
80
+ throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "ts must be Unix epoch seconds (10 digits) or milliseconds (13 digits), UTC" };
81
+ const nowMs = Date.now();
82
+ const nowSec = Math.floor(nowMs / 1000);
83
+ const tsSec = params.ts.length === 13
84
+ ? Math.floor(parseInt(params.ts, 10) / 1000)
85
+ : parseInt(params.ts, 10);
86
+ const delta = tsSec - nowSec;
87
+ if (delta < -InterceptorPayAuth.TS_PAST_SEC || delta > InterceptorPayAuth.TS_FUTURE_SEC)
88
+ throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: `ts out of window: off by ${delta}s (server epoch ${nowSec})` };
89
+ this.purgeNonces(nowMs);
90
+ if (this.nonceSeen.has(params.nonce))
91
+ throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "Duplicate nonce" };
92
+ if (!authorizations || authorizations.length === 0) {
93
+ if (this.authorizationsPreset && this.authorizationsPreset.length > 0)
94
+ authorizations = this.authorizationsPreset;
95
+ else
96
+ throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "No authorizations provided" };
97
+ }
55
98
  for (let i of authorizations) {
56
99
  if (i.kid === params.kid) {
57
- if (params.alg.toLowerCase() === "hmac-sha256") {
100
+ if (params.alg && params.alg.toLowerCase() === "hmac-sha256") {
58
101
  if (b64) {
59
102
  const key = Buffer.from(i.key, "hex");
60
103
  const sig = crypto_1.default.createHmac("sha256", key).update(b64 + params.ts + params.nonce).digest("hex");
61
- if (sig === params.sig)
104
+ const got = Buffer.from(String(params.sig), "utf-8");
105
+ const want = Buffer.from(sig, "utf-8");
106
+ if (got.length === want.length && crypto_1.default.timingSafeEqual(got, want)) {
107
+ this.nonceSeen.set(params.nonce, nowMs + InterceptorPayAuth.NONCE_TTL_MS);
62
108
  return i;
63
- throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "Signature mismatch, calc:" + sig + ", got:" + params.sig, stack: params };
109
+ }
110
+ throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "Signature mismatch" };
64
111
  }
65
112
  else {
66
113
  throw { ...DefaultErrors_1.default.UNAUTHORIZED, details: "Can't calc signature, no b64" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badmfck-api-server",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "description": "Simple API http server based on express",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",