@eggjs/cookies 3.0.1 → 4.0.0-beta.13

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 CHANGED
@@ -1,16 +1,10 @@
1
1
  # @eggjs/cookies
2
2
 
3
3
  [![NPM version][npm-image]][npm-url]
4
- [![build status][ci-image]][ci-url]
5
- [![Test coverage][codecov-image]][codecov-url]
6
4
  [![npm download][download-image]][download-url]
7
5
 
8
6
  [npm-image]: https://img.shields.io/npm/v/@eggjs/cookies.svg?style=flat-square
9
7
  [npm-url]: https://npmjs.org/package/@eggjs/cookies
10
- [ci-image]: https://github.com/eggjs/egg-cookies/actions/workflows/nodejs.yml/badge.svg
11
- [ci-url]: https://github.com/eggjs/egg-cookies/actions/workflows/nodejs.yml
12
- [codecov-image]: https://codecov.io/gh/eggjs/egg-cookies/branch/master/graph/badge.svg
13
- [codecov-url]: https://codecov.io/gh/eggjs/egg-cookies
14
8
  [download-image]: https://img.shields.io/npm/dm/@eggjs/cookies.svg?style=flat-square
15
9
  [download-url]: https://npmjs.org/package/@eggjs/cookies
16
10
 
@@ -53,6 +47,6 @@ cookies.set('foo', longText);
53
47
 
54
48
  ## Contributors
55
49
 
