@fast-china/utils 2.0.0 → 2.0.2
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/CHANGELOG.md +27 -0
- package/Fast.png +0 -0
- package/README.md +27 -29
- package/README.zh.md +27 -29
- package/dist/base64/index.mjs.map +1 -1
- package/dist/crypto/index.d.mts +265 -96
- package/dist/crypto/index.mjs +563 -261
- package/dist/crypto/index.mjs.map +1 -1
- package/dist/identity/index.d.mts +5 -5
- package/dist/identity/index.mjs +2 -2
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.global.min.js +3 -0
- package/dist/index.global.min.js.map +1 -0
- package/dist/index.mjs +2 -2
- package/dist/storage/index.d.mts +11 -8
- package/dist/storage/index.mjs +21 -18
- package/dist/storage/index.mjs.map +1 -1
- package/dist/vue/emits.mjs.map +1 -1
- package/dist/vue/index.d.mts +2 -2
- package/dist/vue/install.d.mts +11 -29
- package/dist/vue/install.mjs +25 -25
- package/dist/vue/install.mjs.map +1 -1
- package/dist/vue/props.mjs.map +1 -1
- package/dist/vue/render.d.mts +2 -3
- package/dist/vue/render.mjs +3 -9
- package/dist/vue/render.mjs.map +1 -1
- package/docs/API.md +30 -14
- package/docs/API.zh-CN.md +30 -14
- package/docs/DEVELOPMENT_RELEASE.zh-CN.md +4 -4
- package/docs/RUNTIME_CONTRACT.md +10 -10
- package/package.json +9 -11
- package/dist/array/index.d.mts.map +0 -1
- package/dist/async/index.d.mts.map +0 -1
- package/dist/base64/index.d.mts.map +0 -1
- package/dist/color/index.d.mts.map +0 -1
- package/dist/crypto/index.d.mts.map +0 -1
- package/dist/date/index.d.mts.map +0 -1
- package/dist/dom/style.d.mts.map +0 -1
- package/dist/env/index.d.mts.map +0 -1
- package/dist/identity/index.d.mts.map +0 -1
- package/dist/logger/index.d.mts.map +0 -1
- package/dist/number/index.d.mts.map +0 -1
- package/dist/object/index.d.mts.map +0 -1
- package/dist/storage/index.d.mts.map +0 -1
- package/dist/string/index.d.mts.map +0 -1
- package/dist/vue/emits.d.mts.map +0 -1
- package/dist/vue/expose.d.mts.map +0 -1
- package/dist/vue/func.d.mts.map +0 -1
- package/dist/vue/install.d.mts.map +0 -1
- package/dist/vue/props.d.mts.map +0 -1
- package/dist/vue/render.d.mts.map +0 -1
- package/dist/vue/slots.d.mts.map +0 -1
- package/dist/vue/with.d.mts.map +0 -1
- package/src/array/index.ts +0 -173
- package/src/async/index.ts +0 -475
- package/src/base64/index.ts +0 -374
- package/src/color/index.ts +0 -208
- package/src/crypto/index.ts +0 -670
- package/src/date/index.ts +0 -451
- package/src/dom/index.ts +0 -6
- package/src/dom/style.ts +0 -92
- package/src/env/index.ts +0 -169
- package/src/identity/index.ts +0 -144
- package/src/index.ts +0 -20
- package/src/internal/text.ts +0 -46
- package/src/logger/index.ts +0 -219
- package/src/number/index.ts +0 -235
- package/src/object/index.ts +0 -160
- package/src/storage/index.ts +0 -524
- package/src/string/index.ts +0 -328
- package/src/vue/emits.ts +0 -71
- package/src/vue/expose.ts +0 -11
- package/src/vue/func.ts +0 -17
- package/src/vue/index.ts +0 -13
- package/src/vue/install.ts +0 -185
- package/src/vue/props.ts +0 -39
- package/src/vue/render.ts +0 -41
- package/src/vue/slots.ts +0 -23
- package/src/vue/with.ts +0 -10
package/src/crypto/index.ts
DELETED
|
@@ -1,670 +0,0 @@
|
|
|
1
|
-
import AES from "crypto-js/aes.js";
|
|
2
|
-
import Hex from "crypto-js/enc-hex.js";
|
|
3
|
-
import Utf8 from "crypto-js/enc-utf8.js";
|
|
4
|
-
import MD5 from "crypto-js/md5.js";
|
|
5
|
-
import ECB from "crypto-js/mode-ecb.js";
|
|
6
|
-
import Pkcs7 from "crypto-js/pad-pkcs7.js";
|
|
7
|
-
import SHA1 from "crypto-js/sha1.js";
|
|
8
|
-
|
|
9
|
-
import { decodeBase64Bytes, decodeBase64UrlBytes, encodeBase64Bytes, encodeBase64UrlBytes } from "../base64/index.js";
|
|
10
|
-
import { encodeUtf8, getTextDecoder } from "../internal/text.js";
|
|
11
|
-
|
|
12
|
-
import type CryptoJS from "crypto-js";
|
|
13
|
-
|
|
14
|
-
/** 密码加密协议固定边界;修改任一值都需要同步安全评估、测试与文档。 */
|
|
15
|
-
const defaultPbkdf2Iterations = 600_000;
|
|
16
|
-
const minimumPbkdf2Iterations = 100_000;
|
|
17
|
-
const maximumPbkdf2Iterations = 5_000_000;
|
|
18
|
-
const maximumPasswordBytes = 1024;
|
|
19
|
-
const maximumPlaintextBytes = 8 * 1024 * 1024;
|
|
20
|
-
const maximumPayloadLength = 16 * 1024 * 1024;
|
|
21
|
-
const encryptedPayloadPrefix = "FAST-AES-256-GCM-V2";
|
|
22
|
-
|
|
23
|
-
/** 用于逐项验证实际依赖能力,而不是只判断 `subtle` 对象是否存在。 */
|
|
24
|
-
type RuntimeSubtleCrypto = Partial<
|
|
25
|
-
Pick<SubtleCrypto, "decrypt" | "deriveBits" | "deriveKey" | "digest" | "encrypt" | "exportKey" | "generateKey" | "importKey" | "sign" | "verify">
|
|
26
|
-
>;
|
|
27
|
-
/** Web Crypto 的运行时最小结构,避免用不安全的类型断言掩盖能力缺失。 */
|
|
28
|
-
interface RuntimeCrypto extends Partial<Pick<Crypto, "getRandomValues">> {
|
|
29
|
-
/** 可选 SubtleCrypto 能力;密码原语入口会继续逐项验证所需方法。 */
|
|
30
|
-
subtle?: RuntimeSubtleCrypto;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Crypto 工具延迟读取的平台全局对象最小视图。 */
|
|
34
|
-
interface RuntimeCryptoGlobals {
|
|
35
|
-
/** 可选 Web Crypto 实现;具体方法仍由每个能力入口逐项校验。 */
|
|
36
|
-
crypto?: RuntimeCrypto;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const runtimeCryptoGlobals = globalThis as unknown as RuntimeCryptoGlobals;
|
|
40
|
-
|
|
41
|
-
/** 可直接哈希或比较的二进制输入。 */
|
|
42
|
-
export type TextOrBytes = string | Uint8Array;
|
|
43
|
-
|
|
44
|
-
/** {@link encryptTextWithPassword} 的密钥派生选项。 */
|
|
45
|
-
export interface PasswordEncryptionOptions {
|
|
46
|
-
/**
|
|
47
|
-
* PBKDF2-HMAC-SHA-256 迭代次数,默认 `600_000`,允许 100,000 至 5,000,000。
|
|
48
|
-
* 低于默认值需要调用方基于目标硬件完成明确的安全与性能评估。
|
|
49
|
-
*/
|
|
50
|
-
iterations?: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** CryptoJS 兼容 AES 模式。 */
|
|
54
|
-
export type LegacyAesCipherMode = "CBC" | "ECB";
|
|
55
|
-
|
|
56
|
-
/** Web Crypto 导出的 PEM 公私钥对。 */
|
|
57
|
-
export interface PemKeyPair {
|
|
58
|
-
/** 未加密的 PKCS#8 PEM 私钥,包含标准 `PRIVATE KEY` 头尾和 64 字符换行。 */
|
|
59
|
-
privateKey: string;
|
|
60
|
-
/** SubjectPublicKeyInfo PEM 公钥,包含标准 `PUBLIC KEY` 头尾和 64 字符换行。 */
|
|
61
|
-
publicKey: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** RSA-OAEP 密钥生成选项。 */
|
|
65
|
-
export interface RsaKeyPairOptions {
|
|
66
|
-
/** RSA 模数位数,默认 2048;必须是不小于 2048 的 256 倍数。 */
|
|
67
|
-
modulusLength?: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 本模块支持的 Web Crypto 椭圆曲线。 */
|
|
71
|
-
export type EcNamedCurve = "P-256" | "P-384" | "P-521";
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 获取安全随机数能力。
|
|
75
|
-
*
|
|
76
|
-
* @remarks 随机字节和随机字符串不依赖 SubtleCrypto,因此这里与完整密码原语检查分开。
|
|
77
|
-
* @returns 当前平台的 Crypto 对象。
|
|
78
|
-
* @throws `Error` 当平台缺少 `getRandomValues`。
|
|
79
|
-
*/
|
|
80
|
-
const requireRandomCrypto = (): Crypto => {
|
|
81
|
-
const crypto = runtimeCryptoGlobals.crypto;
|
|
82
|
-
if (typeof crypto?.getRandomValues !== "function") {
|
|
83
|
-
throw new Error("Web Crypto is unavailable in the current runtime.");
|
|
84
|
-
}
|
|
85
|
-
return crypto as Crypto;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 获取本模块非随机 API 所需的完整 Web Crypto 能力。
|
|
90
|
-
*
|
|
91
|
-
* @returns 已确认实现摘要、派生、密钥导入导出、加解密、签名和验证方法的 Crypto 对象。
|
|
92
|
-
* @throws `Error` 当任一必要 SubtleCrypto 方法缺失。
|
|
93
|
-
*/
|
|
94
|
-
const requireWebCrypto = (): Crypto => {
|
|
95
|
-
const crypto = requireRandomCrypto();
|
|
96
|
-
const subtle = (crypto as unknown as RuntimeCrypto).subtle;
|
|
97
|
-
if (
|
|
98
|
-
typeof subtle?.decrypt !== "function" ||
|
|
99
|
-
typeof subtle.deriveBits !== "function" ||
|
|
100
|
-
typeof subtle.deriveKey !== "function" ||
|
|
101
|
-
typeof subtle.digest !== "function" ||
|
|
102
|
-
typeof subtle.encrypt !== "function" ||
|
|
103
|
-
typeof subtle.exportKey !== "function" ||
|
|
104
|
-
typeof subtle.generateKey !== "function" ||
|
|
105
|
-
typeof subtle.importKey !== "function" ||
|
|
106
|
-
typeof subtle.sign !== "function" ||
|
|
107
|
-
typeof subtle.verify !== "function"
|
|
108
|
-
) {
|
|
109
|
-
throw new Error("Web Crypto SubtleCrypto is unavailable in the current runtime.");
|
|
110
|
-
}
|
|
111
|
-
return crypto;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 把二进制联合输入规范为字节。
|
|
116
|
-
*
|
|
117
|
-
* @param value - UTF-8 文本或现有字节数组。
|
|
118
|
-
* @returns 文本的新编码结果,或原始字节数组引用。
|
|
119
|
-
*/
|
|
120
|
-
const toInputBytes = (value: TextOrBytes): Uint8Array => (typeof value === "string" ? encodeUtf8(value) : value);
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 把 Uint8Array 复制为独立的完整 ArrayBuffer。
|
|
124
|
-
*
|
|
125
|
-
* @remarks 不能直接返回 `bytes.buffer`,因为输入可能只是更大 Buffer 的切片。
|
|
126
|
-
* @param bytes - 源字节视图。
|
|
127
|
-
* @returns 长度与视图完全一致、偏移为零的新 ArrayBuffer。
|
|
128
|
-
*/
|
|
129
|
-
const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => {
|
|
130
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
131
|
-
copy.set(bytes);
|
|
132
|
-
return copy.buffer;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/** 本模块导出的 PKCS#8 与 SPKI PEM 标签。 */
|
|
136
|
-
const pemLabels = {
|
|
137
|
-
private: "PRIVATE KEY",
|
|
138
|
-
public: "PUBLIC KEY",
|
|
139
|
-
} as const;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 把 DER 内容封装为 PEM。
|
|
143
|
-
*
|
|
144
|
-
* @param label - PEM Begin/End 标签,不包含分隔线。
|
|
145
|
-
* @param value - DER 二进制内容。
|
|
146
|
-
* @returns 每行最多 64 个 Base64 字符的 PEM 文本。
|
|
147
|
-
*/
|
|
148
|
-
const toPem = (label: string, value: ArrayBuffer): string => {
|
|
149
|
-
const encoded = encodeBase64Bytes(new Uint8Array(value));
|
|
150
|
-
const lines = encoded.match(/.{1,64}/gu)?.join("\n") ?? "";
|
|
151
|
-
return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----`;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 校验 PEM 标签并还原 DER。
|
|
156
|
-
*
|
|
157
|
-
* @param value - PEM 文本。
|
|
158
|
-
* @param label - 当前算法预期的标签。
|
|
159
|
-
* @returns 独立的 DER ArrayBuffer。
|
|
160
|
-
* @throws `TypeError` 当 Begin/End 标签不匹配或 Base64 内容非法。
|
|
161
|
-
*/
|
|
162
|
-
const fromPem = (value: string, label: string): ArrayBuffer => {
|
|
163
|
-
const header = `-----BEGIN ${label}-----`;
|
|
164
|
-
const footer = `-----END ${label}-----`;
|
|
165
|
-
const trimmed = value.trim();
|
|
166
|
-
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) throw new TypeError(`Expected a ${label} PEM value.`);
|
|
167
|
-
const encoded = trimmed.slice(header.length, -footer.length).replace(/\s+/gu, "");
|
|
168
|
-
return toArrayBuffer(decodeBase64Bytes(encoded));
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* 导出 Web Crypto 密钥对。
|
|
173
|
-
*
|
|
174
|
-
* @param keyPair - 可导出的公私钥对。
|
|
175
|
-
* @returns PKCS#8 私钥和 SPKI 公钥组成的 PEM 对象。
|
|
176
|
-
* @throws `Error` 当平台拒绝导出或密钥格式不兼容。
|
|
177
|
-
*/
|
|
178
|
-
const exportKeyPair = async (keyPair: CryptoKeyPair): Promise<PemKeyPair> => {
|
|
179
|
-
const crypto = requireWebCrypto();
|
|
180
|
-
const [privateKey, publicKey] = await Promise.all([
|
|
181
|
-
crypto.subtle.exportKey("pkcs8", keyPair.privateKey),
|
|
182
|
-
crypto.subtle.exportKey("spki", keyPair.publicKey),
|
|
183
|
-
]);
|
|
184
|
-
return {
|
|
185
|
-
privateKey: toPem(pemLabels.private, privateKey),
|
|
186
|
-
publicKey: toPem(pemLabels.public, publicKey),
|
|
187
|
-
};
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 缩小 Web Crypto `generateKey` 的联合返回值。
|
|
192
|
-
*
|
|
193
|
-
* @param value - 平台返回的单密钥或密钥对。
|
|
194
|
-
* @returns 同时具有公钥和私钥的密钥对。
|
|
195
|
-
* @throws `Error` 当平台没有按请求生成密钥对。
|
|
196
|
-
*/
|
|
197
|
-
const assertKeyPair = (value: CryptoKey | CryptoKeyPair): CryptoKeyPair => {
|
|
198
|
-
if ("privateKey" in value && "publicKey" in value) return value;
|
|
199
|
-
throw new Error("The runtime did not generate a key pair.");
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 按旧协议规范化 AES Key。
|
|
204
|
-
*
|
|
205
|
-
* @param key - 任意长度 UTF-8 文本 Key。
|
|
206
|
-
* @returns 补字符 `f` 或截断到 32 个 UTF-16 Code Unit 后的 CryptoJS WordArray。
|
|
207
|
-
*/
|
|
208
|
-
const normalizeAesKey = (key: string): CryptoJS.lib.WordArray => Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
209
|
-
/**
|
|
210
|
-
* 按旧协议规范化 AES IV。
|
|
211
|
-
*
|
|
212
|
-
* @param vector - 任意长度 UTF-8 文本 IV。
|
|
213
|
-
* @returns 补字符 `f` 或截断到 16 个 UTF-16 Code Unit 后的 CryptoJS WordArray。
|
|
214
|
-
*/
|
|
215
|
-
const normalizeAesVector = (vector: string): CryptoJS.lib.WordArray => Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* 选择与 ECDSA 曲线对应的摘要算法。
|
|
219
|
-
*
|
|
220
|
-
* @param namedCurve - 本模块支持的 NIST 曲线。
|
|
221
|
-
* @returns P-256、P-384、P-521 分别对应 SHA-256、SHA-384、SHA-512。
|
|
222
|
-
*/
|
|
223
|
-
const getEcdsaHash = (namedCurve: EcNamedCurve): "SHA-256" | "SHA-384" | "SHA-512" => {
|
|
224
|
-
switch (namedCurve) {
|
|
225
|
-
case "P-256":
|
|
226
|
-
return "SHA-256";
|
|
227
|
-
case "P-384":
|
|
228
|
-
return "SHA-384";
|
|
229
|
-
case "P-521":
|
|
230
|
-
return "SHA-512";
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 获取 ECDH 共享秘密的导出位数。
|
|
236
|
-
*
|
|
237
|
-
* @param namedCurve - 本模块支持的 NIST 曲线。
|
|
238
|
-
* @returns 曲线字段按完整字节对齐后的位数;P-521 返回 528。
|
|
239
|
-
*/
|
|
240
|
-
const getEcBitLength = (namedCurve: EcNamedCurve): number => {
|
|
241
|
-
switch (namedCurve) {
|
|
242
|
-
case "P-256":
|
|
243
|
-
return 256;
|
|
244
|
-
case "P-384":
|
|
245
|
-
return 384;
|
|
246
|
-
case "P-521":
|
|
247
|
-
return 528;
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* 校验并编码密码。
|
|
253
|
-
*
|
|
254
|
-
* @param password - 用户提供的密码文本。
|
|
255
|
-
* @returns UTF-8 密码字节。
|
|
256
|
-
* @throws `TypeError` 当密码为空。
|
|
257
|
-
* @throws `RangeError` 当 UTF-8 长度超过 1,024 字节。
|
|
258
|
-
*/
|
|
259
|
-
const encodeValidatedPassword = (password: string): Uint8Array => {
|
|
260
|
-
const bytes = encodeUtf8(password);
|
|
261
|
-
if (bytes.length === 0) throw new TypeError("The encryption password cannot be empty.");
|
|
262
|
-
if (bytes.length > maximumPasswordBytes) {
|
|
263
|
-
throw new RangeError(`The UTF-8 password cannot exceed ${maximumPasswordBytes} bytes.`);
|
|
264
|
-
}
|
|
265
|
-
return bytes;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* 校验 PBKDF2 迭代次数。
|
|
270
|
-
*
|
|
271
|
-
* @param iterations - 待使用的迭代次数。
|
|
272
|
-
* @returns 校验后的原始整数。
|
|
273
|
-
* @throws `RangeError` 当值不是 100,000 至 5,000,000 的安全整数。
|
|
274
|
-
*/
|
|
275
|
-
const validateIterations = (iterations: number): number => {
|
|
276
|
-
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) {
|
|
277
|
-
throw new RangeError(`iterations must be a safe integer between ${minimumPbkdf2Iterations} and ${maximumPbkdf2Iterations}.`);
|
|
278
|
-
}
|
|
279
|
-
return iterations;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* 从密码派生 AES-256-GCM Key。
|
|
284
|
-
*
|
|
285
|
-
* @param passwordBytes - 已校验的 UTF-8 密码。
|
|
286
|
-
* @param salt - 载荷中保存的 16 字节随机盐。
|
|
287
|
-
* @param iterations - 已校验 PBKDF2 成本。
|
|
288
|
-
* @param usage - 派生 Key 唯一允许的加密或解密用途。
|
|
289
|
-
* @returns 不可导出的 AES-GCM CryptoKey。
|
|
290
|
-
* @throws `Error` 当平台缺少相应 Web Crypto 能力或拒绝派生。
|
|
291
|
-
*/
|
|
292
|
-
const derivePasswordEncryptionKey = async (
|
|
293
|
-
passwordBytes: Uint8Array,
|
|
294
|
-
salt: Uint8Array,
|
|
295
|
-
iterations: number,
|
|
296
|
-
usage: "decrypt" | "encrypt"
|
|
297
|
-
): Promise<CryptoKey> => {
|
|
298
|
-
const crypto = requireWebCrypto();
|
|
299
|
-
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveKey"]);
|
|
300
|
-
return crypto.subtle.deriveKey(
|
|
301
|
-
{ hash: "SHA-256", iterations, name: "PBKDF2", salt: toArrayBuffer(salt) },
|
|
302
|
-
material,
|
|
303
|
-
{ length: 256, name: "AES-GCM" },
|
|
304
|
-
false,
|
|
305
|
-
[usage]
|
|
306
|
-
);
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* 生成安全随机字节。
|
|
311
|
-
*
|
|
312
|
-
* @param length - 0 至 65,536 的安全整数;上限来自 Web Crypto 单次请求限制。
|
|
313
|
-
* @returns 新建的 `Uint8Array`。
|
|
314
|
-
* @throws 参数非法时抛出 `RangeError`;缺少 Web Crypto 时抛出 `Error`。
|
|
315
|
-
*/
|
|
316
|
-
export function generateRandomBytes(length: number): Uint8Array {
|
|
317
|
-
if (!Number.isSafeInteger(length) || length < 0 || length > 65_536) {
|
|
318
|
-
throw new RangeError("length must be a safe integer from 0 through 65,536.");
|
|
319
|
-
}
|
|
320
|
-
return requireRandomCrypto().getRandomValues(new Uint8Array(length));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* 计算 SHA-256 摘要。
|
|
325
|
-
*
|
|
326
|
-
* @remarks SHA-256 是快速摘要,不适合直接存储或校验密码。
|
|
327
|
-
* @param value - UTF-8 字符串或原始字节。
|
|
328
|
-
* @returns 32 字节摘要。
|
|
329
|
-
* @throws 缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
330
|
-
*/
|
|
331
|
-
export async function sha256Bytes(value: TextOrBytes): Promise<Uint8Array> {
|
|
332
|
-
const digest = await requireWebCrypto().subtle.digest("SHA-256", toArrayBuffer(toInputBytes(value)));
|
|
333
|
-
return new Uint8Array(digest);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* 计算 SHA-256 并格式化为十六进制。
|
|
338
|
-
*
|
|
339
|
-
* @param value - UTF-8 字符串或原始字节。
|
|
340
|
-
* @returns 64 字符小写十六进制文本。
|
|
341
|
-
* @throws 缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
342
|
-
*/
|
|
343
|
-
export async function sha256Hex(value: TextOrBytes): Promise<string> {
|
|
344
|
-
const digest = await sha256Bytes(value);
|
|
345
|
-
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 计算 MD5 摘要并返回小写十六进制文本。
|
|
350
|
-
*
|
|
351
|
-
* @remarks MD5 仅用于历史协议和非安全校验,不得用于密码、签名或抗碰撞场景。
|
|
352
|
-
* @param value - UTF-8 文本。
|
|
353
|
-
* @returns 32 字符小写十六进制摘要。
|
|
354
|
-
*/
|
|
355
|
-
export function md5Hex(value: string): string {
|
|
356
|
-
return MD5(value).toString(Hex);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* 计算 SHA-1 摘要并返回小写十六进制文本。
|
|
361
|
-
*
|
|
362
|
-
* @remarks SHA-1 仅用于旧系统互通;新用途应使用 {@link sha256Hex}。
|
|
363
|
-
* @param value - UTF-8 文本。
|
|
364
|
-
* @returns 40 字符小写十六进制摘要。
|
|
365
|
-
*/
|
|
366
|
-
export function sha1Hex(value: string): string {
|
|
367
|
-
return SHA1(value).toString(Hex);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* 使用 AES-256-CBC 与 PKCS#7 填充加密文本。
|
|
372
|
-
*
|
|
373
|
-
* @remarks 密钥按 UTF-8 文本补齐或截断为 32 个字符,IV 补齐或截断为 16 个字符。
|
|
374
|
-
* 该格式不包含认证标签;新数据优先使用 {@link encryptTextWithPassword}。
|
|
375
|
-
* @param plaintext - UTF-8 明文。
|
|
376
|
-
* @param key - 兼容密钥文本。
|
|
377
|
-
* @param vector - 兼容初始化向量文本。
|
|
378
|
-
* @returns CryptoJS Base64 密文。
|
|
379
|
-
*/
|
|
380
|
-
export function encryptLegacyAesCbc(plaintext: string, key: string, vector: string): string {
|
|
381
|
-
if (plaintext.length === 0) return "";
|
|
382
|
-
return AES.encrypt(plaintext, normalizeAesKey(key), {
|
|
383
|
-
iv: normalizeAesVector(vector),
|
|
384
|
-
padding: Pkcs7,
|
|
385
|
-
}).toString();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** 解密 {@link encryptLegacyAesCbc} 生成的 CryptoJS Base64 密文。 */
|
|
389
|
-
export function decryptLegacyAesCbc(ciphertext: string, key: string, vector: string): string {
|
|
390
|
-
if (ciphertext.length === 0) return "";
|
|
391
|
-
return AES.decrypt(ciphertext, normalizeAesKey(key), {
|
|
392
|
-
iv: normalizeAesVector(vector),
|
|
393
|
-
padding: Pkcs7,
|
|
394
|
-
}).toString(Utf8);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* 使用 AES-256-ECB 与 PKCS#7 填充加密文本。
|
|
399
|
-
*
|
|
400
|
-
* @remarks ECB 会暴露重复明文块结构,只能用于必须兼容的旧协议,不得用于新数据设计。
|
|
401
|
-
* @param plaintext - UTF-8 明文。
|
|
402
|
-
* @param key - 兼容密钥文本,补齐或截断为 32 个字符。
|
|
403
|
-
* @returns CryptoJS Base64 密文。
|
|
404
|
-
*/
|
|
405
|
-
export function encryptLegacyAesEcb(plaintext: string, key: string): string {
|
|
406
|
-
if (plaintext.length === 0) return "";
|
|
407
|
-
return AES.encrypt(plaintext, normalizeAesKey(key), {
|
|
408
|
-
mode: ECB,
|
|
409
|
-
padding: Pkcs7,
|
|
410
|
-
}).toString();
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** 解密 {@link encryptLegacyAesEcb} 生成的 CryptoJS Base64 密文。 */
|
|
414
|
-
export function decryptLegacyAesEcb(ciphertext: string, key: string): string {
|
|
415
|
-
if (ciphertext.length === 0) return "";
|
|
416
|
-
return AES.decrypt(ciphertext, normalizeAesKey(key), {
|
|
417
|
-
mode: ECB,
|
|
418
|
-
padding: Pkcs7,
|
|
419
|
-
}).toString(Utf8);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** 生成可导出的 RSA-OAEP/SHA-256 PEM 密钥对。 */
|
|
423
|
-
export async function generateRsaOaepKeyPair(options: RsaKeyPairOptions = {}): Promise<PemKeyPair> {
|
|
424
|
-
const modulusLength = options.modulusLength ?? 2048;
|
|
425
|
-
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) {
|
|
426
|
-
throw new RangeError("modulusLength must be a safe integer of at least 2048 and divisible by 256.");
|
|
427
|
-
}
|
|
428
|
-
const crypto = requireWebCrypto();
|
|
429
|
-
const keyPair = assertKeyPair(
|
|
430
|
-
await crypto.subtle.generateKey(
|
|
431
|
-
{
|
|
432
|
-
hash: "SHA-256",
|
|
433
|
-
modulusLength,
|
|
434
|
-
name: "RSA-OAEP",
|
|
435
|
-
publicExponent: Uint8Array.of(1, 0, 1),
|
|
436
|
-
},
|
|
437
|
-
true,
|
|
438
|
-
["decrypt", "encrypt"]
|
|
439
|
-
)
|
|
440
|
-
);
|
|
441
|
-
return exportKeyPair(keyPair);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/** 使用 RSA-OAEP/SHA-256 公钥加密 UTF-8 文本,并返回 Base64 密文。 */
|
|
445
|
-
export async function encryptRsaOaep(plaintext: string, publicKeyPem: string): Promise<string> {
|
|
446
|
-
const crypto = requireWebCrypto();
|
|
447
|
-
const key = await crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), { hash: "SHA-256", name: "RSA-OAEP" }, false, [
|
|
448
|
-
"encrypt",
|
|
449
|
-
]);
|
|
450
|
-
const ciphertext = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, key, toArrayBuffer(encodeUtf8(plaintext)));
|
|
451
|
-
return encodeBase64Bytes(new Uint8Array(ciphertext));
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/** 使用 RSA-OAEP/SHA-256 私钥解密 Base64 密文。 */
|
|
455
|
-
export async function decryptRsaOaep(ciphertext: string, privateKeyPem: string): Promise<string> {
|
|
456
|
-
const crypto = requireWebCrypto();
|
|
457
|
-
const key = await crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), { hash: "SHA-256", name: "RSA-OAEP" }, false, [
|
|
458
|
-
"decrypt",
|
|
459
|
-
]);
|
|
460
|
-
const plaintext = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, key, toArrayBuffer(decodeBase64Bytes(ciphertext)));
|
|
461
|
-
return getTextDecoder().decode(plaintext);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** 生成 ECDSA PEM 签名密钥对。 */
|
|
465
|
-
export async function generateEcdsaKeyPair(namedCurve: EcNamedCurve = "P-256"): Promise<PemKeyPair> {
|
|
466
|
-
const crypto = requireWebCrypto();
|
|
467
|
-
const keyPair = assertKeyPair(await crypto.subtle.generateKey({ name: "ECDSA", namedCurve }, true, ["sign", "verify"]));
|
|
468
|
-
return exportKeyPair(keyPair);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** 使用 ECDSA 私钥签名文本或字节,并返回 Base64 签名。 */
|
|
472
|
-
export async function signEcdsa(value: TextOrBytes, privateKeyPem: string, namedCurve: EcNamedCurve = "P-256"): Promise<string> {
|
|
473
|
-
const crypto = requireWebCrypto();
|
|
474
|
-
const key = await crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), { name: "ECDSA", namedCurve }, false, ["sign"]);
|
|
475
|
-
const signature = await crypto.subtle.sign({ hash: getEcdsaHash(namedCurve), name: "ECDSA" }, key, toArrayBuffer(toInputBytes(value)));
|
|
476
|
-
return encodeBase64Bytes(new Uint8Array(signature));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/** 使用 ECDSA 公钥验证 Base64 签名。 */
|
|
480
|
-
export async function verifyEcdsa(value: TextOrBytes, signature: string, publicKeyPem: string, namedCurve: EcNamedCurve = "P-256"): Promise<boolean> {
|
|
481
|
-
const crypto = requireWebCrypto();
|
|
482
|
-
const key = await crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), { name: "ECDSA", namedCurve }, false, ["verify"]);
|
|
483
|
-
return crypto.subtle.verify(
|
|
484
|
-
{ hash: getEcdsaHash(namedCurve), name: "ECDSA" },
|
|
485
|
-
key,
|
|
486
|
-
toArrayBuffer(decodeBase64Bytes(signature)),
|
|
487
|
-
toArrayBuffer(toInputBytes(value))
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** 生成 ECDH PEM 密钥协商密钥对。 */
|
|
492
|
-
export async function generateEcdhKeyPair(namedCurve: EcNamedCurve = "P-256"): Promise<PemKeyPair> {
|
|
493
|
-
const crypto = requireWebCrypto();
|
|
494
|
-
const keyPair = assertKeyPair(await crypto.subtle.generateKey({ name: "ECDH", namedCurve }, true, ["deriveBits"]));
|
|
495
|
-
return exportKeyPair(keyPair);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* 使用本方 ECDH 私钥与对方 ECDH 公钥派生共享秘密。
|
|
500
|
-
*
|
|
501
|
-
* @remarks 返回值仍需经过合适的 KDF 后才能作为对称密钥,不应直接长期存储。
|
|
502
|
-
*/
|
|
503
|
-
export async function deriveEcdhSecret(privateKeyPem: string, publicKeyPem: string, namedCurve: EcNamedCurve = "P-256"): Promise<Uint8Array> {
|
|
504
|
-
const crypto = requireWebCrypto();
|
|
505
|
-
const [privateKey, publicKey] = await Promise.all([
|
|
506
|
-
crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), { name: "ECDH", namedCurve }, false, ["deriveBits"]),
|
|
507
|
-
crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), { name: "ECDH", namedCurve }, false, []),
|
|
508
|
-
]);
|
|
509
|
-
const secret = await crypto.subtle.deriveBits({ name: "ECDH", public: publicKey }, privateKey, getEcBitLength(namedCurve));
|
|
510
|
-
return new Uint8Array(secret);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* 以不提前退出的方式比较两个字节数组。
|
|
515
|
-
*
|
|
516
|
-
* @remarks JavaScript 引擎不保证严格常量时间;该函数只避免显式短路,不能替代服务端
|
|
517
|
-
* 密码学库提供的 timing-safe primitive。长度是否相同仍属于可观察信息。
|
|
518
|
-
* @param left - 第一字节序列。
|
|
519
|
-
* @param right - 第二字节序列。
|
|
520
|
-
* @returns 长度和每个字节均相同时返回 `true`。
|
|
521
|
-
*/
|
|
522
|
-
export function areBytesEqualWithoutEarlyExit(left: Uint8Array, right: Uint8Array): boolean {
|
|
523
|
-
const length = Math.max(left.length, right.length);
|
|
524
|
-
let difference = left.length === right.length ? 0 : 1;
|
|
525
|
-
// 不按长度或首个差异提前退出;缺失位置按零参与累计差异。
|
|
526
|
-
for (let index = 0; index < length; index += 1) difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
527
|
-
return difference === 0;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* 使用 PBKDF2-HMAC-SHA-256 派生密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
532
|
-
*
|
|
533
|
-
* @remarks 每次调用生成独立 16 字节盐与 12 字节 IV。输出是本库 v2 自描述载荷,
|
|
534
|
-
* 不应被拆分、修改或当作跨语言标准格式。密码加密不替代密钥管理;长期高价值数据
|
|
535
|
-
* 应使用平台 KMS 或专门的加密方案。
|
|
536
|
-
* @param plaintext - 原始文本,不进行 JSON 推断;UTF-8 编码后最大 8 MiB。
|
|
537
|
-
* @param password - 1 至 1024 UTF-8 字节的秘密口令。
|
|
538
|
-
* @param options - PBKDF2 工作因子。
|
|
539
|
-
* @returns 认证密文字符串;相同输入每次产生不同结果。
|
|
540
|
-
* @throws 口令非法时抛出 `TypeError` 或 `RangeError`;明文过大时抛出 `RangeError`;
|
|
541
|
-
* 运行时缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
542
|
-
*/
|
|
543
|
-
export async function encryptTextWithPassword(plaintext: string, password: string, options: PasswordEncryptionOptions = {}): Promise<string> {
|
|
544
|
-
const passwordBytes = encodeValidatedPassword(password);
|
|
545
|
-
const plaintextBytes = encodeUtf8(plaintext);
|
|
546
|
-
if (plaintextBytes.length > maximumPlaintextBytes) {
|
|
547
|
-
throw new RangeError(`The UTF-8 plaintext cannot exceed ${maximumPlaintextBytes} bytes.`);
|
|
548
|
-
}
|
|
549
|
-
const iterations = validateIterations(options.iterations ?? defaultPbkdf2Iterations);
|
|
550
|
-
const crypto = requireWebCrypto();
|
|
551
|
-
const salt = generateRandomBytes(16);
|
|
552
|
-
const iv = generateRandomBytes(12);
|
|
553
|
-
const key = await derivePasswordEncryptionKey(passwordBytes, salt, iterations, "encrypt");
|
|
554
|
-
// Prefix 同时作为明文版本字段和 AAD,攻击者无法在不破坏认证标签的情况下替换协议版本。
|
|
555
|
-
const ciphertext = await crypto.subtle.encrypt(
|
|
556
|
-
{ additionalData: toArrayBuffer(encodeUtf8(encryptedPayloadPrefix)), iv: toArrayBuffer(iv), name: "AES-GCM", tagLength: 128 },
|
|
557
|
-
key,
|
|
558
|
-
plaintextBytes
|
|
559
|
-
);
|
|
560
|
-
// 字段顺序属于持久化协议:版本、KDF 成本、盐、IV、带 GCM Tag 的密文。
|
|
561
|
-
return [
|
|
562
|
-
encryptedPayloadPrefix,
|
|
563
|
-
String(iterations),
|
|
564
|
-
encodeBase64UrlBytes(salt),
|
|
565
|
-
encodeBase64UrlBytes(iv),
|
|
566
|
-
encodeBase64UrlBytes(new Uint8Array(ciphertext)),
|
|
567
|
-
].join(":");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* 解密 {@link encryptTextWithPassword} 生成的 v2 认证载荷。
|
|
572
|
-
*
|
|
573
|
-
* @param payload - 未修改的 v2 载荷,最大约 16 MiB 文本。
|
|
574
|
-
* @param password - 加密时使用的口令。
|
|
575
|
-
* @returns 原始 UTF-8 文本。
|
|
576
|
-
* @throws 格式或字段非法时抛出 `TypeError`,载荷过大时抛出 `RangeError`,认证或密码
|
|
577
|
-
* 失败及缺少平台能力时抛出 `Error`。
|
|
578
|
-
*/
|
|
579
|
-
export async function decryptTextWithPassword(payload: string, password: string): Promise<string> {
|
|
580
|
-
const passwordBytes = encodeValidatedPassword(password);
|
|
581
|
-
if (payload.length > maximumPayloadLength) {
|
|
582
|
-
throw new RangeError("The encrypted payload exceeds the supported size.");
|
|
583
|
-
}
|
|
584
|
-
// 先验证固定字段数和版本,再对高成本 KDF 与解密进行任何工作。
|
|
585
|
-
const parts = payload.split(":");
|
|
586
|
-
if (parts.length !== 5 || parts[0] !== encryptedPayloadPrefix) {
|
|
587
|
-
throw new TypeError("The encrypted payload format is not supported.");
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
let iterations: number;
|
|
591
|
-
let salt: Uint8Array;
|
|
592
|
-
let iv: Uint8Array;
|
|
593
|
-
let ciphertext: Uint8Array;
|
|
594
|
-
try {
|
|
595
|
-
iterations = validateIterations(Number(parts[1]));
|
|
596
|
-
salt = decodeBase64UrlBytes(parts[2] ?? "");
|
|
597
|
-
iv = decodeBase64UrlBytes(parts[3] ?? "");
|
|
598
|
-
ciphertext = decodeBase64UrlBytes(parts[4] ?? "");
|
|
599
|
-
if (salt.length !== 16 || iv.length !== 12 || ciphertext.length < 16) {
|
|
600
|
-
throw new TypeError("The encrypted payload has invalid field lengths.");
|
|
601
|
-
}
|
|
602
|
-
} catch (cause) {
|
|
603
|
-
throw new TypeError("The encrypted payload contains invalid fields.", { cause });
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const crypto = requireWebCrypto();
|
|
607
|
-
const textDecoder = getTextDecoder();
|
|
608
|
-
try {
|
|
609
|
-
const key = await derivePasswordEncryptionKey(passwordBytes, salt, iterations, "decrypt");
|
|
610
|
-
const plaintext = await crypto.subtle.decrypt(
|
|
611
|
-
{ additionalData: toArrayBuffer(encodeUtf8(encryptedPayloadPrefix)), iv: toArrayBuffer(iv), name: "AES-GCM", tagLength: 128 },
|
|
612
|
-
key,
|
|
613
|
-
toArrayBuffer(ciphertext)
|
|
614
|
-
);
|
|
615
|
-
return textDecoder.decode(plaintext);
|
|
616
|
-
} catch (cause) {
|
|
617
|
-
throw new Error("The payload could not be authenticated or decrypted.", { cause });
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* 规范化历史 AES 模式参数。
|
|
623
|
-
*
|
|
624
|
-
* @param cipherMode - 公共 API 接收的未知模式值。
|
|
625
|
-
* @returns 省略值和 `CBC` 返回 `CBC`,`ECB` 保持原值。
|
|
626
|
-
* @throws `RangeError` 当值不是公开协议允许的字符串。
|
|
627
|
-
*/
|
|
628
|
-
const resolveLegacyAesMode = (cipherMode: unknown): LegacyAesCipherMode => {
|
|
629
|
-
if (cipherMode === undefined || cipherMode === "CBC") return "CBC";
|
|
630
|
-
if (cipherMode === "ECB") return "ECB";
|
|
631
|
-
throw new RangeError('cipherMode must be "CBC" or "ECB".');
|
|
632
|
-
};
|
|
633
|
-
|
|
634
|
-
/** 解密历史 AES-CBC/ECB 字符串,并优先解析 JSON 结果。 */
|
|
635
|
-
export function decryptLegacyAesValue<Result = string>(
|
|
636
|
-
dataStr: string,
|
|
637
|
-
key: string,
|
|
638
|
-
vector: string,
|
|
639
|
-
cipherMode?: LegacyAesCipherMode
|
|
640
|
-
): Result | null {
|
|
641
|
-
if (dataStr.length === 0) return null;
|
|
642
|
-
try {
|
|
643
|
-
const mode = resolveLegacyAesMode(cipherMode);
|
|
644
|
-
const plaintext = mode === "ECB" ? decryptLegacyAesEcb(dataStr, key) : decryptLegacyAesCbc(dataStr, key, vector);
|
|
645
|
-
if (plaintext.length === 0) return null;
|
|
646
|
-
try {
|
|
647
|
-
return JSON.parse(plaintext) as Result;
|
|
648
|
-
} catch {
|
|
649
|
-
return plaintext as Result;
|
|
650
|
-
}
|
|
651
|
-
} catch {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/** 按指定 CBC/ECB 模式加密历史 AES 字符串。 */
|
|
657
|
-
export function encryptLegacyAes(dataStr: string, key: string, vector: string, cipherMode?: LegacyAesCipherMode): string {
|
|
658
|
-
const mode = resolveLegacyAesMode(cipherMode);
|
|
659
|
-
return mode === "ECB" ? encryptLegacyAesEcb(dataStr, key) : encryptLegacyAesCbc(dataStr, key, vector);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/** 返回历史兼容的大写 MD5 十六进制摘要。 */
|
|
663
|
-
export function md5UpperHex(value: string): string {
|
|
664
|
-
return value.length === 0 ? value : md5Hex(value).toUpperCase();
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/** 返回历史兼容的大写 SHA-1 十六进制摘要。 */
|
|
668
|
-
export function sha1UpperHex(value: string): string {
|
|
669
|
-
return value.length === 0 ? value : sha1Hex(value).toUpperCase();
|
|
670
|
-
}
|