sm-crypto-wasm 0.3.0 → 0.3.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/README.md CHANGED
@@ -240,6 +240,8 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
240
240
  | 浏览器(经打包器) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`,由打包器归档并重写 URL |
241
241
  | Web Worker | 加载器不依赖 DOM,Worker 内可用 |
242
242
  | Deno / Bun | 走 Node 同路径 |
243
+ | 微信内置浏览器 | 与浏览器相同(`fetch` + `crypto.getRandomValues`);见[微信与小程序](https://jktantan.github.io/sm-crypto-wasm/guide/wechat) |
244
+ | 微信小程序 | `WXWebAssembly` + `wx.getRandomValues`;见[微信与小程序](https://jktantan.github.io/sm-crypto-wasm/guide/wechat) |
243
245
 
244
246
  ### 打包器支持
245
247
 
@@ -604,6 +606,8 @@ Available sub-paths: `sm-crypto-wasm/sm2` · `/sm3` · `/sm4` · `/sm9` · `/zuc
604
606
  | Browser (via a bundler) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; the bundler archives and rewrites it |
605
607
  | Web Worker | the loader is DOM-free and works inside a Worker |
606
608
  | Deno / Bun | follows the Node path |
609
+ | WeChat built-in browser | same as browsers (`fetch` + `crypto.getRandomValues`); see [WeChat & Mini Programs](https://jktantan.github.io/sm-crypto-wasm/en/guide/wechat) |
610
+ | WeChat Mini Program | `WXWebAssembly` + `wx.getRandomValues`; see [WeChat & Mini Programs](https://jktantan.github.io/sm-crypto-wasm/en/guide/wechat) |
607
611
 
608
612
  ### Bundler support
609
613
 
@@ -997,6 +1001,8 @@ import { encryptCBC } from 'sm-crypto-wasm/sm4';
997
1001
  | Браузер (через сборщик) | `fetch(new URL('../<algo>/<algo>.wasm', import.meta.url))`; сборщик кладёт файл в вывод и переписывает URL |
998
1002
  | Web Worker | загрузчик не зависит от DOM и работает внутри Worker'а |
999
1003
  | Deno / Bun | идут по Node-пути |
1004
+ | Встроенный браузер WeChat | как обычный браузер (`fetch` + `crypto.getRandomValues`); см. [WeChat и мини-программы](https://jktantan.github.io/sm-crypto-wasm/ru/guide/wechat) |
1005
+ | Мини-программа WeChat | `WXWebAssembly` + `wx.getRandomValues`; см. [WeChat и мини-программы](https://jktantan.github.io/sm-crypto-wasm/ru/guide/wechat) |
1000
1006
 
1001
1007
  ### Поддержка сборщиков
1002
1008
 
@@ -0,0 +1,22 @@
1
+ /**
2
+ * base64 纯 JS 实现(宿主既没有 `Buffer`、也没有 `btoa` / `atob` 时的兜底)。
3
+ *
4
+ * 存在理由:微信小程序等运行时没有 `btoa` / `atob`,而 SM2 / SM9 的 PEM 密钥
5
+ * 生成与解析需要 base64。base64 是确定性编码、不属于密码学原语,因此在库内兜底
6
+ * 不需要像 RNG 那样 fail-closed。
7
+ *
8
+ * 只在 `runtime/convert.ts` 的最后一级回退里使用;Node 有 `Buffer`、浏览器有
9
+ * `btoa` / `atob` 时都不会走到这里。零 `node:*` 引用,可安全进入浏览器/小程序产物。
10
+ */
11
+ /** 标准 base64 编码(带 `=` padding)。 */
12
+ export declare function encodeBase64(bytes: Uint8Array): string;
13
+ /**
14
+ * 标准 base64 解码。
15
+ *
16
+ * 与 `atob` 的宽松度对齐:允许空白字符与缺失 padding;非法字符、非法长度抛
17
+ * `TypeError`(`convert.base64ToBytes` 的契约是失败抛 TypeError)。
18
+ *
19
+ * @param b64 base64 字符串。
20
+ * @param label 错误信息里的字段名。
21
+ */
22
+ export declare function decodeBase64(b64: string, label?: string): Uint8Array;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * base64 纯 JS 实现(宿主既没有 `Buffer`、也没有 `btoa` / `atob` 时的兜底)。
3
+ *
4
+ * 存在理由:微信小程序等运行时没有 `btoa` / `atob`,而 SM2 / SM9 的 PEM 密钥
5
+ * 生成与解析需要 base64。base64 是确定性编码、不属于密码学原语,因此在库内兜底
6
+ * 不需要像 RNG 那样 fail-closed。
7
+ *
8
+ * 只在 `runtime/convert.ts` 的最后一级回退里使用;Node 有 `Buffer`、浏览器有
9
+ * `btoa` / `atob` 时都不会走到这里。零 `node:*` 引用,可安全进入浏览器/小程序产物。
10
+ */
11
+ const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
12
+ /** 标准 base64 编码(带 `=` padding)。 */
13
+ export function encodeBase64(bytes) {
14
+ let out = '';
15
+ for (let i = 0; i < bytes.length; i += 3) {
16
+ const a = bytes[i] ?? 0;
17
+ const hasB = i + 1 < bytes.length;
18
+ const hasC = i + 2 < bytes.length;
19
+ const b = hasB ? (bytes[i + 1] ?? 0) : 0;
20
+ const c = hasC ? (bytes[i + 2] ?? 0) : 0;
21
+ const n = (a << 16) | (b << 8) | c;
22
+ out += B64.charAt((n >> 18) & 63);
23
+ out += B64.charAt((n >> 12) & 63);
24
+ out += hasB ? B64.charAt((n >> 6) & 63) : '=';
25
+ out += hasC ? B64.charAt(n & 63) : '=';
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * 标准 base64 解码。
31
+ *
32
+ * 与 `atob` 的宽松度对齐:允许空白字符与缺失 padding;非法字符、非法长度抛
33
+ * `TypeError`(`convert.base64ToBytes` 的契约是失败抛 TypeError)。
34
+ *
35
+ * @param b64 base64 字符串。
36
+ * @param label 错误信息里的字段名。
37
+ */
38
+ export function decodeBase64(b64, label = 'base64') {
39
+ const stripped = b64.replace(/[\t\n\f\r ]/g, '');
40
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(stripped)) {
41
+ throw new TypeError(`${label}: invalid base64`);
42
+ }
43
+ const pad = stripped.endsWith('==') ? 2 : stripped.endsWith('=') ? 1 : 0;
44
+ if (pad > 0 && stripped.length % 4 !== 0) {
45
+ throw new TypeError(`${label}: invalid base64 padding`);
46
+ }
47
+ const body = pad > 0 ? stripped.slice(0, -pad) : stripped;
48
+ if (body.length % 4 === 1) {
49
+ throw new TypeError(`${label}: invalid base64 length`);
50
+ }
51
+ const out = new Uint8Array(Math.floor((body.length * 3) / 4));
52
+ let buffer = 0;
53
+ let bits = 0;
54
+ let pos = 0;
55
+ for (let i = 0; i < body.length; i++) {
56
+ const v = B64.indexOf(body.charAt(i));
57
+ if (v < 0)
58
+ throw new TypeError(`${label}: invalid base64 character at index ${i}`);
59
+ buffer = (buffer << 6) | v;
60
+ bits += 6;
61
+ if (bits >= 8) {
62
+ bits -= 8;
63
+ out[pos++] = (buffer >> bits) & 0xff;
64
+ buffer &= (1 << bits) - 1;
65
+ }
66
+ }
67
+ return out.subarray(0, pos);
68
+ }
@@ -40,10 +40,19 @@ export declare function fromBytes(bytes: Uint8Array, format?: OutputFormat): str
40
40
  export declare function bytesToHex(bytes: Uint8Array): string;
41
41
  /** hex 字符串 → bytes。支持 `0x` 前缀。奇数位长度或非法字符抛错。 */
42
42
  export declare function hexToBytes(hex: string, label?: string): Uint8Array;
43
- /** bytes → base64。Node `Buffer` 优先,浏览器回落到 `btoa`。 */
43
+ /**
44
+ * bytes → base64。优先级:Node `Buffer` → 宿主 `btoa` → 纯 JS 兜底。
45
+ *
46
+ * 微信小程序既没有 `Buffer` 也没有 `btoa`,会走最后一级;不需要宿主再安装
47
+ * base64 polyfill。
48
+ */
44
49
  export declare function bytesToBase64(bytes: Uint8Array): string;
45
- /** base64 字符串 → bytes。失败抛 TypeError。 */
46
- export declare function base64ToBytes(b64: string, _label?: string): Uint8Array;
50
+ /**
51
+ * base64 字符串 → bytes。失败抛 TypeError。
52
+ *
53
+ * 优先级:Node `Buffer` → 宿主 `atob` → 纯 JS 兜底(微信小程序等)。
54
+ */
55
+ export declare function base64ToBytes(b64: string, label?: string): Uint8Array;
47
56
  /** PEM → bytes。剥去 BEGIN/END armor,base64 解码。 */
48
57
  export declare function pemToBytes(pem: string, label?: string): Uint8Array;
49
58
  /** bytes → PEM(带 BEGIN/END 头)。`kind` 例:`SM2 PRIVATE KEY`。 */
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * spec/plan 对应:plan Task 2;spec §6 API 默认 hex、§9 安全姿态。
18
18
  */
19
+ import { decodeBase64, encodeBase64 } from './base64.core.js';
19
20
  /**
20
21
  * 把任意 `Input` 归一化为 `Uint8Array`。
21
22
  *
@@ -106,20 +107,32 @@ export function hexToBytes(hex, label = 'hex') {
106
107
  // tryParseHex 已覆盖全部合法情形,走到这里说明上面漏判,兜底抛错。
107
108
  throw new TypeError(`hexToBytes(${label}): invalid hex`);
108
109
  }
109
- /** bytes → base64。Node `Buffer` 优先,浏览器回落到 `btoa`。 */
110
+ /**
111
+ * bytes → base64。优先级:Node `Buffer` → 宿主 `btoa` → 纯 JS 兜底。
112
+ *
113
+ * 微信小程序既没有 `Buffer` 也没有 `btoa`,会走最后一级;不需要宿主再安装
114
+ * base64 polyfill。
115
+ */
110
116
  export function bytesToBase64(bytes) {
111
117
  if (HAS_BUFFER) {
112
118
  return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64');
113
119
  }
114
- let bin = '';
115
- for (let i = 0; i < bytes.length; i++)
116
- bin += String.fromCharCode(bytes[i] ?? 0);
117
- return btoa(bin);
120
+ if (HAS_BTOA) {
121
+ let bin = '';
122
+ for (let i = 0; i < bytes.length; i++)
123
+ bin += String.fromCharCode(bytes[i] ?? 0);
124
+ return btoa(bin);
125
+ }
126
+ return encodeBase64(bytes);
118
127
  }
119
128
  /** base64 合法字符集(含结尾 padding)。 */
120
129
  const B64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
121
- /** base64 字符串 → bytes。失败抛 TypeError。 */
122
- export function base64ToBytes(b64, _label = 'base64') {
130
+ /**
131
+ * base64 字符串 → bytes。失败抛 TypeError。
132
+ *
133
+ * 优先级:Node `Buffer` → 宿主 `atob` → 纯 JS 兜底(微信小程序等)。
134
+ */
135
+ export function base64ToBytes(b64, label = 'base64') {
123
136
  // Node 快路径。`Buffer.from(..., 'base64')` 对非法字符是静默丢弃的,
124
137
  // 先用正则挡掉,非法的落到 `atob` 去抛错,保持与浏览器一致的行为。
125
138
  if (HAS_BUFFER && B64_RE.test(b64)) {
@@ -128,11 +141,14 @@ export function base64ToBytes(b64, _label = 'base64') {
128
141
  out.set(buf);
129
142
  return out;
130
143
  }
131
- const bin = atob(b64);
132
- const out = new Uint8Array(bin.length);
133
- for (let i = 0; i < bin.length; i++)
134
- out[i] = bin.charCodeAt(i);
135
- return out;
144
+ if (HAS_ATOB) {
145
+ const bin = atob(b64);
146
+ const out = new Uint8Array(bin.length);
147
+ for (let i = 0; i < bin.length; i++)
148
+ out[i] = bin.charCodeAt(i);
149
+ return out;
150
+ }
151
+ return decodeBase64(b64, label);
136
152
  }
137
153
  /** PEM → bytes。剥去 BEGIN/END armor,base64 解码。 */
138
154
  export function pemToBytes(pem, label = 'pem') {
@@ -169,6 +185,9 @@ export function bytesToPem(bytes, kind) {
169
185
  * (实测 4 KB 输入:hex 编码 52.6 µs → 1.9 µs)。
170
186
  */
171
187
  const HAS_BUFFER = typeof Buffer !== 'undefined' && typeof Buffer.from === 'function';
188
+ /** 宿主是否提供 `btoa` / `atob`(现代浏览器有;微信小程序没有)。 */
189
+ const HAS_BTOA = typeof btoa === 'function';
190
+ const HAS_ATOB = typeof atob === 'function';
172
191
  /** 16 进制数字查表(浏览器路径)。 */
173
192
  const HEX_CHARS = '0123456789abcdef';
174
193
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sm-crypto-wasm",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "国密 SM2/SM3/SM4/SM9/ZUC 的 WASM 实现,Node 18+ 与现代浏览器均可使用,按算法分包懒加载。",
5
5
  "description:en": "SM2/SM3/SM4/SM9/ZUC (China crypto) implemented in AssemblyScript -> WASM. Node 18+ & modern browsers, per-algorithm lazy loading.",
6
6
  "license": "Apache-2.0",
@@ -96,6 +96,7 @@
96
96
  "docs:dev": "pnpm build && vitepress dev website",
97
97
  "docs:build": "pnpm build && vitepress build website",
98
98
  "docs:preview": "vitepress preview website",
99
+ "example:wechat:prepare": "node examples/wechat-miniprogram/scripts/prepare.mjs",
99
100
  "prepack": "pnpm build && pnpm test"
100
101
  },
101
102
  "devDependencies": {