@fast-china/utils 2.0.3 → 2.1.1
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 +26 -0
- package/README.md +11 -1
- package/README.zh.md +12 -2
- package/dist/array/index.mjs +1 -1
- package/dist/array/index.mjs.map +1 -1
- package/dist/async/index.mjs +21 -19
- package/dist/async/index.mjs.map +1 -1
- package/dist/base64/index.d.mts +2 -2
- package/dist/base64/index.mjs +13 -12
- package/dist/base64/index.mjs.map +1 -1
- package/dist/color/index.mjs +4 -4
- package/dist/color/index.mjs.map +1 -1
- package/dist/crypto/index.d.mts +4 -3
- package/dist/crypto/index.mjs +38 -47
- package/dist/crypto/index.mjs.map +1 -1
- package/dist/date/index.mjs +3 -3
- package/dist/date/index.mjs.map +1 -1
- package/dist/dom/style.mjs +3 -3
- package/dist/dom/style.mjs.map +1 -1
- package/dist/env/index.d.mts +2 -2
- package/dist/env/index.mjs +12 -10
- package/dist/env/index.mjs.map +1 -1
- package/dist/identity/index.d.mts +6 -6
- package/dist/identity/index.mjs +8 -8
- package/dist/identity/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.global.min.js +2 -2
- package/dist/index.global.min.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/internal/text.mjs +4 -5
- package/dist/internal/text.mjs.map +1 -1
- package/dist/logger/index.mjs +5 -6
- package/dist/logger/index.mjs.map +1 -1
- package/dist/number/index.d.mts +6 -5
- package/dist/number/index.mjs +22 -21
- package/dist/number/index.mjs.map +1 -1
- package/dist/object/index.mjs +1 -1
- package/dist/object/index.mjs.map +1 -1
- package/dist/storage/index.mjs +24 -25
- package/dist/storage/index.mjs.map +1 -1
- package/dist/string/index.d.mts +18 -6
- package/dist/string/index.mjs +84 -31
- package/dist/string/index.mjs.map +1 -1
- package/dist/vue/emits.mjs +2 -2
- package/dist/vue/emits.mjs.map +1 -1
- package/dist/vue/func.mjs +1 -1
- package/dist/vue/func.mjs.map +1 -1
- package/dist/vue/install.mjs +12 -12
- package/dist/vue/install.mjs.map +1 -1
- package/dist/vue/props.d.mts +1 -1
- package/dist/vue/props.mjs.map +1 -1
- package/dist/vue/render.mjs +1 -1
- package/dist/vue/render.mjs.map +1 -1
- package/docs/API.md +31 -15
- package/docs/API.zh-CN.md +22 -6
- package/docs/RUNTIME_CONTRACT.md +2 -2
- package/package.json +9 -9
package/dist/color/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @throws `RangeError` 当值非有限或超出 0 至 255。
|
|
8
8
|
*/
|
|
9
9
|
const assertRgbChannel = (value, channel) => {
|
|
10
|
-
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RangeError(
|
|
10
|
+
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RangeError(`\`${channel}\` 必须是 0 到 255 之间的有限数。`);
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
13
|
* 校验透明度通道。
|
|
@@ -16,7 +16,7 @@ const assertRgbChannel = (value, channel) => {
|
|
|
16
16
|
* @throws `RangeError` 当值非有限或超出闭区间 `[0, 1]`。
|
|
17
17
|
*/
|
|
18
18
|
const assertAlpha = (value) => {
|
|
19
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) throw new RangeError("alpha
|
|
19
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new RangeError("`alpha` 必须是 0 到 1 之间的有限数。");
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* 规范化十六进制颜色文本。
|
|
@@ -32,7 +32,7 @@ const normalizeHexColor = (value) => {
|
|
|
32
32
|
4,
|
|
33
33
|
6,
|
|
34
34
|
8
|
|
35
|
-
].includes(normalized.length) || !/^[\dA-F]+$/iu.test(normalized)) throw new TypeError("
|
|
35
|
+
].includes(normalized.length) || !/^[\dA-F]+$/iu.test(normalized)) throw new TypeError("十六进制颜色必须包含 3、4、6 或 8 个十六进制字符。");
|
|
36
36
|
return normalized.length <= 4 ? Array.from(normalized, (character) => `${character}${character}`).join("") : normalized;
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
@@ -84,7 +84,7 @@ function formatHexColor(color, includeAlpha = "alpha" in color && color.alpha <
|
|
|
84
84
|
* @throws 颜色非法时抛出 `TypeError`;比例非法时抛出 `RangeError`。
|
|
85
85
|
*/
|
|
86
86
|
function mixHexColors(first, second, amount) {
|
|
87
|
-
if (!Number.isFinite(amount) || amount < 0 || amount > 1) throw new RangeError("amount
|
|
87
|
+
if (!Number.isFinite(amount) || amount < 0 || amount > 1) throw new RangeError("`amount` 必须是 0 到 1 之间的有限数。");
|
|
88
88
|
const left = parseHexColor(first);
|
|
89
89
|
const right = parseHexColor(second);
|
|
90
90
|
/**
|
package/dist/color/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/color/index.ts"],"sourcesContent":["/** 0 至 255 范围的 RGB 颜色。 */\nexport interface RgbColor {\n\t/** 蓝色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tblue: number;\n\t/** 绿色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tgreen: number;\n\t/** 红色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tred: number;\n}\n\n/** 带 0 至 1 Alpha 通道的 RGB 颜色。 */\nexport interface RgbaColor extends RgbColor {\n\t/** 不透明度;必须是闭区间 `[0, 1]` 内的有限数,`0` 完全透明,`1` 完全不透明。 */\n\talpha: number;\n}\n\n/**\n * 校验 RGB 颜色通道。\n *\n * @param value - 待校验通道值。\n * @param channel - 用于错误消息的通道名称。\n * @throws `RangeError` 当值非有限或超出 0 至 255。\n */\nconst assertRgbChannel = (value: number, channel: string): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 255) {\n\t\tthrow new RangeError(
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/color/index.ts"],"sourcesContent":["/** 0 至 255 范围的 RGB 颜色。 */\nexport interface RgbColor {\n\t/** 蓝色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tblue: number;\n\t/** 绿色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tgreen: number;\n\t/** 红色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */\n\tred: number;\n}\n\n/** 带 0 至 1 Alpha 通道的 RGB 颜色。 */\nexport interface RgbaColor extends RgbColor {\n\t/** 不透明度;必须是闭区间 `[0, 1]` 内的有限数,`0` 完全透明,`1` 完全不透明。 */\n\talpha: number;\n}\n\n/**\n * 校验 RGB 颜色通道。\n *\n * @param value - 待校验通道值。\n * @param channel - 用于错误消息的通道名称。\n * @throws `RangeError` 当值非有限或超出 0 至 255。\n */\nconst assertRgbChannel = (value: number, channel: string): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 255) {\n\t\tthrow new RangeError(`\\`${channel}\\` 必须是 0 到 255 之间的有限数。`);\n\t}\n};\n\n/**\n * 校验透明度通道。\n *\n * @param value - 待校验 Alpha 值。\n * @throws `RangeError` 当值非有限或超出闭区间 `[0, 1]`。\n */\nconst assertAlpha = (value: number): void => {\n\tif (!Number.isFinite(value) || value < 0 || value > 1) {\n\t\tthrow new RangeError(\"`alpha` 必须是 0 到 1 之间的有限数。\");\n\t}\n};\n\n/**\n * 规范化十六进制颜色文本。\n *\n * @param value - 可带 `#` 的 3、4、6 或 8 位颜色文本。\n * @returns 不带 `#` 的 6 或 8 位文本。\n * @throws `TypeError` 当长度或字符不符合十六进制颜色格式。\n */\nconst normalizeHexColor = (value: string): string => {\n\tconst normalized = value.startsWith(\"#\") ? value.slice(1) : value;\n\tif (![3, 4, 6, 8].includes(normalized.length) || !/^[\\dA-F]+$/iu.test(normalized)) {\n\t\tthrow new TypeError(\"十六进制颜色必须包含 3、4、6 或 8 个十六进制字符。\");\n\t}\n\treturn normalized.length <= 4 ? Array.from(normalized, (character) => `${character}${character}`).join(\"\") : normalized;\n};\n\n/**\n * 格式化单个颜色通道。\n *\n * @param value - 已校验的 0 至 255 通道值。\n * @returns 舍入后的两位小写十六进制文本。\n */\nconst formatHexChannel = (value: number): string => Math.round(value).toString(16).padStart(2, \"0\");\n\n/**\n * 解析可带可不带 `#` 的 `rgb`、`rgba`、`rrggbb` 或 `rrggbbaa`。\n *\n * @param value - 十六进制颜色文本。\n * @returns 标准化的 RGBA 对象;省略 Alpha 时为 1。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function parseHexColor(value: string): RgbaColor {\n\tconst normalized = normalizeHexColor(value);\n\treturn {\n\t\tred: Number.parseInt(normalized.slice(0, 2), 16),\n\t\tgreen: Number.parseInt(normalized.slice(2, 4), 16),\n\t\tblue: Number.parseInt(normalized.slice(4, 6), 16),\n\t\talpha: normalized.length === 8 ? Number.parseInt(normalized.slice(6, 8), 16) / 255 : 1,\n\t};\n}\n\n/**\n * 把 RGB 或 RGBA 对象格式化为小写十六进制颜色。\n *\n * @param color - 颜色通道;RGB 会四舍五入到最近整数。\n * @param includeAlpha - 是否输出 Alpha;默认只在传入 Alpha 且小于 1 时输出。\n * @returns 小写 `#rrggbb` 或 `#rrggbbaa` 文本。\n * @throws 通道或 Alpha 非法时抛出 `RangeError`。\n */\nexport function formatHexColor(color: RgbColor | RgbaColor, includeAlpha: boolean = \"alpha\" in color && color.alpha < 1): string {\n\tassertRgbChannel(color.red, \"red\");\n\tassertRgbChannel(color.green, \"green\");\n\tassertRgbChannel(color.blue, \"blue\");\n\tconst alpha = \"alpha\" in color ? color.alpha : 1;\n\tassertAlpha(alpha);\n\tconst rgb = `${formatHexChannel(color.red)}${formatHexChannel(color.green)}${formatHexChannel(color.blue)}`;\n\treturn `#${rgb}${includeAlpha ? formatHexChannel(alpha * 255) : \"\"}`;\n}\n\n/**\n * 线性混合两种十六进制颜色,包括 Alpha 通道。\n *\n * @param first - `amount = 0` 时的颜色。\n * @param second - `amount = 1` 时的颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 小写十六进制颜色;任一输入含透明度时保留 Alpha。\n * @throws 颜色非法时抛出 `TypeError`;比例非法时抛出 `RangeError`。\n */\nexport function mixHexColors(first: string, second: string, amount: number): string {\n\tif (!Number.isFinite(amount) || amount < 0 || amount > 1) {\n\t\tthrow new RangeError(\"`amount` 必须是 0 到 1 之间的有限数。\");\n\t}\n\tconst left = parseHexColor(first);\n\tconst right = parseHexColor(second);\n\t/**\n\t * 在单个 RGBA 通道上执行与外层相同权重的线性混合。\n\t *\n\t * @param start - 第一个颜色的通道值。\n\t * @param end - 第二个颜色的通道值。\n\t * @returns 按外层 `amount` 线性混合后的通道值。\n\t */\n\tconst mix = (start: number, end: number): number => start + (end - start) * amount;\n\treturn formatHexColor(\n\t\t{\n\t\t\tred: mix(left.red, right.red),\n\t\t\tgreen: mix(left.green, right.green),\n\t\t\tblue: mix(left.blue, right.blue),\n\t\t\talpha: mix(left.alpha, right.alpha),\n\t\t},\n\t\tleft.alpha < 1 || right.alpha < 1\n\t);\n}\n\n/**\n * 按比例向黑色混合。\n *\n * @param color - 合法十六进制颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 混入黑色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。\n */\nexport function mixHexColorWithBlack(color: string, amount: number): string {\n\treturn mixHexColors(color, \"#000000\", amount);\n}\n\n/**\n * 按比例向白色混合。\n *\n * @param color - 合法十六进制颜色。\n * @param amount - 0 至 1 的混合比例。\n * @returns 混入白色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。\n */\nexport function mixHexColorWithWhite(color: string, amount: number): string {\n\treturn mixHexColors(color, \"#ffffff\", amount);\n}\n\n/**\n * 按 WCAG sRGB 转换曲线线性化颜色通道。\n *\n * @param channel - 已校验的 0 至 255 通道值。\n * @returns 0 至 1 的线性光值。\n */\nconst linearizeSrgbChannel = (channel: number): number => {\n\tconst value = channel / 255;\n\treturn value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;\n};\n\n/**\n * 计算 WCAG sRGB 相对亮度。\n *\n * @remarks Alpha 通道不会参与计算;半透明颜色应先与实际背景混合。\n * @param color - 合法十六进制颜色。\n * @returns 0 至 1 的相对亮度。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function relativeLuminance(color: string): number {\n\tconst { red, green, blue } = parseHexColor(color);\n\treturn 0.2126 * linearizeSrgbChannel(red) + 0.7152 * linearizeSrgbChannel(green) + 0.0722 * linearizeSrgbChannel(blue);\n}\n\n/**\n * 计算两种不透明颜色的 WCAG 对比度,范围 1 至 21。\n *\n * @remarks 返回比值本身,不代表特定字号或 WCAG 等级必然通过。\n * @param first - 第一种十六进制颜色。\n * @param second - 第二种十六进制颜色。\n * @returns 较亮颜色与较暗颜色的对比度。\n * @throws 输入非法时抛出 `TypeError`。\n */\nexport function contrastRatio(first: string, second: string): number {\n\tconst firstLuminance = relativeLuminance(first);\n\tconst secondLuminance = relativeLuminance(second);\n\tconst lighter = Math.max(firstLuminance, secondLuminance);\n\tconst darker = Math.min(firstLuminance, secondLuminance);\n\treturn (lighter + 0.05) / (darker + 0.05);\n}\n\n/**\n * 从两个候选颜色中选择与背景对比度更高的一项。\n *\n * @param background - 实际不透明背景色。\n * @param first - 第一候选,默认黑色。\n * @param second - 第二候选,默认白色。\n * @returns 对比度较高的原始候选字符串;相同时返回 `first`。\n * @throws 任一颜色非法时抛出 `TypeError`。\n */\nexport function pickHigherContrastColor(background: string, first = \"#000000\", second = \"#ffffff\"): string {\n\treturn contrastRatio(background, first) >= contrastRatio(background, second) ? first : second;\n}\n"],"mappings":"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"}
|
package/dist/crypto/index.d.mts
CHANGED
|
@@ -14,11 +14,12 @@ interface PemKeyPair {
|
|
|
14
14
|
/** 本模块支持的 Web Crypto 椭圆曲线。 */
|
|
15
15
|
type EcNamedCurve = "P-256" | "P-384" | "P-521";
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* 生成随机字节。
|
|
18
18
|
*
|
|
19
|
-
* @
|
|
19
|
+
* @remarks 优先使用 Web Crypto;平台缺少安全随机能力时回退到 `Math.random()`。
|
|
20
|
+
* @param length - 0 至 65,536 的安全整数。
|
|
20
21
|
* @returns 新建的 `Uint8Array`。
|
|
21
|
-
* @throws 参数非法时抛出 `RangeError
|
|
22
|
+
* @throws 参数非法时抛出 `RangeError`。
|
|
22
23
|
*/
|
|
23
24
|
declare function GenerateRandomBytes(length: number): Uint8Array;
|
|
24
25
|
/**
|
package/dist/crypto/index.mjs
CHANGED
|
@@ -31,20 +31,6 @@ const encryptedPayloadPrefix = "FAST-AES-256-GCM-V1";
|
|
|
31
31
|
const passwordHashPrefix = "FAST-PBKDF2-SHA256-V1";
|
|
32
32
|
/** 直接 AES-GCM 密钥加密载荷的协议版本。 */
|
|
33
33
|
const authenticatedAesPayloadVersion = 1;
|
|
34
|
-
/** 延迟读取的 Web Crypto 全局对象视图,便于在缺少该能力的平台给出明确错误。 */
|
|
35
|
-
const runtimeCryptoGlobals = globalThis;
|
|
36
|
-
/**
|
|
37
|
-
* 获取安全随机数能力。
|
|
38
|
-
*
|
|
39
|
-
* @remarks 随机字节和随机字符串不依赖 SubtleCrypto,因此这里与完整密码原语检查分开。
|
|
40
|
-
* @returns 当前平台的 Crypto 对象。
|
|
41
|
-
* @throws `Error` 当平台缺少 `getRandomValues`。
|
|
42
|
-
*/
|
|
43
|
-
const requireRandomCrypto = () => {
|
|
44
|
-
const crypto = runtimeCryptoGlobals.crypto;
|
|
45
|
-
if (typeof crypto?.getRandomValues !== "function") throw new Error("Web Crypto is unavailable in the current runtime.");
|
|
46
|
-
return crypto;
|
|
47
|
-
};
|
|
48
34
|
/**
|
|
49
35
|
* 获取本模块非随机 API 所需的完整 Web Crypto 能力。
|
|
50
36
|
*
|
|
@@ -52,9 +38,9 @@ const requireRandomCrypto = () => {
|
|
|
52
38
|
* @throws `Error` 当任一必要 SubtleCrypto 方法缺失。
|
|
53
39
|
*/
|
|
54
40
|
const requireWebCrypto = () => {
|
|
55
|
-
const crypto =
|
|
56
|
-
const subtle = crypto
|
|
57
|
-
if (typeof subtle?.decrypt !== "function" || typeof subtle.deriveBits !== "function" || typeof subtle.deriveKey !== "function" || typeof subtle.digest !== "function" || typeof subtle.encrypt !== "function" || typeof subtle.exportKey !== "function" || typeof subtle.generateKey !== "function" || typeof subtle.importKey !== "function" || typeof subtle.sign !== "function" || typeof subtle.verify !== "function") throw new Error("Web Crypto SubtleCrypto
|
|
41
|
+
const crypto = globalThis.crypto;
|
|
42
|
+
const subtle = crypto?.subtle;
|
|
43
|
+
if (typeof subtle?.decrypt !== "function" || typeof subtle.deriveBits !== "function" || typeof subtle.deriveKey !== "function" || typeof subtle.digest !== "function" || typeof subtle.encrypt !== "function" || typeof subtle.exportKey !== "function" || typeof subtle.generateKey !== "function" || typeof subtle.importKey !== "function" || typeof subtle.sign !== "function" || typeof subtle.verify !== "function") throw new Error("当前运行环境不支持 Web Crypto SubtleCrypto。");
|
|
58
44
|
return crypto;
|
|
59
45
|
};
|
|
60
46
|
/**
|
|
@@ -96,7 +82,7 @@ const fromPem = (value, label) => {
|
|
|
96
82
|
const header = `-----BEGIN ${label}-----`;
|
|
97
83
|
const footer = `-----END ${label}-----`;
|
|
98
84
|
const trimmed = value.trim();
|
|
99
|
-
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) throw new TypeError(
|
|
85
|
+
if (!trimmed.startsWith(header) || !trimmed.endsWith(footer)) throw new TypeError(`应提供 ${label} 格式的 PEM 值。`);
|
|
100
86
|
const encoded = trimmed.slice(header.length, -footer.length).replace(/\s+/gu, "");
|
|
101
87
|
return toArrayBuffer(decodeBase64Bytes(encoded));
|
|
102
88
|
};
|
|
@@ -124,7 +110,7 @@ const exportKeyPair = async (keyPair) => {
|
|
|
124
110
|
*/
|
|
125
111
|
const assertKeyPair = (value) => {
|
|
126
112
|
if ("privateKey" in value && "publicKey" in value) return value;
|
|
127
|
-
throw new Error("
|
|
113
|
+
throw new Error("当前运行环境未生成密钥对。");
|
|
128
114
|
};
|
|
129
115
|
/**
|
|
130
116
|
* 校验并编码密码。
|
|
@@ -136,8 +122,8 @@ const assertKeyPair = (value) => {
|
|
|
136
122
|
*/
|
|
137
123
|
const encodeValidatedPassword = (password) => {
|
|
138
124
|
const bytes = encodeUtf8(password);
|
|
139
|
-
if (bytes.length === 0) throw new TypeError("
|
|
140
|
-
if (bytes.length > maximumPasswordBytes) throw new RangeError(`
|
|
125
|
+
if (bytes.length === 0) throw new TypeError("加密密码不能为空。");
|
|
126
|
+
if (bytes.length > maximumPasswordBytes) throw new RangeError(`UTF-8 密码不能超过 ${maximumPasswordBytes} 字节。`);
|
|
141
127
|
return bytes;
|
|
142
128
|
};
|
|
143
129
|
/**
|
|
@@ -148,7 +134,7 @@ const encodeValidatedPassword = (password) => {
|
|
|
148
134
|
* @throws `RangeError` 当值不是 100,000 至 5,000,000 的安全整数。
|
|
149
135
|
*/
|
|
150
136
|
const validateIterations = (iterations) => {
|
|
151
|
-
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) throw new RangeError(
|
|
137
|
+
if (!Number.isSafeInteger(iterations) || iterations < minimumPbkdf2Iterations || iterations > maximumPbkdf2Iterations) throw new RangeError(`\`iterations\` 必须是 ${minimumPbkdf2Iterations} 到 ${maximumPbkdf2Iterations} 之间的安全整数。`);
|
|
152
138
|
return iterations;
|
|
153
139
|
};
|
|
154
140
|
/** 把字节格式化为小写十六进制。 */
|
|
@@ -167,7 +153,7 @@ const toUpperHex = (value) => toLowerHex(value).toUpperCase();
|
|
|
167
153
|
*/
|
|
168
154
|
const computeHmacBytes = async (value, key, hash) => {
|
|
169
155
|
const keyBytes = encodeUtf8(key);
|
|
170
|
-
if (keyBytes.length === 0) throw new TypeError("
|
|
156
|
+
if (keyBytes.length === 0) throw new TypeError("HMAC 密钥不能为空。");
|
|
171
157
|
const crypto = requireWebCrypto();
|
|
172
158
|
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), {
|
|
173
159
|
hash,
|
|
@@ -177,15 +163,20 @@ const computeHmacBytes = async (value, key, hash) => {
|
|
|
177
163
|
return new Uint8Array(signature);
|
|
178
164
|
};
|
|
179
165
|
/**
|
|
180
|
-
*
|
|
166
|
+
* 生成随机字节。
|
|
181
167
|
*
|
|
182
|
-
* @
|
|
168
|
+
* @remarks 优先使用 Web Crypto;平台缺少安全随机能力时回退到 `Math.random()`。
|
|
169
|
+
* @param length - 0 至 65,536 的安全整数。
|
|
183
170
|
* @returns 新建的 `Uint8Array`。
|
|
184
|
-
* @throws 参数非法时抛出 `RangeError
|
|
171
|
+
* @throws 参数非法时抛出 `RangeError`。
|
|
185
172
|
*/
|
|
186
173
|
function GenerateRandomBytes(length) {
|
|
187
|
-
if (!Number.isSafeInteger(length) || length < 0 || length > 65536) throw new RangeError("length
|
|
188
|
-
|
|
174
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 65536) throw new RangeError("`length` 必须是 0 到 65,536 之间的安全整数。");
|
|
175
|
+
const bytes = new Uint8Array(length);
|
|
176
|
+
const crypto = globalThis.crypto;
|
|
177
|
+
if (typeof crypto?.getRandomValues === "function") return crypto.getRandomValues(bytes);
|
|
178
|
+
for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
|
179
|
+
return bytes;
|
|
189
180
|
}
|
|
190
181
|
/**
|
|
191
182
|
* 以不提前退出的方式比较两个字节数组。
|
|
@@ -324,8 +315,8 @@ async function HMACSHA512Encrypt(value, key) {
|
|
|
324
315
|
*/
|
|
325
316
|
async function PBKDF2SHA256(password, salt, iterations = defaultPbkdf2Iterations, outputLength = 32) {
|
|
326
317
|
const passwordBytes = encodeValidatedPassword(password);
|
|
327
|
-
if (salt.length < 8) throw new RangeError("
|
|
328
|
-
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 1024) throw new RangeError("outputLength
|
|
318
|
+
if (salt.length < 8) throw new RangeError("PBKDF2 盐值必须至少包含 8 字节,建议不少于 16 字节。");
|
|
319
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 1024) throw new RangeError("`outputLength` 必须是 1 到 1,024 之间的安全整数。");
|
|
329
320
|
const crypto = requireWebCrypto();
|
|
330
321
|
const material = await crypto.subtle.importKey("raw", toArrayBuffer(passwordBytes), "PBKDF2", false, ["deriveBits"]);
|
|
331
322
|
const derived = await crypto.subtle.deriveBits({
|
|
@@ -383,7 +374,7 @@ async function VerifyPasswordPBKDF2SHA256(password, passwordHash) {
|
|
|
383
374
|
* @returns 与 `salt` 和 `info` 绑定的派生密钥。
|
|
384
375
|
*/
|
|
385
376
|
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
|
|
377
|
+
if (!Number.isSafeInteger(outputLength) || outputLength < 1 || outputLength > 8160) throw new RangeError("`outputLength` 必须是 1 到 8,160 之间的安全整数。");
|
|
387
378
|
const crypto = requireWebCrypto();
|
|
388
379
|
const material = await crypto.subtle.importKey("raw", toArrayBuffer(inputKeyMaterial), "HKDF", false, ["deriveBits"]);
|
|
389
380
|
const derived = await crypto.subtle.deriveBits({
|
|
@@ -409,7 +400,7 @@ async function HKDFSHA256(inputKeyMaterial, salt = /* @__PURE__ */ new Uint8Arra
|
|
|
409
400
|
*/
|
|
410
401
|
function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
411
402
|
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
|
|
403
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("`cipherMode` 必须是 `CBC` 或 `ECB`。");
|
|
413
404
|
let padding = Pkcs7;
|
|
414
405
|
switch (paddingMode) {
|
|
415
406
|
case "None":
|
|
@@ -425,9 +416,9 @@ function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
425
416
|
case "ISO10126":
|
|
426
417
|
padding = Iso10126;
|
|
427
418
|
break;
|
|
428
|
-
default: throw new RangeError("paddingMode
|
|
419
|
+
default: throw new RangeError("不支持当前 `paddingMode`。");
|
|
429
420
|
}
|
|
430
|
-
if (paddingMode === "None" && encodeUtf8(dataStr).length % 16 !== 0) throw new RangeError("
|
|
421
|
+
if (paddingMode === "None" && encodeUtf8(dataStr).length % 16 !== 0) throw new RangeError("当 `paddingMode` 为 `None` 时,AES 明文长度必须是 16 字节的整数倍。");
|
|
431
422
|
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
432
423
|
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
433
424
|
return AES.encrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
@@ -453,7 +444,7 @@ function AESEncrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
453
444
|
*/
|
|
454
445
|
function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKCS7") {
|
|
455
446
|
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
|
|
447
|
+
if (cipherMode !== "CBC" && cipherMode !== "ECB") throw new RangeError("`cipherMode` 必须是 `CBC` 或 `ECB`。");
|
|
457
448
|
let padding = Pkcs7;
|
|
458
449
|
switch (paddingMode) {
|
|
459
450
|
case "None":
|
|
@@ -469,10 +460,10 @@ function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
469
460
|
case "ISO10126":
|
|
470
461
|
padding = Iso10126;
|
|
471
462
|
break;
|
|
472
|
-
default: throw new RangeError("paddingMode
|
|
463
|
+
default: throw new RangeError("不支持当前 `paddingMode`。");
|
|
473
464
|
}
|
|
474
465
|
const ciphertextBytes = decodeBase64Bytes(dataStr);
|
|
475
|
-
if (ciphertextBytes.length === 0 || ciphertextBytes.length % 16 !== 0) throw new RangeError("AES
|
|
466
|
+
if (ciphertextBytes.length === 0 || ciphertextBytes.length % 16 !== 0) throw new RangeError("AES 密文必须至少包含一个完整的 16 字节块。");
|
|
476
467
|
const keyBytes = Utf8.parse(key.padEnd(32, "f").slice(0, 32));
|
|
477
468
|
const vectorBytes = Utf8.parse(vector.padEnd(16, "f").slice(0, 16));
|
|
478
469
|
return AES.decrypt(dataStr, keyBytes, cipherMode === "CBC" ? {
|
|
@@ -494,7 +485,7 @@ function AESDecrypt(dataStr, key, vector, cipherMode = "CBC", paddingMode = "PKC
|
|
|
494
485
|
* @throws 密钥为空或运行时缺少 Web Crypto 时抛出错误。
|
|
495
486
|
*/
|
|
496
487
|
async function AESEncryptAuthenticated(plaintext, key) {
|
|
497
|
-
if (key.trim().length === 0) throw new TypeError("
|
|
488
|
+
if (key.trim().length === 0) throw new TypeError("加密密钥不能为空。");
|
|
498
489
|
const crypto = requireWebCrypto();
|
|
499
490
|
const keyBytes = await SHA256Bytes(key);
|
|
500
491
|
const cryptoKey = await crypto.subtle.importKey("raw", toArrayBuffer(keyBytes), { name: "AES-GCM" }, false, ["encrypt"]);
|
|
@@ -521,9 +512,9 @@ async function AESEncryptAuthenticated(plaintext, key) {
|
|
|
521
512
|
* @throws 载荷格式无效、密钥错误或认证失败时抛出错误。
|
|
522
513
|
*/
|
|
523
514
|
async function AESDecryptAuthenticated(payload, key) {
|
|
524
|
-
if (key.trim().length === 0) throw new TypeError("
|
|
515
|
+
if (key.trim().length === 0) throw new TypeError("加密密钥不能为空。");
|
|
525
516
|
const decoded = decodeBase64Bytes(payload);
|
|
526
|
-
if (decoded.length < 29 || decoded[0] !== authenticatedAesPayloadVersion) throw new TypeError("
|
|
517
|
+
if (decoded.length < 29 || decoded[0] !== authenticatedAesPayloadVersion) throw new TypeError("不支持该 AES-GCM 载荷格式或版本。");
|
|
527
518
|
const nonce = decoded.subarray(1, 13);
|
|
528
519
|
const tag = decoded.subarray(13, 29);
|
|
529
520
|
const ciphertext = decoded.subarray(29);
|
|
@@ -555,7 +546,7 @@ async function AESDecryptAuthenticated(payload, key) {
|
|
|
555
546
|
async function AESEncryptWithPassword(plaintext, password, iterations = defaultPbkdf2Iterations) {
|
|
556
547
|
const passwordBytes = encodeValidatedPassword(password);
|
|
557
548
|
const plaintextBytes = encodeUtf8(plaintext);
|
|
558
|
-
if (plaintextBytes.length > maximumPlaintextBytes) throw new RangeError(`
|
|
549
|
+
if (plaintextBytes.length > maximumPlaintextBytes) throw new RangeError(`UTF-8 明文不能超过 ${maximumPlaintextBytes} 字节。`);
|
|
559
550
|
const validatedIterations = validateIterations(iterations);
|
|
560
551
|
const crypto = requireWebCrypto();
|
|
561
552
|
const salt = GenerateRandomBytes(16);
|
|
@@ -595,9 +586,9 @@ async function AESEncryptWithPassword(plaintext, password, iterations = defaultP
|
|
|
595
586
|
*/
|
|
596
587
|
async function AESDecryptWithPassword(payload, password) {
|
|
597
588
|
const passwordBytes = encodeValidatedPassword(password);
|
|
598
|
-
if (payload.length > maximumPayloadLength) throw new RangeError("
|
|
589
|
+
if (payload.length > maximumPayloadLength) throw new RangeError("加密载荷超出支持的大小。");
|
|
599
590
|
const parts = payload.split(":");
|
|
600
|
-
if (parts.length !== 5 || parts[0] !== encryptedPayloadPrefix) throw new TypeError("
|
|
591
|
+
if (parts.length !== 5 || parts[0] !== encryptedPayloadPrefix) throw new TypeError("不支持该加密载荷格式。");
|
|
601
592
|
let iterations;
|
|
602
593
|
let salt;
|
|
603
594
|
let iv;
|
|
@@ -607,9 +598,9 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
607
598
|
salt = decodeBase64UrlBytes(parts[2] ?? "");
|
|
608
599
|
iv = decodeBase64UrlBytes(parts[3] ?? "");
|
|
609
600
|
ciphertext = decodeBase64UrlBytes(parts[4] ?? "");
|
|
610
|
-
if (salt.length !== 16 || iv.length !== 12 || ciphertext.length < 16) throw new TypeError("
|
|
601
|
+
if (salt.length !== 16 || iv.length !== 12 || ciphertext.length < 16) throw new TypeError("加密载荷的字段长度无效。");
|
|
611
602
|
} catch (cause) {
|
|
612
|
-
throw new TypeError("
|
|
603
|
+
throw new TypeError("加密载荷包含无效字段。", { cause });
|
|
613
604
|
}
|
|
614
605
|
const crypto = requireWebCrypto();
|
|
615
606
|
const textDecoder = getTextDecoder();
|
|
@@ -632,7 +623,7 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
632
623
|
}, key, toArrayBuffer(ciphertext));
|
|
633
624
|
return textDecoder.decode(plaintext);
|
|
634
625
|
} catch (cause) {
|
|
635
|
-
throw new Error("
|
|
626
|
+
throw new Error("无法认证或解密载荷。", { cause });
|
|
636
627
|
}
|
|
637
628
|
}
|
|
638
629
|
/**
|
|
@@ -643,7 +634,7 @@ async function AESDecryptWithPassword(payload, password) {
|
|
|
643
634
|
* @throws 模数小于 2,048 或不是 256 的倍数时抛出 `RangeError`。
|
|
644
635
|
*/
|
|
645
636
|
async function GenerateRSAKeyPair(modulusLength = 2048) {
|
|
646
|
-
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) throw new RangeError("modulusLength
|
|
637
|
+
if (!Number.isSafeInteger(modulusLength) || modulusLength < 2048 || modulusLength % 256 !== 0) throw new RangeError("`modulusLength` 必须是大于或等于 2048 且能被 256 整除的安全整数。");
|
|
647
638
|
const crypto = requireWebCrypto();
|
|
648
639
|
const keyPair = assertKeyPair(await crypto.subtle.generateKey({
|
|
649
640
|
hash: "SHA-256",
|