56
- [![Contributors](https://contrib.rocks/image?repo=eggjs/egg-cookies)](https://github.com/eggjs/egg-cookies/graphs/contributors)
50
+ [![Contributors](https://contrib.rocks/image?repo=eggjs/egg)](https://github.com/eggjs/egg/graphs/contributors)
57
51
 
58
52
  Made with [contributors-img](https://contrib.rocks).
package/README.zh-CN.md CHANGED
@@ -1,16 +1,10 @@
1
1
  # @eggjs/cookies
2
2
 
3
3
  [![NPM version][npm-image]][npm-url]
4
- [![build status][ci-image]][ci-url]
5
- [![Test coverage][codecov-image]][codecov-url]
6
4
  [![npm download][download-image]][download-url]
7
5
 
8
6
  [npm-image]: https://img.shields.io/npm/v/@eggjs/cookies.svg?style=flat-square
9
7
  [npm-url]: https://npmjs.org/package/@eggjs/cookies
10
- [ci-image]: https://github.com/eggjs/egg-cookies/actions/workflows/nodejs.yml/badge.svg
11
- [ci-url]: https://github.com/eggjs/egg-cookies/actions/workflows/nodejs.yml
12
- [codecov-image]: https://codecov.io/gh/eggjs/egg-cookies/branch/master/graph/badge.svg
13
- [codecov-url]: https://codecov.io/gh/eggjs/egg-cookies
14
8
  [download-image]: https://img.shields.io/npm/dm/@eggjs/cookies.svg?style=flat-square
15
9
  [download-url]: https://npmjs.org/package/@eggjs/cookies
16
10
 
@@ -34,8 +28,8 @@ ctx.cookies.set('key', 'value', options);
34
28
  全局默认配置:
35
29
 
36
30
  - autoChips - `Boolean` 是否开启 [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips#security_design) 的自动适配方案,
37
- 会自动给 Cookie 新增一个 `_CHIPS-` 为前缀的分区 Cookie,优先读取非分区 Cookie,读取失败则尝试读取 `_CHIPS-` 前缀的同名 Cookie 适配三方 Cookie 禁止逻辑。
38
- 一旦 `cookies.set` 设置 `partitioned=true`,那么会强制忽略 `autoChips` 参数。
31
+ 会自动给 Cookie 新增一个 `_CHIPS-` 为前缀的分区 Cookie,优先读取非分区 Cookie,读取失败则尝试读取 `_CHIPS-` 前缀的同名 Cookie 适配三方 Cookie 禁止逻辑。
32
+ 一旦 `cookies.set` 设置 `partitioned=true`,那么会强制忽略 `autoChips` 参数。
39
33
 
40
34
  ## 设置 cookie
41
35
 
@@ -71,6 +65,6 @@ ctx.cookies.set('key', 'value', options);
71
65
 
72
66
  ## Contributors
73
67
 
74
- [![Contributors](https://contrib.rocks/image?repo=eggjs/egg-cookies)](https://github.com/eggjs/egg-cookies/graphs/contributors)
68
+ [![Contributors](https://contrib.rocks/image?repo=eggjs/egg)](https://github.com/eggjs/egg/graphs/contributors)
75
69
 
76
70
  Made with [contributors-img](https://contrib.rocks).
@@ -0,0 +1,69 @@
1
+ //#region src/cookie.d.ts
2
+ interface CookieSetOptions {
3
+ /**
4
+ * The path for the cookie to be set in
5
+ */
6
+ path?: string | null;
7
+ /**
8
+ * The domain for the cookie
9
+ */
10
+ domain?: string | (() => string);
11
+ /**
12
+ * Is overridable
13
+ */
14
+ overwrite?: boolean;
15
+ /**
16
+ * Is the same site
17
+ */
18
+ sameSite?: string | boolean;
19
+ /**
20
+ * Encrypt the cookie's value or not
21
+ */
22
+ encrypt?: boolean;
23
+ /**
24
+ * Max age for browsers
25
+ */
26
+ maxAge?: number;
27
+ /**
28
+ * Expire time
29
+ */
30
+ expires?: Date;
31
+ /**
32
+ * Is for http only
33
+ */
34
+ httpOnly?: boolean;
35
+ /**
36
+ * Encrypt the cookie's value or not
37
+ */
38
+ secure?: boolean;
39
+ /**
40
+ * Once `true` and secure set to `true`, ignore the secure error in a none-ssl environment.
41
+ */
42
+ ignoreSecureError?: boolean;
43
+ /**
44
+ * Is it signed or not.
45
+ */
46
+ signed?: boolean | number;
47
+ /**
48
+ * Is it partitioned or not.
49
+ */
50
+ partitioned?: boolean;
51
+ /**
52
+ * Remove unpartitioned same name cookie or not.
53
+ */
54
+ removeUnpartitioned?: boolean;
55
+ /**
56
+ * The cookie priority.
57
+ */
58
+ priority?: 'low' | 'medium' | 'high' | 'LOW' | 'MEDIUM' | 'HIGH';
59
+ }
60
+ declare class Cookie {
61
+ name: string;
62
+ value: string;
63
+ readonly attrs: CookieSetOptions;
64
+ constructor(name: string, value?: string | null, attrs?: CookieSetOptions);
65
+ toString(): string;
66
+ toHeader(): string;
67
+ }
68
+ //#endregion
69
+ export { Cookie, CookieSetOptions };
package/dist/cookie.js ADDED
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert";
2
+
3
+ //#region src/cookie.ts
4
+ /**
5
+ * RegExp to match field-content in RFC 7230 sec 3.2
6
+ *
7
+ * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
8
+ * field-vchar = VCHAR / obs-text
9
+ * obs-text = %x80-FF
10
+ */
11
+ const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
12
+ /**
13
+ * RegExp to match Same-Site cookie attribute value.
14
+ * https://en.wikipedia.org/wiki/HTTP_cookie#SameSite_cookie
15
+ */
16
+ const sameSiteRegExp = /^(?:none|lax|strict)$/i;
17
+ /**
18
+ * RegExp to match Priority cookie attribute value.
19
+ */
20
+ const PRIORITY_REGEXP = /^(?:low|medium|high)$/i;
21
+ var Cookie = class {
22
+ name;
23
+ value;
24
+ attrs;
25
+ constructor(name, value, attrs) {
26
+ assert(fieldContentRegExp.test(name), "argument name is invalid");
27
+ assert(!value || fieldContentRegExp.test(value), "argument value is invalid");
28
+ this.name = name;
29
+ this.value = value ?? "";
30
+ this.attrs = mergeDefaultAttrs(attrs);
31
+ assert(!this.attrs.path || fieldContentRegExp.test(this.attrs.path), "argument option path is invalid");
32
+ if (typeof this.attrs.domain === "function") this.attrs.domain = this.attrs.domain();
33
+ assert(!this.attrs.domain || fieldContentRegExp.test(this.attrs.domain), "argument option domain is invalid");
34
+ assert(!this.attrs.sameSite || this.attrs.sameSite === true || sameSiteRegExp.test(this.attrs.sameSite), "argument option sameSite is invalid");
35
+ assert(!this.attrs.priority || PRIORITY_REGEXP.test(this.attrs.priority), "argument option priority is invalid");
36
+ if (!value) {
37
+ this.attrs.expires = /* @__PURE__ */ new Date(0);
38
+ this.attrs.maxAge = void 0;
39
+ }
40
+ }
41
+ toString() {
42
+ return this.name + "=" + this.value;
43
+ }
44
+ toHeader() {
45
+ let header = this.toString();
46
+ const attrs = this.attrs;
47
+ if (attrs.path) header += "; path=" + attrs.path;
48
+ const maxAge = typeof attrs.maxAge === "string" ? parseInt(attrs.maxAge, 10) : attrs.maxAge;
49
+ if (maxAge) {
50
+ header += "; max-age=" + Math.round(maxAge / 1e3);
51
+ attrs.expires = new Date(Date.now() + maxAge);
52
+ }
53
+ if (attrs.expires) header += "; expires=" + attrs.expires.toUTCString();
54
+ if (attrs.domain) header += "; domain=" + attrs.domain;
55
+ if (attrs.priority) header += "; priority=" + attrs.priority.toLowerCase();
56
+ if (attrs.sameSite) header += "; samesite=" + (attrs.sameSite === true ? "strict" : attrs.sameSite.toLowerCase());
57
+ if (attrs.secure) header += "; secure";
58
+ if (attrs.httpOnly) header += "; httponly";
59
+ if (attrs.partitioned) header += "; partitioned";
60
+ return header;
61
+ }
62
+ };
63
+ function mergeDefaultAttrs(attrs) {
64
+ return {
65
+ path: "/",
66
+ httpOnly: true,
67
+ secure: false,
68
+ overwrite: false,
69
+ sameSite: false,
70
+ partitioned: false,
71
+ priority: void 0,
72
+ ...attrs
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ export { Cookie };
@@ -0,0 +1,47 @@
1
+ import { Keygrip } from "./keygrip.js";
2
+ import { CookieSetOptions } from "./cookie.js";
3
+
4
+ //#region src/cookies.d.ts
5
+ interface DefaultCookieOptions extends CookieSetOptions {
6
+ /**
7
+ * Auto get and set `_CHIPS-` prefix cookie to adaptation CHIPS mode (The default value is false).
8
+ */
9
+ autoChips?: boolean;
10
+ }
11
+ interface CookieGetOptions {
12
+ /**
13
+ * Whether to sign or not (The default value is true).
14
+ */
15
+ signed?: boolean;
16
+ /**
17
+ * Encrypt the cookie's value or not (The default value is false).
18
+ */
19
+ encrypt?: boolean;
20
+ }
21
+ /**
22
+ * cookies for egg
23
+ * extend pillarjs/cookies, add encrypt and decrypt
24
+ */
25
+ declare class Cookies {
26
+ #private;
27
+ readonly ctx: Record<string, any>;
28
+ readonly app: Record<string, any>;
29
+ readonly secure: boolean;
30
+ constructor(ctx: Record<string, any>, keys: string[], defaultCookieOptions?: DefaultCookieOptions);
31
+ get keys(): Keygrip;
32
+ /**
33
+ * get cookie value by name
34
+ * @param {String} name - cookie's name
35
+ * @param {Object} opts - cookies' options
36
+ * - {Boolean} signed - default to true
37
+ * - {Boolean} encrypt - default to false
38
+ * @return {String} value - cookie's value
39
+ */
40
+ get(name: string, opts?: CookieGetOptions): string | undefined;
41
+ _get(name: string, opts: CookieGetOptions): string | undefined;
42
+ set(name: string, value: string | null, opts?: CookieSetOptions): this;
43
+ isSameSiteNoneCompatible(userAgent: string): boolean;
44
+ isPartitionedCompatible(userAgent: string): boolean;
45
+ }
46
+ //#endregion
47
+ export { CookieGetOptions, Cookies, DefaultCookieOptions };
@@ -0,0 +1,231 @@
1
+ import { Keygrip } from "./keygrip.js";
2
+ import { Cookie } from "./cookie.js";
3
+ import { CookieError } from "./error.js";
4
+ import assert from "node:assert";
5
+ import { base64decode, base64encode } from "utility";
6
+ import { isSameSiteNoneCompatible } from "should-send-same-site-none";
7
+
8
+ //#region src/cookies.ts
9
+ const keyCache = /* @__PURE__ */ new Map();
10
+ /**
11
+ * cookies for egg
12
+ * extend pillarjs/cookies, add encrypt and decrypt
13
+ */
14
+ var Cookies = class {
15
+ #keysArray;
16
+ #keys;
17
+ #defaultCookieOptions;
18
+ #autoChips;
19
+ ctx;
20
+ app;
21
+ secure;
22
+ #parseChromiumResult;
23
+ constructor(ctx, keys, defaultCookieOptions) {
24
+ this.#keysArray = keys;
25
+ this.#defaultCookieOptions = defaultCookieOptions;
26
+ this.#autoChips = defaultCookieOptions?.autoChips;
27
+ this.ctx = ctx;
28
+ this.secure = this.ctx.secure;
29
+ this.app = ctx.app;
30
+ }
31
+ get keys() {
32
+ if (!this.#keys) {
33
+ assert(Array.isArray(this.#keysArray), ".keys required for encrypt/sign cookies");
34
+ const cache = keyCache.get(this.#keysArray);
35
+ if (cache) this.#keys = cache;
36
+ else {
37
+ this.#keys = new Keygrip(this.#keysArray);
38
+ keyCache.set(this.#keysArray, this.#keys);
39
+ }
40
+ }
41
+ return this.#keys;
42
+ }
43
+ /**
44
+ * get cookie value by name
45
+ * @param {String} name - cookie's name
46
+ * @param {Object} opts - cookies' options
47
+ * - {Boolean} signed - default to true
48
+ * - {Boolean} encrypt - default to false
49
+ * @return {String} value - cookie's value
50
+ */
51
+ get(name, opts = {}) {
52
+ let value = this._get(name, opts);
53
+ if (value === void 0 && this.#autoChips) value = this._get(this.#formatChipsCookieName(name), opts);
54
+ return value;
55
+ }
56
+ _get(name, opts) {
57
+ const signed = computeSigned(opts);
58
+ const header = this.ctx.get("cookie");
59
+ if (!header) return;
60
+ const match = header.match(getPattern(name));
61
+ if (!match) return;
62
+ let value = match[1];
63
+ if (!opts.encrypt && !signed) return value;
64
+ if (signed) {
65
+ const sigName = name + ".sig";
66
+ const sigValue = this.get(sigName, { signed: false });
67
+ if (!sigValue) return;
68
+ const raw = name + "=" + value;
69
+ const index = this.keys.verify(raw, sigValue);
70
+ if (index < 0) {
71
+ this.set(sigName, null, {
72
+ path: "/",
73
+ signed: false,
74
+ overwrite: true
75
+ });
76
+ return;
77
+ }
78
+ if (index > 0) this.set(sigName, this.keys.sign(raw), {
79
+ signed: false,
80
+ overwrite: true
81
+ });
82
+ return value;
83
+ }
84
+ value = base64decode(value, true, "buffer");
85
+ const res = this.keys.decrypt(value);
86
+ return res ? res.value.toString() : void 0;
87
+ }
88
+ set(name, value, opts) {
89
+ opts = {
90
+ ...this.#defaultCookieOptions,
91
+ ...opts
92
+ };
93
+ const signed = computeSigned(opts);
94
+ const shouldIgnoreSecureError = opts && opts.ignoreSecureError;
95
+ value = value || "";
96
+ if (!shouldIgnoreSecureError) {
97
+ if (!this.secure && opts.secure) throw new CookieError("Cannot send secure cookie over unencrypted connection");
98
+ }
99
+ let headers = this.ctx.response.get("set-cookie") || [];
100
+ if (!Array.isArray(headers)) headers = [headers];
101
+ if (opts.encrypt) value = value && base64encode(this.keys.encrypt(value), true);
102
+ if (value.length > 4093) this.app.emit("cookieLimitExceed", {
103
+ name,
104
+ value,
105
+ ctx: this.ctx
106
+ });
107
+ const userAgent = this.ctx.get("user-agent");
108
+ let isSameSiteNone = false;
109
+ let autoChips = !opts.partitioned && this.#autoChips;
110
+ if (opts.sameSite && typeof opts.sameSite === "string" && opts.sameSite.toLowerCase() === "none") {
111
+ isSameSiteNone = true;
112
+ if (opts.secure === false || !this.secure || userAgent && !this.isSameSiteNoneCompatible(userAgent)) {
113
+ opts.sameSite = false;
114
+ isSameSiteNone = false;
115
+ }
116
+ }
117
+ if (autoChips || opts.partitioned) {
118
+ if (!isSameSiteNone || opts.secure === false || !this.secure || userAgent && !this.isPartitionedCompatible(userAgent)) {
119
+ autoChips = false;
120
+ opts.partitioned = false;
121
+ }
122
+ }
123
+ if (opts.partitioned && opts.removeUnpartitioned) {
124
+ if (opts.overwrite) {
125
+ opts.overwrite = false;
126
+ headers = ignoreCookiesByName(headers, name);
127
+ }
128
+ const removeCookieOpts = {
129
+ ...opts,
130
+ partitioned: false
131
+ };
132
+ const removeUnpartitionedCookie = new Cookie(name, "", removeCookieOpts);
133
+ if (opts.secure === void 0) removeUnpartitionedCookie.attrs.secure = this.secure;
134
+ headers = pushCookie(headers, removeUnpartitionedCookie);
135
+ if (signed) {
136
+ removeUnpartitionedCookie.name += ".sig";
137
+ headers = ignoreCookiesByNameAndPath(headers, removeUnpartitionedCookie.name, removeUnpartitionedCookie.attrs.path);
138
+ headers = pushCookie(headers, removeUnpartitionedCookie);
139
+ }
140
+ } else if (autoChips) {
141
+ const newCookieName = this.#formatChipsCookieName(name);
142
+ const newCookieOpts = {
143
+ ...opts,
144
+ partitioned: true
145
+ };
146
+ const newPartitionedCookie = new Cookie(newCookieName, value, newCookieOpts);
147
+ if (opts.secure === void 0) newPartitionedCookie.attrs.secure = this.secure;
148
+ headers = pushCookie(headers, newPartitionedCookie);
149
+ if (signed) {
150
+ newPartitionedCookie.value = value && this.keys.sign(newPartitionedCookie.toString());
151
+ newPartitionedCookie.name += ".sig";
152
+ headers = ignoreCookiesByNameAndPath(headers, newPartitionedCookie.name, newPartitionedCookie.attrs.path);
153
+ headers = pushCookie(headers, newPartitionedCookie);
154
+ }
155
+ }
156
+ const cookie = new Cookie(name, value, opts);
157
+ if (opts.secure === void 0) cookie.attrs.secure = this.secure;
158
+ headers = pushCookie(headers, cookie);
159
+ if (signed) {
160
+ cookie.value = value && this.keys.sign(cookie.toString());
161
+ cookie.name += ".sig";
162
+ headers = pushCookie(headers, cookie);
163
+ }
164
+ this.ctx.set("set-cookie", headers);
165
+ return this;
166
+ }
167
+ #formatChipsCookieName(name) {
168
+ return `_CHIPS-${name}`;
169
+ }
170
+ #parseChromiumAndMajorVersion(userAgent) {
171
+ if (!this.#parseChromiumResult) this.#parseChromiumResult = parseChromiumAndMajorVersion(userAgent);
172
+ return this.#parseChromiumResult;
173
+ }
174
+ isSameSiteNoneCompatible(userAgent) {
175
+ const result = this.#parseChromiumAndMajorVersion(userAgent);
176
+ if (result.chromium) return result.majorVersion >= 80;
177
+ return isSameSiteNoneCompatible(userAgent);
178
+ }
179
+ isPartitionedCompatible(userAgent) {
180
+ const result = this.#parseChromiumAndMajorVersion(userAgent);
181
+ if (result.chromium) return result.majorVersion >= 118;
182
+ return false;
183
+ }
184
+ };
185
+ function parseChromiumAndMajorVersion(userAgent) {
186
+ const m = /Chrom[^ /]{1,100}\/(\d{1,100}?)\./.exec(userAgent);
187
+ if (!m) return {
188
+ chromium: false,
189
+ majorVersion: 0
190
+ };
191
+ return {
192
+ chromium: true,
193
+ majorVersion: parseInt(m[1])
194
+ };
195
+ }
196
+ const _patternCache = /* @__PURE__ */ new Map();
197
+ function getPattern(name) {
198
+ const cache = _patternCache.get(name);
199
+ if (cache) return cache;
200
+ const reg = /* @__PURE__ */ new RegExp("(?:^|;) *" + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + "=([^;]*)");
201
+ _patternCache.set(name, reg);
202
+ return reg;
203
+ }
204
+ function computeSigned(opts) {
205
+ if (opts.encrypt) return false;
206
+ return opts.signed !== false;
207
+ }
208
+ function pushCookie(cookies, cookie) {
209
+ if (cookie.attrs.overwrite) cookies = ignoreCookiesByName(cookies, cookie.name);
210
+ cookies.push(cookie.toHeader());
211
+ return cookies;
212
+ }
213
+ function ignoreCookiesByName(cookies, name) {
214
+ const prefix = `${name}=`;
215
+ return cookies.filter((c) => !c.startsWith(prefix));
216
+ }
217
+ function ignoreCookiesByNameAndPath(cookies, name, path) {
218
+ if (!path) return ignoreCookiesByName(cookies, name);
219
+ const prefix = `${name}=`;
220
+ const includedPath = `; path=${path};`;
221
+ const endsWithPath = `; path=${path}`;
222
+ return cookies.filter((c) => {
223
+ if (c.startsWith(prefix)) {
224
+ if (c.includes(includedPath) || c.endsWith(endsWithPath)) return false;
225
+ }
226
+ return true;
227
+ });
228
+ }
229
+
230
+ //#endregion
231
+ export { Cookies };
@@ -0,0 +1,6 @@
1
+ //#region src/error.d.ts
2
+ declare class CookieError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
5
+ //#endregion
6
+ export { CookieError };
package/dist/error.js ADDED
@@ -0,0 +1,11 @@
1
+ //#region src/error.ts
2
+ var CookieError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = this.constructor.name;
6
+ if ("captureStackTrace" in Error) Error.captureStackTrace(this, this.constructor);
7
+ }
8
+ };
9
+
10
+ //#endregion
11
+ export { CookieError };
@@ -0,0 +1,5 @@
1
+ import { Keygrip } from "./keygrip.js";
2
+ import { Cookie, CookieSetOptions } from "./cookie.js";
3
+ import { CookieGetOptions, Cookies, DefaultCookieOptions } from "./cookies.js";
4
+ import { CookieError } from "./error.js";
5
+ export { Cookie, CookieError, CookieGetOptions, CookieSetOptions, Cookies, DefaultCookieOptions, Keygrip };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { Keygrip } from "./keygrip.js";
2
+ import { Cookie } from "./cookie.js";
3
+ import { CookieError } from "./error.js";
4
+ import { Cookies } from "./cookies.js";
5
+
6
+ export { Cookie, CookieError, Cookies, Keygrip };
@@ -0,0 +1,14 @@
1
+ //#region src/keygrip.d.ts
2
+ declare class Keygrip {
3
+ #private;
4
+ constructor(keys: string[]);
5
+ encrypt(data: string, key?: string): Buffer<ArrayBuffer>;
6
+ decrypt(data: string | Buffer): {
7
+ value: Buffer;
8
+ index: number;
9
+ } | false;
10
+ sign(data: string | Buffer, key?: string): string;
11
+ verify(data: string, digest: string): number;
12
+ }
13
+ //#endregion
14
+ export { Keygrip };
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert";
2
+ import { debuglog } from "node:util";
3
+ import crypto from "node:crypto";
4
+
5
+ //#region src/keygrip.ts
6
+ const debug = debuglog("egg/cookies:keygrip");
7
+ const KEY_LEN = 32;
8
+ const IV_SIZE = 16;
9
+ const passwordCache = /* @__PURE__ */ new Map();
10
+ const replacer = {
11
+ "/": "_",
12
+ "+": "-",
13
+ "=": ""
14
+ };
15
+ function constantTimeCompare(a, b) {
16
+ if (a.length !== b.length) return false;
17
+ return crypto.timingSafeEqual(a, b);
18
+ }
19
+ var Keygrip = class {
20
+ #keys;
21
+ #hash = "sha256";
22
+ #cipher = "aes-256-cbc";
23
+ constructor(keys) {
24
+ assert(Array.isArray(keys) && keys.length > 0, "keys must be provided and should be an array");
25
+ this.#keys = keys;
26
+ }
27
+ encrypt(data, key) {
28
+ key = key || this.#keys[0];
29
+ const password = keyToPassword(key);
30
+ const cipher = crypto.createCipheriv(this.#cipher, password.key, password.iv);
31
+ return crypt(cipher, data);
32
+ }
33
+ decrypt(data) {
34
+ const keys = this.#keys;
35
+ for (let i = 0; i < keys.length; i++) {
36
+ const value = this.#decryptByKey(data, keys[i]);
37
+ if (value !== false) return {
38
+ value,
39
+ index: i
40
+ };
41
+ }
42
+ return false;
43
+ }
44
+ #decryptByKey(data, key) {
45
+ try {
46
+ const password = keyToPassword(key);
47
+ const cipher = crypto.createDecipheriv(this.#cipher, password.key, password.iv);
48
+ return crypt(cipher, data);
49
+ } catch (err) {
50
+ debug("crypt error: %s", err);
51
+ return false;
52
+ }
53
+ }
54
+ sign(data, key) {
55
+ key = key || this.#keys[0];
56
+ return crypto.createHmac(this.#hash, key).update(data).digest("base64").replace(/\/|\+|=/g, (x) => {
57
+ return replacer[x];
58
+ });
59
+ }
60
+ verify(data, digest) {
61
+ const keys = this.#keys;
62
+ for (let i = 0; i < keys.length; i++) {
63
+ const key = keys[i];
64
+ if (constantTimeCompare(Buffer.from(digest), Buffer.from(this.sign(data, key)))) {
65
+ debug("data %s match key %s, index: %d", data, key, i);
66
+ return i;
67
+ }
68
+ }
69
+ return -1;
70
+ }
71
+ };
72
+ function crypt(cipher, data) {
73
+ const text = Buffer.isBuffer(data) ? cipher.update(data) : cipher.update(data, "utf-8");
74
+ const pad = cipher.final();
75
+ return Buffer.concat([text, pad]);
76
+ }
77
+ function keyToPassword(key) {
78
+ if (passwordCache.has(key)) return passwordCache.get(key);
79
+ const bytes = Buffer.alloc(KEY_LEN + IV_SIZE);
80
+ let lastHash = null, nBytes = 0;
81
+ while (nBytes < bytes.length) {
82
+ const hash = crypto.createHash("md5");
83
+ if (lastHash) hash.update(lastHash);
84
+ hash.update(key);
85
+ lastHash = hash.digest();
86
+ lastHash.copy(bytes, nBytes);
87
+ nBytes += lastHash.length;
88
+ }
89
+ const password = {
90
+ key: bytes.subarray(0, KEY_LEN),
91
+ iv: bytes.subarray(KEY_LEN, bytes.length)
92
+ };
93
+ passwordCache.set(key, password);
94
+ return password;
95
+ }
96
+
97
+ //#endregion
98
+ export { Keygrip };