@fast-china/utils 2.0.0 → 2.0.3
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 +34 -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 +266 -97
- package/dist/crypto/index.mjs +565 -263
- 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/dist/crypto/index.mjs
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { encodeUtf8, getTextDecoder } from "../internal/text.mjs";
|
|
2
2
|
import { decodeBase64Bytes, decodeBase64UrlBytes, encodeBase64Bytes, encodeBase64UrlBytes } from "../base64/index.mjs";
|
|
3
3
|
import AES from "crypto-js/aes.js";
|
|
4
|
+
import CryptoCore from "crypto-js/core.js";
|
|
4
5
|
import Hex from "crypto-js/enc-hex.js";
|
|
5
6
|
import Utf8 from "crypto-js/enc-utf8.js";
|
|
6
7
|
import MD5 from "crypto-js/md5.js";
|
|
7
8
|
import ECB from "crypto-js/mode-ecb.js";
|
|
9
|
+
import "crypto-js/pad-ansix923.js";
|
|
10
|
+
import Iso10126 from "crypto-js/pad-iso10126.js";
|
|
11
|
+
import NoPadding from "crypto-js/pad-nopadding.js";
|
|
8
12
|
import Pkcs7 from "crypto-js/pad-pkcs7.js";
|
|
13
|
+
import ZeroPadding from "crypto-js/pad-zeropadding.js";
|
|
9
14
|
import SHA1 from "crypto-js/sha1.js";
|
|
10
15
|
//#region src/crypto/index.ts
|
|
11
|
-
/**
|
|
16
|
+
/** PBKDF2 默认迭代次数。 */
|
|
12
17
|
const defaultPbkdf2Iterations = 6e5;
|
|
18
|
+
/** PBKDF2 允许的最小迭代次数。 */
|
|
13
19
|
const minimumPbkdf2Iterations = 1e5;
|
|
20
|
+
/** PBKDF2 允许的最大迭代次数,用于限制异常输入造成的计算资源消耗。 */
|
|
14
21
|
const maximumPbkdf2Iterations = 5e6;
|
|
22
|
+
/** 密码允许的最大 UTF-8 字节数。 */
|
|
15
23
|
const maximumPasswordBytes = 1024;
|
|
24
|
+
/** 密码加密接口允许的最大明文字节数。 */
|
|
16
25
|
const maximumPlaintextBytes = 8388608;
|
|
26
|
+
/** 密码解密接口允许的最大协议载荷字符数。 */
|
|
17
27
|
const maximumPayloadLength = 16777216;
|
|
18
|
-
|
|
28
|
+
/** 密码加密载荷的协议与算法版本前缀。 */
|
|
29
|
+
const encryptedPayloadPrefix = "FAST-AES-256-GCM-V1";
|
|
30
|
+
/** PBKDF2 密码哈希的协议与算法版本前缀。 */
|
|
31
|
+
const passwordHashPrefix = "FAST-PBKDF2-SHA256-V1";
|
|
32
|
+
/** 直接 AES-GCM 密钥加密载荷的协议版本。 */
|
|
33
|
+
const authenticatedAesPayloadVersion = 1;
|
|
34
|
+
/** 延迟读取的 Web Crypto 全局对象视图,便于在缺少该能力的平台给出明确错误。 */
|
|
19
35
|
const runtimeCryptoGlobals = globalThis;
|
|
20
36
|
/**
|
|
21
37
|
* 获取安全随机数能力。
|
|
@@ -42,13 +58,6 @@ const requireWebCrypto = () => {
|
|
|
42
58
|
return crypto;
|
|
43
59
|
};
|
|
44
60
|
/**
|
|
45
|
-
* 把二进制联合输入规范为字节。
|
|
46
|
-
*
|
|
47
|
-
* @param value - UTF-8 文本或现有字节数组。
|
|
48
|
-
* @returns 文本的新编码结果,或原始字节数组引用。
|
|
49
|
-
*/
|
|
50
|
-
const toInputBytes = (value) => typeof value === "string" ? encodeUtf8(value) : value;
|
|
51
|
-
/**
|
|
52
61
|
* 把 Uint8Array 复制为独立的完整 ArrayBuffer。
|
|
53
62
|
*
|
|
54
63
|
* @remarks 不能直接返回 `bytes.buffer`,因为输入可能只是更大 Buffer 的切片。
|
|
@@ -118,46 +127,6 @@ const assertKeyPair = (value) => {
|
|
|
118
127
|
throw new Error("The runtime did not generate a key pair.");
|
|
119
128
|
};
|
|
120
129
|
/**
|
|
121
|
-
* 按旧协议规范化 AES Key。
|
|
122
|
-
*
|
|
123
|
-
* @param key - 任意长度 UTF-8 文本 Key。
|
|
124
|
-
* @returns 补字符 `f` 或截断到 32 个 UTF-16 Code Unit 后的 CryptoJS WordArray。
|
|
125
|
-
*/
|
|
126
|
-
const normalizeAesKey = (key) => Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
127
|
-
/**
|
|
128
|
-
* 按旧协议规范化 AES IV。
|
|
129
|
-
*
|
|
130
|
-
* @param vector - 任意长度 UTF-8 文本 IV。
|
|
131
|
-
* @returns 补字符 `f` 或截断到 16 个 UTF-16 Code Unit 后的 CryptoJS WordArray。
|
|
132
|
-
*/
|
|
133
|
-
const normalizeAesVector = (vector) => Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
134
|
-
/**
|
|
135
|
-
* 选择与 ECDSA 曲线对应的摘要算法。
|
|
136
|
-
*
|
|
137
|
-
* @param namedCurve - 本模块支持的 NIST 曲线。
|
|
138
|
-
* @returns P-256、P-384、P-521 分别对应 SHA-256、SHA-384、SHA-512。
|
|
139
|
-
*/
|
|
140
|
-
const getEcdsaHash = (namedCurve) => {
|
|
141
|
-
switch (namedCurve) {
|
|
142
|
-
case "P-256": return "SHA-256";
|
|
143
|
-
case "P-384": return "SHA-384";
|
|
144
|
-
case "P-521": return "SHA-512";
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* 获取 ECDH 共享秘密的导出位数。
|
|
149
|
-
*
|
|
150
|
-
* @param namedCurve - 本模块支持的 NIST 曲线。
|
|
151
|
-
* @returns 曲线字段按完整字节对齐后的位数;P-521 返回 528。
|
|
152
|
-
*/
|
|
153
|
-
const getEcBitLength = (namedCurve) => {
|
|
154
|
-
switch (namedCurve) {
|
|
155
|
-
case "P-256": return 256;
|
|
156
|
-
case "P-384": return 384;
|
|
157
|
-
case "P-521": return 528;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
/**
|
|
161
130
|
* 校验并编码密码。
|
|
162
131
|
*
|
|
163
132
|
* @param password - 用户提供的密码文本。
|
|
@@ -182,28 +151,30 @@ const validateIterations = (iterations) => {
|
|
|
182
151
|
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) throw new RangeError(`iterations must be a safe integer between ${minimumPbkdf2Iterations} and ${maximumPbkdf2Iterations}.`);
|
|
183
152
|
return iterations;
|
|
184
153
|
};
|
|
154
|
+
/** 把字节格式化为小写十六进制。 */
|
|
155
|
+
const toLowerHex = (value) => Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
156
|
+
/** 把字节格式化为大写十六进制。 */
|
|
157
|
+
const toUpperHex = (value) => toLowerHex(value).toUpperCase();
|
|
185
158
|
/**
|
|
186
|
-
*
|
|
159
|
+
* 使用指定的 Web Crypto HMAC 算法计算原始认证标签。
|
|
187
160
|
*
|
|
188
|
-
* @param
|
|
189
|
-
* @param
|
|
190
|
-
* @param
|
|
191
|
-
* @
|
|
192
|
-
* @
|
|
193
|
-
* @throws `Error`
|
|
161
|
+
* @param value - 要认证的 UTF-8 文本或原始字节。
|
|
162
|
+
* @param key - 非空的 UTF-8 文本密钥或原始密钥字节。
|
|
163
|
+
* @param hash - HMAC 使用的 SHA-2 摘要算法。
|
|
164
|
+
* @returns 算法规定长度的原始认证标签。
|
|
165
|
+
* @throws `TypeError` 当密钥为空。
|
|
166
|
+
* @throws `Error` 当运行时缺少所需 Web Crypto 能力。
|
|
194
167
|
*/
|
|
195
|
-
const
|
|
168
|
+
const computeHmacBytes = async (value, key, hash) => {
|
|
169
|
+
const keyBytes = encodeUtf8(key);
|
|
170
|
+
if (keyBytes.length === 0) throw new TypeError("The HMAC key cannot be empty.");
|
|
196
171
|
const crypto = requireWebCrypto();
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}, material, {
|
|
204
|
-
length: 256,
|
|
205
|
-
name: "AES-GCM"
|
|
206
|
-
}, false, [usage]);
|
|
172
|
+
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), {
|
|
173
|
+
hash,
|
|
174
|
+
name: "HMAC"
|
|
175
|
+
}, false, ["sign"]);
|
|
176
|
+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, toArrayBuffer(encodeUtf8(value)));
|
|
177
|
+
return new Uint8Array(signature);
|
|
207
178
|
};
|
|
208
179
|
/**
|
|
209
180
|
* 生成安全随机字节。
|
|
@@ -212,11 +183,46 @@ const derivePasswordEncryptionKey = async (passwordBytes, salt, iterations, usag
|
|
|
212
183
|
* @returns 新建的 `Uint8Array`。
|
|
213
184
|
* @throws 参数非法时抛出 `RangeError`;缺少 Web Crypto 时抛出 `Error`。
|
|
214
185
|
*/
|
|
215
|
-
function
|
|
186
|
+
function GenerateRandomBytes(length) {
|
|
216
187
|
if (!Number.isSafeInteger(length) || length < 0 || length > 65536) throw new RangeError("length must be a safe integer from 0 through 65,536.");
|
|
217
188
|
return requireRandomCrypto().getRandomValues(new Uint8Array(length));
|
|
218
189
|
}
|
|
219
190
|
/**
|
|
191
|
+
* 以不提前退出的方式比较两个字节数组。
|
|
192
|
+
*
|
|
193
|
+
* @remarks JavaScript 引擎不保证严格常量时间;该函数只避免显式短路,不能替代服务端
|
|
194
|
+
* 密码学库提供的 timing-safe primitive。长度是否相同仍属于可观察信息。
|
|
195
|
+
* @param left - 第一字节序列。
|
|
196
|
+
* @param right - 第二字节序列。
|
|
197
|
+
* @returns 长度和每个字节均相同时返回 `true`。
|
|
198
|
+
*/
|
|
199
|
+
function FixedTimeEquals(left, right) {
|
|
200
|
+
const length = Math.max(left.length, right.length);
|
|
201
|
+
let difference = left.length === right.length ? 0 : 1;
|
|
202
|
+
for (let index = 0; index < length; index += 1) difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
203
|
+
return difference === 0;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* 计算 MD5 摘要并返回小写十六进制文本。
|
|
207
|
+
*
|
|
208
|
+
* @remarks MD5 仅用于非安全的普通校验,不得用于密码、签名或抗碰撞场景。
|
|
209
|
+
* @param value - UTF-8 文本。
|
|
210
|
+
* @returns 32 字符小写十六进制摘要。
|
|
211
|
+
*/
|
|
212
|
+
function MD5Encrypt(value) {
|
|
213
|
+
return MD5(value).toString(Hex);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 计算 SHA-1 摘要并返回大写十六进制文本。
|
|
217
|
+
*
|
|
218
|
+
* @remarks SHA-1 仅用于非安全的普通校验,不得用于密码、签名或抗碰撞场景。
|
|
219
|
+
* @param value - UTF-8 文本。
|
|
220
|
+
* @returns 40 字符大写十六进制摘要。
|
|
221
|
+
*/
|
|
222
|
+
function SHA1Encrypt(value) {
|
|
223
|
+
return SHA1(value).toString(Hex).toUpperCase();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
220
226
|
* 计算 SHA-256 摘要。
|
|
221
227
|
*
|
|
222
228
|
* @remarks SHA-256 是快速摘要,不适合直接存储或校验密码。
|
|
@@ -224,222 +230,346 @@ function generateRandomBytes(length) {
|
|
|
224
230
|
* @returns 32 字节摘要。
|
|
225
231
|
* @throws 缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
226
232
|
*/
|
|
227
|
-
async function
|
|
228
|
-
const digest = await requireWebCrypto().subtle.digest("SHA-256", toArrayBuffer(
|
|
233
|
+
async function SHA256Bytes(value) {
|
|
234
|
+
const digest = await requireWebCrypto().subtle.digest("SHA-256", toArrayBuffer(encodeUtf8(value)));
|
|
229
235
|
return new Uint8Array(digest);
|
|
230
236
|
}
|
|
231
237
|
/**
|
|
232
238
|
* 计算 SHA-256 并格式化为十六进制。
|
|
233
239
|
*
|
|
234
240
|
* @param value - UTF-8 字符串或原始字节。
|
|
235
|
-
* @returns 64
|
|
241
|
+
* @returns 64 字符大写十六进制文本。
|
|
236
242
|
* @throws 缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
237
243
|
*/
|
|
238
|
-
async function
|
|
239
|
-
|
|
240
|
-
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
244
|
+
async function SHA256Encrypt(value) {
|
|
245
|
+
return toUpperHex(await SHA256Bytes(value));
|
|
241
246
|
}
|
|
242
247
|
/**
|
|
243
|
-
* 计算
|
|
248
|
+
* 计算 SHA-384 摘要。
|
|
244
249
|
*
|
|
245
|
-
* @
|
|
246
|
-
* @
|
|
247
|
-
* @returns 32 字符小写十六进制摘要。
|
|
250
|
+
* @param value - UTF-8 文本或原始字节。
|
|
251
|
+
* @returns 48 字节摘要。
|
|
248
252
|
*/
|
|
249
|
-
function
|
|
250
|
-
|
|
253
|
+
async function SHA384Bytes(value) {
|
|
254
|
+
const digest = await requireWebCrypto().subtle.digest("SHA-384", toArrayBuffer(encodeUtf8(value)));
|
|
255
|
+
return new Uint8Array(digest);
|
|
251
256
|
}
|
|
252
257
|
/**
|
|
253
|
-
* 计算 SHA-
|
|
258
|
+
* 计算 SHA-384 并格式化为十六进制文本。
|
|
254
259
|
*
|
|
255
|
-
* @
|
|
256
|
-
* @
|
|
257
|
-
* @returns 40 字符小写十六进制摘要。
|
|
260
|
+
* @param value - UTF-8 文本或原始字节。
|
|
261
|
+
* @returns 96 个大写十六进制字符组成的摘要。
|
|
258
262
|
*/
|
|
259
|
-
function
|
|
260
|
-
return
|
|
263
|
+
async function SHA384Encrypt(value) {
|
|
264
|
+
return toUpperHex(await SHA384Bytes(value));
|
|
261
265
|
}
|
|
262
266
|
/**
|
|
263
|
-
*
|
|
267
|
+
* 计算 SHA-512 摘要。
|
|
264
268
|
*
|
|
265
|
-
* @
|
|
266
|
-
*
|
|
267
|
-
* @param plaintext - UTF-8 明文。
|
|
268
|
-
* @param key - 兼容密钥文本。
|
|
269
|
-
* @param vector - 兼容初始化向量文本。
|
|
270
|
-
* @returns CryptoJS Base64 密文。
|
|
269
|
+
* @param value - UTF-8 文本或原始字节。
|
|
270
|
+
* @returns 64 字节摘要。
|
|
271
271
|
*/
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
return
|
|
275
|
-
iv: normalizeAesVector(vector),
|
|
276
|
-
padding: Pkcs7
|
|
277
|
-
}).toString();
|
|
272
|
+
async function SHA512Bytes(value) {
|
|
273
|
+
const digest = await requireWebCrypto().subtle.digest("SHA-512", toArrayBuffer(encodeUtf8(value)));
|
|
274
|
+
return new Uint8Array(digest);
|
|
278
275
|
}
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
276
|
+
/**
|
|
277
|
+
* 计算 SHA-512 并格式化为十六进制文本。
|
|
278
|
+
*
|
|
279
|
+
* @param value - UTF-8 文本或原始字节。
|
|
280
|
+
* @returns 128 个大写十六进制字符组成的摘要。
|
|
281
|
+
*/
|
|
282
|
+
async function SHA512Encrypt(value) {
|
|
283
|
+
return toUpperHex(await SHA512Bytes(value));
|
|
286
284
|
}
|
|
287
285
|
/**
|
|
288
|
-
* 使用
|
|
286
|
+
* 使用 HMAC-SHA-256 认证文本,并返回十六进制标签。
|
|
289
287
|
*
|
|
290
|
-
* @
|
|
291
|
-
* @param
|
|
292
|
-
* @
|
|
293
|
-
* @returns CryptoJS Base64 密文。
|
|
288
|
+
* @param value - 要认证的 UTF-8 文本或原始字节。
|
|
289
|
+
* @param key - 非空的 UTF-8 文本密钥或原始密钥字节。
|
|
290
|
+
* @returns 64 个小写十六进制字符组成的认证标签。
|
|
294
291
|
*/
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
return AES.encrypt(plaintext, normalizeAesKey(key), {
|
|
298
|
-
mode: ECB,
|
|
299
|
-
padding: Pkcs7
|
|
300
|
-
}).toString();
|
|
292
|
+
async function HMACSHA256Encrypt(value, key) {
|
|
293
|
+
return toLowerHex(await computeHmacBytes(value, key, "SHA-256"));
|
|
301
294
|
}
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
295
|
+
/**
|
|
296
|
+
* 使用 HMAC-SHA-384 认证文本或字节,并返回十六进制标签。
|
|
297
|
+
*
|
|
298
|
+
* @param value - 要认证的 UTF-8 文本或原始字节。
|
|
299
|
+
* @param key - 非空的 UTF-8 文本密钥或原始密钥字节。
|
|
300
|
+
* @returns 96 个小写十六进制字符组成的认证标签。
|
|
301
|
+
*/
|
|
302
|
+
async function HMACSHA384Encrypt(value, key) {
|
|
303
|
+
return toLowerHex(await computeHmacBytes(value, key, "SHA-384"));
|
|
309
304
|
}
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
publicExponent: Uint8Array.of(1, 0, 1)
|
|
320
|
-
}, true, ["decrypt", "encrypt"]));
|
|
321
|
-
return exportKeyPair(keyPair);
|
|
305
|
+
/**
|
|
306
|
+
* 使用 HMAC-SHA-512 认证文本或字节,并返回十六进制标签。
|
|
307
|
+
*
|
|
308
|
+
* @param value - 要认证的 UTF-8 文本或原始字节。
|
|
309
|
+
* @param key - 非空的 UTF-8 文本密钥或原始密钥字节。
|
|
310
|
+
* @returns 128 个小写十六进制字符组成的认证标签。
|
|
311
|
+
*/
|
|
312
|
+
async function HMACSHA512Encrypt(value, key) {
|
|
313
|
+
return toLowerHex(await computeHmacBytes(value, key, "SHA-512"));
|
|
322
314
|
}
|
|
323
|
-
/**
|
|
324
|
-
|
|
315
|
+
/**
|
|
316
|
+
* 使用 PBKDF2-HMAC-SHA-256 从密码派生密钥。
|
|
317
|
+
*
|
|
318
|
+
* @param password - 1 至 1,024 UTF-8 字节的密码。
|
|
319
|
+
* @param salt - 至少 8 字节的盐。
|
|
320
|
+
* @param iterations - 迭代次数,范围为 100,000 至 5,000,000。
|
|
321
|
+
* @param outputLength - 输出长度,范围为 1 至 1,024 字节。
|
|
322
|
+
* @returns 指定长度的派生密钥。
|
|
323
|
+
* @throws 参数超过协议边界时抛出 `TypeError` 或 `RangeError`。
|
|
324
|
+
*/
|
|
325
|
+
async function PBKDF2SHA256(password, salt, iterations = defaultPbkdf2Iterations, outputLength = 32) {
|
|
326
|
+
const passwordBytes = encodeValidatedPassword(password);
|
|
327
|
+
if (salt.length < 8) throw new RangeError("The PBKDF2 salt must contain at least 8 bytes; 16 or more is recommended.");
|
|
328
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 1024) throw new RangeError("outputLength must be a safe integer between 1 and 1,024.");
|
|
325
329
|
const crypto = requireWebCrypto();
|
|
326
|
-
const
|
|
330
|
+
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveBits"]);
|
|
331
|
+
const derived = await crypto.subtle.deriveBits({
|
|
327
332
|
hash: "SHA-256",
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
333
|
+
iterations: validateIterations(iterations),
|
|
334
|
+
name: "PBKDF2",
|
|
335
|
+
salt: toArrayBuffer(salt)
|
|
336
|
+
}, material, outputLength * 8);
|
|
337
|
+
return new Uint8Array(derived);
|
|
332
338
|
}
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
/**
|
|
340
|
+
* 生成可持久化的随机盐 PBKDF2-HMAC-SHA-256 密码哈希。
|
|
341
|
+
*
|
|
342
|
+
* @param password - 1 至 1,024 UTF-8 字节的密码。
|
|
343
|
+
* @param iterations - 迭代次数,范围为 100,000 至 5,000,000。
|
|
344
|
+
* @returns 包含版本、迭代次数、16 字节随机盐和 32 字节派生密钥的自描述字符串。
|
|
345
|
+
*/
|
|
346
|
+
async function HashPasswordPBKDF2SHA256(password, iterations = defaultPbkdf2Iterations) {
|
|
347
|
+
const salt = GenerateRandomBytes(16);
|
|
348
|
+
const derivedKey = await PBKDF2SHA256(password, salt, iterations);
|
|
349
|
+
return [
|
|
350
|
+
passwordHashPrefix,
|
|
351
|
+
String(iterations),
|
|
352
|
+
encodeBase64UrlBytes(salt),
|
|
353
|
+
encodeBase64UrlBytes(derivedKey)
|
|
354
|
+
].join(":");
|
|
342
355
|
}
|
|
343
|
-
/**
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
356
|
+
/**
|
|
357
|
+
* 验证 {@link HashPasswordPBKDF2SHA256} 生成的密码哈希。
|
|
358
|
+
*
|
|
359
|
+
* @param password - 要验证的密码。
|
|
360
|
+
* @param passwordHash - 自描述的 PBKDF2-HMAC-SHA-256 密码哈希。
|
|
361
|
+
* @returns 格式有效且密码匹配时返回 `true`;格式无效或密码错误时返回 `false`。
|
|
362
|
+
*/
|
|
363
|
+
async function VerifyPasswordPBKDF2SHA256(password, passwordHash) {
|
|
364
|
+
try {
|
|
365
|
+
const parts = passwordHash.split(":");
|
|
366
|
+
if (parts.length !== 4 || parts[0] !== passwordHashPrefix) return false;
|
|
367
|
+
const iterations = validateIterations(Number(parts[1]));
|
|
368
|
+
const salt = decodeBase64UrlBytes(parts[2] ?? "");
|
|
369
|
+
const expected = decodeBase64UrlBytes(parts[3] ?? "");
|
|
370
|
+
if (salt.length !== 16 || expected.length !== 32) return false;
|
|
371
|
+
return FixedTimeEquals(await PBKDF2SHA256(password, salt, iterations, expected.length), expected);
|
|
372
|
+
} catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
351
375
|
}
|
|
352
|
-
/**
|
|
353
|
-
|
|
376
|
+
/**
|
|
377
|
+
* 使用 RFC 5869 HKDF-SHA-256 派生上下文隔离的密钥材料。
|
|
378
|
+
*
|
|
379
|
+
* @param inputKeyMaterial - 输入密钥材料,例如 ECDH 原始共享秘密。
|
|
380
|
+
* @param salt - 可选盐;空值按 RFC 5869 的零盐语义处理。
|
|
381
|
+
* @param info - 应用、协议和密钥用途上下文。
|
|
382
|
+
* @param outputLength - 输出长度,范围为 1 至 8,160 字节。
|
|
383
|
+
* @returns 与 `salt` 和 `info` 绑定的派生密钥。
|
|
384
|
+
*/
|
|
385
|
+
async function HKDFSHA256(inputKeyMaterial, salt = /* @__PURE__ */ new Uint8Array(), info = /* @__PURE__ */ new Uint8Array(), outputLength = 32) {
|
|
386
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 8160) throw new RangeError("outputLength must be a safe integer between 1 and 8,160.");
|
|
354
387
|
const crypto = requireWebCrypto();
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return encodeBase64Bytes(new Uint8Array(signature));
|
|
388
|
+
const material = await crypto.subtle.importKey("raw", toArrayBuffer(inputKeyMaterial), "HKDF", false, ["deriveBits"]);
|
|
389
|
+
const derived = await crypto.subtle.deriveBits({
|
|
390
|
+
hash: "SHA-256",
|
|
391
|
+
info: toArrayBuffer(info),
|
|
392
|
+
name: "HKDF",
|
|
393
|
+
salt: toArrayBuffer(salt)
|
|
394
|
+
}, material, outputLength * 8);
|
|
395
|
+
return new Uint8Array(derived);
|
|
364
396
|
}
|
|
365
|
-
/**
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
397
|
+
/**
|
|
398
|
+
* 使用 AES-256 对 UTF-8 文本进行分组加密。
|
|
399
|
+
*
|
|
400
|
+
* @remarks 密钥和 IV 分别补字符 `f` 或截断到 32、16 个 UTF-16 Code Unit,与 .NET
|
|
401
|
+
* `AESEncrypt` 保持一致。CBC/ECB 不提供完整性认证,密文可能被篡改。
|
|
402
|
+
* @param dataStr - 要加密的 UTF-8 文本;空白文本返回 `null`。
|
|
403
|
+
* @param key - 非空白的密钥文本。
|
|
404
|
+
* @param vector - 非空白的初始化向量文本;ECB 模式仍要求传入该参数以对齐 .NET 签名。
|
|
405
|
+
* @param cipherMode - AES 分组模式,默认 `CBC`。
|
|
406
|
+
* @param paddingMode - AES 填充模式,默认 `PKCS7`。
|
|
407
|
+
* @returns Base64 密文;输入、密钥或 IV 为空白时返回 `null`。
|
|
408
|
+
* @throws 模式或填充不受支持时抛出 `RangeError`。
|
|
409
|
+
*/
|
|
410
|
+
function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
411
|
+
if (dataStr.trim().length === 0 || key.trim().length === 0 || vector.trim().length === 0) return null;
|
|
412
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("cipherMode must be \"CBC\" or \"ECB\".");
|
|
413
|
+
let padding = Pkcs7;
|
|
414
|
+
switch (paddingMode) {
|
|
415
|
+
case "None":
|
|
416
|
+
padding = NoPadding;
|
|
417
|
+
break;
|
|
418
|
+
case "PKCS7": break;
|
|
419
|
+
case "Zeros":
|
|
420
|
+
padding = ZeroPadding;
|
|
421
|
+
break;
|
|
422
|
+
case "ANSIX923":
|
|
423
|
+
padding = CryptoCore.pad.AnsiX923;
|
|
424
|
+
break;
|
|
425
|
+
case "ISO10126":
|
|
426
|
+
padding = Iso10126;
|
|
427
|
+
break;
|
|
428
|
+
default: throw new RangeError("paddingMode is not supported.");
|
|
429
|
+
}
|
|
430
|
+
if (paddingMode === "None" && encodeUtf8(dataStr).length % 16 !== 0) throw new RangeError("AES plaintext must contain a whole number of 16-byte blocks when paddingMode is None.");
|
|
431
|
+
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
432
|
+
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
433
|
+
return AES.encrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
434
|
+
iv: vectorBytes,
|
|
435
|
+
padding
|
|
436
|
+
} : {
|
|
437
|
+
iv: vectorBytes,
|
|
438
|
+
mode: ECB,
|
|
439
|
+
padding
|
|
440
|
+
}).toString();
|
|
376
441
|
}
|
|
377
|
-
/**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
442
|
+
/**
|
|
443
|
+
* 使用 AES-256 解密 Base64 分组密文。
|
|
444
|
+
*
|
|
445
|
+
* @remarks 参数归一化规则与 {@link AESEncrypt} 以及 .NET `AESDecrypt` 相同。
|
|
446
|
+
* @param dataStr - Base64 密文;空白文本返回 `null`。
|
|
447
|
+
* @param key - 加密时使用的密钥文本。
|
|
448
|
+
* @param vector - 加密时使用的初始化向量文本。
|
|
449
|
+
* @param cipherMode - AES 分组模式,默认 `CBC`。
|
|
450
|
+
* @param paddingMode - AES 填充模式,默认 `PKCS7`。
|
|
451
|
+
* @returns 解密后的 UTF-8 文本;输入、密钥或 IV 为空白时返回 `null`。
|
|
452
|
+
* @throws 模式、填充、Base64、密钥或密文无效时抛出错误。
|
|
453
|
+
*/
|
|
454
|
+
function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
455
|
+
if (dataStr.trim().length === 0 || key.trim().length === 0 || vector.trim().length === 0) return null;
|
|
456
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("cipherMode must be \"CBC\" or \"ECB\".");
|
|
457
|
+
let padding = Pkcs7;
|
|
458
|
+
switch (paddingMode) {
|
|
459
|
+
case "None":
|
|
460
|
+
padding = NoPadding;
|
|
461
|
+
break;
|
|
462
|
+
case "PKCS7": break;
|
|
463
|
+
case "Zeros":
|
|
464
|
+
padding = NoPadding;
|
|
465
|
+
break;
|
|
466
|
+
case "ANSIX923":
|
|
467
|
+
padding = CryptoCore.pad.AnsiX923;
|
|
468
|
+
break;
|
|
469
|
+
case "ISO10126":
|
|
470
|
+
padding = Iso10126;
|
|
471
|
+
break;
|
|
472
|
+
default: throw new RangeError("paddingMode is not supported.");
|
|
473
|
+
}
|
|
474
|
+
const ciphertextBytes = decodeBase64Bytes(dataStr);
|
|
475
|
+
if (ciphertextBytes.length === 0 || ciphertextBytes.length % 16 !== 0) throw new RangeError("AES ciphertext must contain one or more complete 16-byte blocks.");
|
|
476
|
+
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
477
|
+
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
478
|
+
return AES.decrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
479
|
+
iv: vectorBytes,
|
|
480
|
+
padding
|
|
481
|
+
} : {
|
|
482
|
+
iv: vectorBytes,
|
|
483
|
+
mode: ECB,
|
|
484
|
+
padding
|
|
485
|
+
}).toString(Utf8);
|
|
385
486
|
}
|
|
386
487
|
/**
|
|
387
|
-
*
|
|
488
|
+
* 使用 SHA-256 归一化文本密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
388
489
|
*
|
|
389
|
-
* @remarks
|
|
490
|
+
* @remarks 输出与 .NET `AESEncryptAuthenticated` 的 v1 Base64 二进制载荷完全一致。
|
|
491
|
+
* @param plaintext - 要加密的 UTF-8 文本。
|
|
492
|
+
* @param key - 非空的 UTF-8 文本密钥;内部归一化为 32 字节 SHA-256 摘要。
|
|
493
|
+
* @returns Base64 编码的 v1 AES-GCM 认证载荷。
|
|
494
|
+
* @throws 密钥为空或运行时缺少 Web Crypto 时抛出错误。
|
|
390
495
|
*/
|
|
391
|
-
async function
|
|
496
|
+
async function AESEncryptAuthenticated(plaintext, key) {
|
|
497
|
+
if (key.trim().length === 0) throw new TypeError("The encryption key cannot be empty.");
|
|
392
498
|
const crypto = requireWebCrypto();
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
499
|
+
const keyBytes = await SHA256Bytes(key);
|
|
500
|
+
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), { name: "AES-GCM" }, false, ["encrypt"]);
|
|
501
|
+
const nonce = GenerateRandomBytes(12);
|
|
502
|
+
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
|
|
503
|
+
iv: toArrayBuffer(nonce),
|
|
504
|
+
name: "AES-GCM",
|
|
505
|
+
tagLength: 128
|
|
506
|
+
}, cryptoKey, toArrayBuffer(encodeUtf8(plaintext))));
|
|
507
|
+
const ciphertextLength = encrypted.length - 16;
|
|
508
|
+
const payload = new Uint8Array(1 + nonce.length + 16 + ciphertextLength);
|
|
509
|
+
payload[0] = authenticatedAesPayloadVersion;
|
|
510
|
+
payload.set(nonce, 1);
|
|
511
|
+
payload.set(encrypted.subarray(ciphertextLength), 1 + nonce.length);
|
|
512
|
+
payload.set(encrypted.subarray(0, ciphertextLength), 1 + nonce.length + 16);
|
|
513
|
+
return encodeBase64Bytes(payload);
|
|
405
514
|
}
|
|
406
515
|
/**
|
|
407
|
-
*
|
|
516
|
+
* 解密并认证 .NET `AESEncryptAuthenticated` 或 {@link AESEncryptAuthenticated} 生成的载荷。
|
|
408
517
|
*
|
|
409
|
-
* @
|
|
410
|
-
*
|
|
411
|
-
* @
|
|
412
|
-
* @
|
|
413
|
-
* @returns 长度和每个字节均相同时返回 `true`。
|
|
518
|
+
* @param payload - Base64 编码的 v1 AES-GCM 二进制载荷。
|
|
519
|
+
* @param key - 加密时使用的非空 UTF-8 文本密钥。
|
|
520
|
+
* @returns 通过认证的 UTF-8 明文。
|
|
521
|
+
* @throws 载荷格式无效、密钥错误或认证失败时抛出错误。
|
|
414
522
|
*/
|
|
415
|
-
function
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
523
|
+
async function AESDecryptAuthenticated(payload, key) {
|
|
524
|
+
if (key.trim().length === 0) throw new TypeError("The encryption key cannot be empty.");
|
|
525
|
+
const decoded = decodeBase64Bytes(payload);
|
|
526
|
+
if (decoded.length < 29 || decoded[0] !== authenticatedAesPayloadVersion) throw new TypeError("The AES-GCM payload format or version is not supported.");
|
|
527
|
+
const nonce = decoded.subarray(1, 13);
|
|
528
|
+
const tag = decoded.subarray(13, 29);
|
|
529
|
+
const ciphertext = decoded.subarray(29);
|
|
530
|
+
const ciphertextAndTag = new Uint8Array(ciphertext.length + tag.length);
|
|
531
|
+
ciphertextAndTag.set(ciphertext);
|
|
532
|
+
ciphertextAndTag.set(tag, ciphertext.length);
|
|
533
|
+
const crypto = requireWebCrypto();
|
|
534
|
+
const keyBytes = await SHA256Bytes(key);
|
|
535
|
+
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), { name: "AES-GCM" }, false, ["decrypt"]);
|
|
536
|
+
const plaintext = await crypto.subtle.decrypt({
|
|
537
|
+
iv: toArrayBuffer(nonce),
|
|
538
|
+
name: "AES-GCM",
|
|
539
|
+
tagLength: 128
|
|
540
|
+
}, cryptoKey, toArrayBuffer(ciphertextAndTag));
|
|
541
|
+
return getTextDecoder().decode(plaintext);
|
|
420
542
|
}
|
|
421
543
|
/**
|
|
422
544
|
* 使用 PBKDF2-HMAC-SHA-256 派生密钥,再以 AES-256-GCM 认证加密 UTF-8 文本。
|
|
423
545
|
*
|
|
424
|
-
* @remarks 每次调用生成独立 16 字节盐与 12 字节 IV
|
|
425
|
-
*
|
|
426
|
-
* 应使用平台 KMS 或专门的加密方案。
|
|
546
|
+
* @remarks 每次调用生成独立 16 字节盐与 12 字节 IV。输出是与 .NET `AESEncryptWithPassword`
|
|
547
|
+
* 一致的 v1 自描述载荷,不应由业务代码手动拆分或修改。密码加密不替代密钥管理。
|
|
427
548
|
* @param plaintext - 原始文本,不进行 JSON 推断;UTF-8 编码后最大 8 MiB。
|
|
428
549
|
* @param password - 1 至 1024 UTF-8 字节的秘密口令。
|
|
429
|
-
* @param
|
|
550
|
+
* @param iterations - PBKDF2 工作因子,默认 600,000。
|
|
430
551
|
* @returns 认证密文字符串;相同输入每次产生不同结果。
|
|
431
552
|
* @throws 口令非法时抛出 `TypeError` 或 `RangeError`;明文过大时抛出 `RangeError`;
|
|
432
553
|
* 运行时缺少 Web Crypto 或 Encoding API 时抛出 `Error`。
|
|
433
554
|
*/
|
|
434
|
-
async function
|
|
555
|
+
async function AESEncryptWithPassword(plaintext, password, iterations = defaultPbkdf2Iterations) {
|
|
435
556
|
const passwordBytes = encodeValidatedPassword(password);
|
|
436
557
|
const plaintextBytes = encodeUtf8(plaintext);
|
|
437
558
|
if (plaintextBytes.length > maximumPlaintextBytes) throw new RangeError(`The UTF-8 plaintext cannot exceed ${maximumPlaintextBytes} bytes.`);
|
|
438
|
-
const
|
|
559
|
+
const validatedIterations = validateIterations(iterations);
|
|
439
560
|
const crypto = requireWebCrypto();
|
|
440
|
-
const salt =
|
|
441
|
-
const iv =
|
|
442
|
-
const
|
|
561
|
+
const salt = GenerateRandomBytes(16);
|
|
562
|
+
const iv = GenerateRandomBytes(12);
|
|
563
|
+
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveKey"]);
|
|
564
|
+
const key = await crypto.subtle.deriveKey({
|
|
565
|
+
hash: "SHA-256",
|
|
566
|
+
iterations: validatedIterations,
|
|
567
|
+
name: "PBKDF2",
|
|
568
|
+
salt: toArrayBuffer(salt)
|
|
569
|
+
}, material, {
|
|
570
|
+
length: 256,
|
|
571
|
+
name: "AES-GCM"
|
|
572
|
+
}, false, ["encrypt"]);
|
|
443
573
|
const ciphertext = await crypto.subtle.encrypt({
|
|
444
574
|
additionalData: toArrayBuffer(encodeUtf8(encryptedPayloadPrefix)),
|
|
445
575
|
iv: toArrayBuffer(iv),
|
|
@@ -448,22 +578,22 @@ async function encryptTextWithPassword(plaintext, password, options = {}) {
|
|
|
448
578
|
}, key, plaintextBytes);
|
|
449
579
|
return [
|
|
450
580
|
encryptedPayloadPrefix,
|
|
451
|
-
String(
|
|
581
|
+
String(validatedIterations),
|
|
452
582
|
encodeBase64UrlBytes(salt),
|
|
453
583
|
encodeBase64UrlBytes(iv),
|
|
454
584
|
encodeBase64UrlBytes(new Uint8Array(ciphertext))
|
|
455
585
|
].join(":");
|
|
456
586
|
}
|
|
457
587
|
/**
|
|
458
|
-
* 解密 {@link
|
|
588
|
+
* 解密 {@link AESEncryptWithPassword} 生成的 v1 认证载荷。
|
|
459
589
|
*
|
|
460
|
-
* @param payload - 未修改的
|
|
590
|
+
* @param payload - 未修改的 v1 载荷,最大约 16 MiB 文本。
|
|
461
591
|
* @param password - 加密时使用的口令。
|
|
462
592
|
* @returns 原始 UTF-8 文本。
|
|
463
593
|
* @throws 格式或字段非法时抛出 `TypeError`,载荷过大时抛出 `RangeError`,认证或密码
|
|
464
594
|
* 失败及缺少平台能力时抛出 `Error`。
|
|
465
595
|
*/
|
|
466
|
-
async function
|
|
596
|
+
async function AESDecryptWithPassword(payload, password) {
|
|
467
597
|
const passwordBytes = encodeValidatedPassword(password);
|
|
468
598
|
if (payload.length > maximumPayloadLength) throw new RangeError("The encrypted payload exceeds the supported size.");
|
|
469
599
|
const parts = payload.split(":");
|
|
@@ -484,7 +614,16 @@ async function decryptTextWithPassword(payload, password) {
|
|
|
484
614
|
const crypto = requireWebCrypto();
|
|
485
615
|
const textDecoder = getTextDecoder();
|
|
486
616
|
try {
|
|
487
|
-
const
|
|
617
|
+
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveKey"]);
|
|
618
|
+
const key = await crypto.subtle.deriveKey({
|
|
619
|
+
hash: "SHA-256",
|
|
620
|
+
iterations,
|
|
621
|
+
name: "PBKDF2",
|
|
622
|
+
salt: toArrayBuffer(salt)
|
|
623
|
+
}, material, {
|
|
624
|
+
length: 256,
|
|
625
|
+
name: "AES-GCM"
|
|
626
|
+
}, false, ["decrypt"]);
|
|
488
627
|
const plaintext = await crypto.subtle.decrypt({
|
|
489
628
|
additionalData: toArrayBuffer(encodeUtf8(encryptedPayloadPrefix)),
|
|
490
629
|
iv: toArrayBuffer(iv),
|
|
@@ -497,45 +636,208 @@ async function decryptTextWithPassword(payload, password) {
|
|
|
497
636
|
}
|
|
498
637
|
}
|
|
499
638
|
/**
|
|
500
|
-
*
|
|
639
|
+
* 生成可供 RSA-OAEP/SHA-256 与 RSA-PSS/SHA-256 共用的 PEM 密钥对。
|
|
501
640
|
*
|
|
502
|
-
* @param
|
|
503
|
-
* @returns
|
|
504
|
-
* @throws `RangeError
|
|
641
|
+
* @param modulusLength - RSA 模数位数,默认 2,048;必须是不小于 2,048 的 256 倍数。
|
|
642
|
+
* @returns 未加密 PKCS#8 私钥和 SubjectPublicKeyInfo 公钥组成的 PEM 密钥对。
|
|
643
|
+
* @throws 模数小于 2,048 或不是 256 的倍数时抛出 `RangeError`。
|
|
505
644
|
*/
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (plaintext.length === 0) return null;
|
|
517
|
-
try {
|
|
518
|
-
return JSON.parse(plaintext);
|
|
519
|
-
} catch {
|
|
520
|
-
return plaintext;
|
|
521
|
-
}
|
|
522
|
-
} catch {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
645
|
+
async function GenerateRSAKeyPair(modulusLength = 2048) {
|
|
646
|
+
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) throw new RangeError("modulusLength must be a safe integer of at least 2048 and divisible by 256.");
|
|
647
|
+
const crypto = requireWebCrypto();
|
|
648
|
+
const keyPair = assertKeyPair(await crypto.subtle.generateKey({
|
|
649
|
+
hash: "SHA-256",
|
|
650
|
+
modulusLength,
|
|
651
|
+
name: "RSA-OAEP",
|
|
652
|
+
publicExponent: Uint8Array.of(1, 0, 1)
|
|
653
|
+
}, true, ["decrypt", "encrypt"]));
|
|
654
|
+
return exportKeyPair(keyPair);
|
|
525
655
|
}
|
|
526
|
-
/**
|
|
527
|
-
|
|
528
|
-
|
|
656
|
+
/**
|
|
657
|
+
* 使用 RSA-OAEP/SHA-256 公钥加密 UTF-8 文本。
|
|
658
|
+
*
|
|
659
|
+
* @param plaintext - 要加密的 UTF-8 文本;长度必须满足 RSA-OAEP 模数限制。
|
|
660
|
+
* @param publicKeyPem - SubjectPublicKeyInfo PEM 公钥。
|
|
661
|
+
* @returns Base64 编码的 RSA 密文。
|
|
662
|
+
* @throws 公钥格式无效或明文超过 RSA-OAEP 容量时抛出错误。
|
|
663
|
+
*/
|
|
664
|
+
async function RSAEncryptOAEP(plaintext, publicKeyPem) {
|
|
665
|
+
const crypto = requireWebCrypto();
|
|
666
|
+
const key = await crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), {
|
|
667
|
+
hash: "SHA-256",
|
|
668
|
+
name: "RSA-OAEP"
|
|
669
|
+
}, false, ["encrypt"]);
|
|
670
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, key, toArrayBuffer(encodeUtf8(plaintext)));
|
|
671
|
+
return encodeBase64Bytes(new Uint8Array(ciphertext));
|
|
529
672
|
}
|
|
530
|
-
/**
|
|
531
|
-
|
|
532
|
-
|
|
673
|
+
/**
|
|
674
|
+
* 使用 RSA-OAEP/SHA-256 私钥解密 Base64 密文。
|
|
675
|
+
*
|
|
676
|
+
* @param ciphertext - Base64 编码的 RSA 密文。
|
|
677
|
+
* @param privateKeyPem - 未加密的 PKCS#8 PEM 私钥。
|
|
678
|
+
* @returns 解密后的 UTF-8 文本。
|
|
679
|
+
* @throws 私钥、Base64 或密文无效时抛出错误。
|
|
680
|
+
*/
|
|
681
|
+
async function RSADecryptOAEP(ciphertext, privateKeyPem) {
|
|
682
|
+
const crypto = requireWebCrypto();
|
|
683
|
+
const key = await crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), {
|
|
684
|
+
hash: "SHA-256",
|
|
685
|
+
name: "RSA-OAEP"
|
|
686
|
+
}, false, ["decrypt"]);
|
|
687
|
+
const plaintext = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, key, toArrayBuffer(decodeBase64Bytes(ciphertext)));
|
|
688
|
+
return getTextDecoder().decode(plaintext);
|
|
533
689
|
}
|
|
534
|
-
/**
|
|
535
|
-
|
|
536
|
-
|
|
690
|
+
/**
|
|
691
|
+
* 使用 RSA-PSS/SHA-256 私钥签名文本或字节。
|
|
692
|
+
*
|
|
693
|
+
* @param value - 要签名的 UTF-8 文本或原始字节。
|
|
694
|
+
* @param privateKeyPem - 未加密的 PKCS#8 PEM 私钥。
|
|
695
|
+
* @returns Base64 编码的 RSA-PSS 签名;盐长度固定为 32 字节。
|
|
696
|
+
* @throws 私钥格式无效或签名失败时抛出错误。
|
|
697
|
+
*/
|
|
698
|
+
async function RSASignPSS(value, privateKeyPem) {
|
|
699
|
+
const crypto = requireWebCrypto();
|
|
700
|
+
const key = await crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), {
|
|
701
|
+
hash: "SHA-256",
|
|
702
|
+
name: "RSA-PSS"
|
|
703
|
+
}, false, ["sign"]);
|
|
704
|
+
const signature = await crypto.subtle.sign({
|
|
705
|
+
name: "RSA-PSS",
|
|
706
|
+
saltLength: 32
|
|
707
|
+
}, key, toArrayBuffer(encodeUtf8(value)));
|
|
708
|
+
return encodeBase64Bytes(new Uint8Array(signature));
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* 使用 RSA-PSS/SHA-256 公钥验证 Base64 签名。
|
|
712
|
+
*
|
|
713
|
+
* @param value - 签名时使用的 UTF-8 文本或原始字节。
|
|
714
|
+
* @param signature - Base64 编码的 RSA-PSS 签名。
|
|
715
|
+
* @param publicKeyPem - SubjectPublicKeyInfo PEM 公钥。
|
|
716
|
+
* @returns 签名与内容、公钥匹配时返回 `true`。
|
|
717
|
+
* @throws 公钥或 Base64 格式无效时抛出错误。
|
|
718
|
+
*/
|
|
719
|
+
async function RSAVerifyPSS(value, signature, publicKeyPem) {
|
|
720
|
+
const crypto = requireWebCrypto();
|
|
721
|
+
const key = await crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), {
|
|
722
|
+
hash: "SHA-256",
|
|
723
|
+
name: "RSA-PSS"
|
|
724
|
+
}, false, ["verify"]);
|
|
725
|
+
return crypto.subtle.verify({
|
|
726
|
+
name: "RSA-PSS",
|
|
727
|
+
saltLength: 32
|
|
728
|
+
}, key, toArrayBuffer(decodeBase64Bytes(signature)), toArrayBuffer(encodeUtf8(value)));
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 生成 ECDSA PEM 签名密钥对。
|
|
732
|
+
*
|
|
733
|
+
* @param namedCurve - NIST 曲线:P-256、P-384 或 P-521。
|
|
734
|
+
* @returns 未加密 PKCS#8 私钥和 SubjectPublicKeyInfo 公钥组成的 PEM 密钥对。
|
|
735
|
+
*/
|
|
736
|
+
async function GenerateECDSAKeyPair(namedCurve = "P-256") {
|
|
737
|
+
const crypto = requireWebCrypto();
|
|
738
|
+
const keyPair = assertKeyPair(await crypto.subtle.generateKey({
|
|
739
|
+
name: "ECDSA",
|
|
740
|
+
namedCurve
|
|
741
|
+
}, true, ["sign", "verify"]));
|
|
742
|
+
return exportKeyPair(keyPair);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* 使用 ECDSA 私钥签名文本或字节。
|
|
746
|
+
*
|
|
747
|
+
* @remarks Web Crypto 返回 IEEE P1363 固定字段拼接格式,与 .NET 实现一致。
|
|
748
|
+
* @param value - 要签名的 UTF-8 文本或原始字节。
|
|
749
|
+
* @param privateKeyPem - 未加密的 EC PKCS#8 PEM 私钥。
|
|
750
|
+
* @param namedCurve - 私钥使用的 NIST 曲线。
|
|
751
|
+
* @returns Base64 编码的 IEEE P1363 ECDSA 签名。
|
|
752
|
+
*/
|
|
753
|
+
async function ECDSASign(value, privateKeyPem, namedCurve = "P-256") {
|
|
754
|
+
const crypto = requireWebCrypto();
|
|
755
|
+
const key = await crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), {
|
|
756
|
+
name: "ECDSA",
|
|
757
|
+
namedCurve
|
|
758
|
+
}, false, ["sign"]);
|
|
759
|
+
const hash = namedCurve === "P-256" ? "SHA-256" : namedCurve === "P-384" ? "SHA-384" : "SHA-512";
|
|
760
|
+
const signature = await crypto.subtle.sign({
|
|
761
|
+
hash,
|
|
762
|
+
name: "ECDSA"
|
|
763
|
+
}, key, toArrayBuffer(encodeUtf8(value)));
|
|
764
|
+
return encodeBase64Bytes(new Uint8Array(signature));
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* 使用 ECDSA 公钥验证 Base64 签名。
|
|
768
|
+
*
|
|
769
|
+
* @param value - 签名时使用的 UTF-8 文本或原始字节。
|
|
770
|
+
* @param signature - Base64 编码的 IEEE P1363 ECDSA 签名。
|
|
771
|
+
* @param publicKeyPem - EC SubjectPublicKeyInfo PEM 公钥。
|
|
772
|
+
* @param namedCurve - 公钥使用的 NIST 曲线。
|
|
773
|
+
* @returns 签名与内容、公钥和曲线匹配时返回 `true`。
|
|
774
|
+
*/
|
|
775
|
+
async function ECDSAVerify(value, signature, publicKeyPem, namedCurve = "P-256") {
|
|
776
|
+
const crypto = requireWebCrypto();
|
|
777
|
+
const key = await crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), {
|
|
778
|
+
name: "ECDSA",
|
|
779
|
+
namedCurve
|
|
780
|
+
}, false, ["verify"]);
|
|
781
|
+
const hash = namedCurve === "P-256" ? "SHA-256" : namedCurve === "P-384" ? "SHA-384" : "SHA-512";
|
|
782
|
+
return crypto.subtle.verify({
|
|
783
|
+
hash,
|
|
784
|
+
name: "ECDSA"
|
|
785
|
+
}, key, toArrayBuffer(decodeBase64Bytes(signature)), toArrayBuffer(encodeUtf8(value)));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 生成 ECDH PEM 密钥协商密钥对。
|
|
789
|
+
*
|
|
790
|
+
* @param namedCurve - NIST 曲线:P-256、P-384 或 P-521。
|
|
791
|
+
* @returns 未加密 PKCS#8 私钥和 SubjectPublicKeyInfo 公钥组成的 PEM 密钥对。
|
|
792
|
+
*/
|
|
793
|
+
async function GenerateECDHKeyPair(namedCurve = "P-256") {
|
|
794
|
+
const crypto = requireWebCrypto();
|
|
795
|
+
const keyPair = assertKeyPair(await crypto.subtle.generateKey({
|
|
796
|
+
name: "ECDH",
|
|
797
|
+
namedCurve
|
|
798
|
+
}, true, ["deriveBits"]));
|
|
799
|
+
return exportKeyPair(keyPair);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* 使用本方 ECDH 私钥与对方 ECDH 公钥派生共享秘密。
|
|
803
|
+
*
|
|
804
|
+
* @remarks 返回值仍需经过合适的 KDF 后才能作为对称密钥,不应直接长期存储。
|
|
805
|
+
* @param privateKeyPem - 本方未加密的 EC PKCS#8 PEM 私钥。
|
|
806
|
+
* @param publicKeyPem - 对方的 EC SubjectPublicKeyInfo PEM 公钥。
|
|
807
|
+
* @param namedCurve - 双方密钥使用的 NIST 曲线。
|
|
808
|
+
* @returns 曲线字段长度的原始 ECDH 共享秘密。
|
|
809
|
+
*/
|
|
810
|
+
async function DeriveECDHSecret(privateKeyPem, publicKeyPem, namedCurve = "P-256") {
|
|
811
|
+
const crypto = requireWebCrypto();
|
|
812
|
+
const [privateKey, publicKey] = await Promise.all([crypto.subtle.importKey("pkcs8", fromPem(privateKeyPem, pemLabels.private), {
|
|
813
|
+
name: "ECDH",
|
|
814
|
+
namedCurve
|
|
815
|
+
}, false, ["deriveBits"]), crypto.subtle.importKey("spki", fromPem(publicKeyPem, pemLabels.public), {
|
|
816
|
+
name: "ECDH",
|
|
817
|
+
namedCurve
|
|
818
|
+
}, false, [])]);
|
|
819
|
+
const bitLength = namedCurve === "P-256" ? 256 : namedCurve === "P-384" ? 384 : 528;
|
|
820
|
+
const secret = await crypto.subtle.deriveBits({
|
|
821
|
+
name: "ECDH",
|
|
822
|
+
public: publicKey
|
|
823
|
+
}, privateKey, bitLength);
|
|
824
|
+
return new Uint8Array(secret);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* 使用 ECDH 后以 SHA-256 派生共享密钥。
|
|
828
|
+
*
|
|
829
|
+
* @remarks 相比直接使用原始共享秘密,此入口与 .NET `DeriveECDHKeySHA256` 一致并固定输出 32 字节。
|
|
830
|
+
* @param privateKeyPem - 本方未加密的 EC PKCS#8 PEM 私钥。
|
|
831
|
+
* @param publicKeyPem - 对方的 EC SubjectPublicKeyInfo PEM 公钥。
|
|
832
|
+
* @param namedCurve - 双方密钥使用的 NIST 曲线。
|
|
833
|
+
* @returns 32 字节共享密钥。
|
|
834
|
+
*/
|
|
835
|
+
async function DeriveECDHKeySHA256(privateKeyPem, publicKeyPem, namedCurve = "P-256") {
|
|
836
|
+
const secret = await DeriveECDHSecret(privateKeyPem, publicKeyPem, namedCurve);
|
|
837
|
+
const digest = await requireWebCrypto().subtle.digest("SHA-256", toArrayBuffer(secret));
|
|
838
|
+
return new Uint8Array(digest);
|
|
537
839
|
}
|
|
538
840
|
//#endregion
|
|
539
|
-
export {
|
|
841
|
+
export { AESDecrypt, AESDecryptAuthenticated, AESDecryptWithPassword, AESEncrypt, AESEncryptAuthenticated, AESEncryptWithPassword, DeriveECDHKeySHA256, DeriveECDHSecret, ECDSASign, ECDSAVerify, FixedTimeEquals, GenerateECDHKeyPair, GenerateECDSAKeyPair, GenerateRSAKeyPair, GenerateRandomBytes, HKDFSHA256, HMACSHA256Encrypt, HMACSHA384Encrypt, HMACSHA512Encrypt, HashPasswordPBKDF2SHA256, MD5Encrypt, PBKDF2SHA256, RSADecryptOAEP, RSAEncryptOAEP, RSASignPSS, RSAVerifyPSS, SHA1Encrypt, SHA256Bytes, SHA256Encrypt, SHA384Bytes, SHA384Encrypt, SHA512Bytes, SHA512Encrypt, VerifyPasswordPBKDF2SHA256 };
|
|
540
842
|
|
|
541
843
|
//# sourceMappingURL=index.mjs.map
|