@trustbaseai/account 0.1.0

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.
Files changed (62) hide show
  1. package/README.md +351 -0
  2. package/dist/address.d.ts +70 -0
  3. package/dist/address.d.ts.map +1 -0
  4. package/dist/address.js +104 -0
  5. package/dist/address.js.map +1 -0
  6. package/dist/bech32.d.ts +78 -0
  7. package/dist/bech32.d.ts.map +1 -0
  8. package/dist/bech32.js +246 -0
  9. package/dist/bech32.js.map +1 -0
  10. package/dist/bytes.d.ts +88 -0
  11. package/dist/bytes.d.ts.map +1 -0
  12. package/dist/bytes.js +215 -0
  13. package/dist/bytes.js.map +1 -0
  14. package/dist/envelope.d.ts +116 -0
  15. package/dist/envelope.d.ts.map +1 -0
  16. package/dist/envelope.js +237 -0
  17. package/dist/envelope.js.map +1 -0
  18. package/dist/errors.d.ts +48 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +41 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/hd.d.ts +85 -0
  23. package/dist/hd.d.ts.map +1 -0
  24. package/dist/hd.js +112 -0
  25. package/dist/hd.js.map +1 -0
  26. package/dist/index.d.ts +60 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +199 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/kdf.d.ts +163 -0
  31. package/dist/kdf.d.ts.map +1 -0
  32. package/dist/kdf.js +351 -0
  33. package/dist/kdf.js.map +1 -0
  34. package/dist/keys.d.ts +55 -0
  35. package/dist/keys.d.ts.map +1 -0
  36. package/dist/keys.js +114 -0
  37. package/dist/keys.js.map +1 -0
  38. package/dist/keystore.d.ts +168 -0
  39. package/dist/keystore.d.ts.map +1 -0
  40. package/dist/keystore.js +340 -0
  41. package/dist/keystore.js.map +1 -0
  42. package/dist/mnemonic.d.ts +84 -0
  43. package/dist/mnemonic.d.ts.map +1 -0
  44. package/dist/mnemonic.js +199 -0
  45. package/dist/mnemonic.js.map +1 -0
  46. package/dist/password.d.ts +79 -0
  47. package/dist/password.d.ts.map +1 -0
  48. package/dist/password.js +237 -0
  49. package/dist/password.js.map +1 -0
  50. package/dist/prf-webauthn.d.ts +85 -0
  51. package/dist/prf-webauthn.d.ts.map +1 -0
  52. package/dist/prf-webauthn.js +204 -0
  53. package/dist/prf-webauthn.js.map +1 -0
  54. package/dist/prf.d.ts +157 -0
  55. package/dist/prf.d.ts.map +1 -0
  56. package/dist/prf.js +318 -0
  57. package/dist/prf.js.map +1 -0
  58. package/dist/webcrypto.d.ts +93 -0
  59. package/dist/webcrypto.d.ts.map +1 -0
  60. package/dist/webcrypto.js +157 -0
  61. package/dist/webcrypto.js.map +1 -0
  62. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # @trustbaseai/account
2
+
3
+ TrustBase 的**账户与密钥库**:地址派生、口令加密的私钥存储、WebAuthn PRF 硬件解锁、
4
+ 口令强度估算与 BIP39 助记词(纸密钥)。
5
+
6
+ > **为什么这是第一个功能包**:SDK 方案 §3.2 拍板「账户能力是 SDK 的默认组件,不是插件。
7
+ > 任何基于本 SDK 的 PWA 打开就带「我的账户」面板」。要做到这件事,先得有一块**能登录、
8
+ > 能管理自己账户**的地基,而且它必须是**安全关键**的那块 —— 本包就是这块地基。
9
+
10
+ 一个硬约束贯穿全部代码:**绝不托管**(SDK 方案 §3.2.1)。本包不知道任何用户的明文,
11
+ 没有"找回密码"能力,助记词是唯一退路。任何看起来像"帮用户保管密钥"的 API 都不属于这里。
12
+
13
+ ---
14
+
15
+ ## 1. 四块能力与模块对应
16
+
17
+ | 能力 | 模块 | 依据(设计文档) |
18
+ |---|---|---|
19
+ | `tct1…` 地址派生 | `src/address.ts` + `src/keys.ts` + `src/bech32.ts` | 入口文档 §8.3.2 ①②、SDK 方案 §3.2.3 |
20
+ | 口令加密 keystore | `src/keystore.ts` + `src/kdf.ts` + `src/webcrypto.ts` | SDK 方案 §3.3.1 / §3.3.2 |
21
+ | PRF 硬件解锁(双轨) | `src/prf.ts` + `src/envelope.ts` + `src/prf-webauthn.ts` | SDK 方案 §3.3.5 / §3.3.7 |
22
+ | 口令强度 + 助记词 | `src/password.ts` + `src/mnemonic.ts` + `src/hd.ts` | SDK 方案 §3.3.6 / §3.3.3 |
23
+
24
+ **同构**:`src/` 零 Node 依赖(浏览器 PWA 与 Node 守护进程同一份代码),
25
+ 这条由 `test/isomorphic.test.ts` 静态扫描守着:出现 `node:` 导入、`process.`、`Buffer`、
26
+ `__dirname`、`Math.random`、或在白名单外碰 `navigator`/`localStorage`,测试直接红。
27
+
28
+ ---
29
+
30
+ ## 2. 地址派生(口径必须与链一致)
31
+
32
+ ```
33
+ address = bech32( hrp='tct', ripemd160( sha256( compressed_secp256k1_pubkey ) ) )
34
+ └──────────── 20 字节 payload ────────────┘
35
+ ```
36
+
37
+ 三个"必须一致"的点,错一个都会在链上静默失败(例如退款无处可退):
38
+
39
+ | # | 要点 | 错了会怎样 |
40
+ |---|---|---|
41
+ | 1 | 公钥必须是**压缩**形态(33 字节) | 用非压缩公钥派生 → 得到另一个地址:收款地址与签名者地址不是同一个 |
42
+ | 2 | 顺序是 `ripemd160(sha256(pubkey))` | 不是链上认的账户地址,`AccAddressFromBech32` 过不了 |
43
+ | 3 | 编码是 **bech32**(BIP-173),HRP = `tct` | 别的钱包解不开;bech32m 会被本实现明确拒绝(报"这是 bech32m"而不是含糊的校验和错误) |
44
+
45
+ ```ts
46
+ import { createAccount, accountAddressFromPublicKey, isAccountAddress, parseAccountAddress } from '@trustbaseai/account';
47
+
48
+ const account = createAccount(); // { mnemonic, privateKey, publicKey, address, path }
49
+ account.address; // tct1…(12 词助记词,路径 m/44'/118'/0'/0/0)
50
+ isAccountAddress('tct1…'); // 不抛异常的判定(校验和 + HRP + 20 字节 payload)
51
+ parseAccountAddress('tct1…').bytes; // 20 字节 payload
52
+ ```
53
+
54
+ **验证方式(三层,缺一不可)**:
55
+
56
+ 1. **公开向量**:BIP-173 的 bech32 测试向量(含 P2WPKH 示例 `bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4`)、
57
+ 比特币"私钥 = 1"的 hash160 `751e76e8199196d454941c45d1b3a323f1433bd6`、BIP-39 官方英文向量。
58
+ 出处逐条写在 `test-vectors/address.golden.json` 的 `sources` 里
59
+ 2. **独立复算**:`test/address.test.ts` 用 `node:crypto`(OpenSSL)自己算一遍 sha256/ripemd160
60
+ 3. **交叉验证**:与 `@cosmjs/proto-signing` 的 `DirectSecp256k1HdWallet` 比地址、与
61
+ `@cosmjs/crypto` 的 `Bip39 + Slip10` 比私钥 —— 官方向量 + **随机助记词 5 轮**,逐字符一致
62
+
63
+ ---
64
+
65
+ ## 3. 加密 keystore(格式 v1)
66
+
67
+ ```jsonc
68
+ {
69
+ "version": 1,
70
+ "kdf": "argon2id", // 或 "pbkdf2"
71
+ "kdfParams": { // argon2id:salt / memory / cost / parallelism / hashLength / version
72
+ "salt": "<base64, 16 字节>", // pbkdf2:salt / iterations / hashLength / hash
73
+ "memory": 19456, "cost": 2, "parallelism": 1, "hashLength": 32, "version": 19
74
+ },
75
+ "cipher": "aes-256-gcm",
76
+ "iv": "<base64, 12 字节>",
77
+ "ciphertext": "<base64>",
78
+ "tag": "<base64, 16 字节>"
79
+ }
80
+ ```
81
+
82
+ ```ts
83
+ import { createKeystore, unlockKeystore, serializeKeystore, parseKeystore, wipe } from '@trustbaseai/account';
84
+
85
+ const blob = await createKeystore(privateKey, password, { minStrength: 3 });
86
+ const store = serializeKeystore(blob); // 只把这段密文写进 IndexedDB / 本机文件
87
+ const opened = await unlockKeystore(store, password); // 明文只在内存里
88
+ // …用完
89
+ wipe(opened);
90
+ ```
91
+
92
+ ### 3.1 明文头参与认证(本实现相对设计文档的加固)
93
+
94
+ 设计文档列的字段里,`version` / `kdf` / `kdfParams` / `cipher` / `iv` 都是**明文头**。
95
+ 如果不认证它们,攻击者可以改掉 KDF 参数(甚至把 `argon2id` 改成 `pbkdf2`)而密文照样解得开
96
+ —— 那就等于"KDF 强度随人调"。本实现把整个明文头作为 GCM 的 **AAD**:
97
+
98
+ ```
99
+ trustbase-keystore-v1|kdf=argon2id|salt=<b64>|memory=19456|cost=2|parallelism=1|hashLength=32|version=19|cipher=aes-256-gcm|iv=<b64>
100
+ trustbase-keystore-v1|kdf=pbkdf2|salt=<b64>|iterations=600000|hashLength=32|hash=sha256|cipher=aes-256-gcm|iv=<b64>
101
+ ```
102
+
103
+ 这串文本被 `test/keystore.test.ts` 用**字面量**钉住(改它 = 改密钥库格式),
104
+ 并且任何一位被改都会让解锁以 `AUTH_FAILED` 失败。
105
+
106
+ ### 3.2 KDF 参数的依据与升级路径
107
+
108
+ | 项 | 默认值 | 依据 |
109
+ |---|---|---|
110
+ | Argon2id `memory` / `cost` / `parallelism` | 19456 KiB / 2 / 1 | OWASP Password Storage Cheat Sheet 的推荐下限(19 MiB, t=2, p=1);本机 WASM 实测 ~40 ms |
111
+ | Argon2id `version` | 19(v1.3) | hash-wasm 内置版本;别的版本号一律拒(`UNSUPPORTED_KDF`),不猜 |
112
+ | PBKDF2 `iterations` | 600000 | SDK 方案 §3.3.1 规定的下限;无 WASM 时兜底 |
113
+ | 盐 | 16 字节随机 | 每次创建新随机,绝不复用 |
114
+ | 派生密钥长度 | 32 字节 | AES-256 的密钥长度;格式 v1 固定,加长要换版本号 |
115
+
116
+ **升级路径**:参数随密文落盘,解锁**只认密文里的参数**。要换更强的参数:
117
+ 用旧参数 `unlockKeystore` → 用新参数 `createKeystore` → 覆盖落盘。旧密文永远解得开,
118
+ 不需要"重抄助记词"。
119
+
120
+ ### 3.3 错误码(调用方按码分支,不要按 message)
121
+
122
+ | 情况 | 错误码 |
123
+ |---|---|
124
+ | 口令错 / 密文被改 / AAD 头被改 / PRF 秘密不匹配 | `AUTH_FAILED`(**刻意含糊**:不区分它们) |
125
+ | 不是密文 / 字段缺失 / 类型不对 / base64 坏 / 参数越界 | `INVALID_BLOB` |
126
+ | `version` 比本实现新 | `UNSUPPORTED_VERSION` |
127
+ | `kdf` 不认 / Argon2 版本不认 / 本环境跑不了 Argon2id | `UNSUPPORTED_KDF` |
128
+ | 环境没有 WebCrypto | `CRYPTO_UNAVAILABLE` |
129
+ | 口令超长(>128)/ secret 形状不对 / 参数低于下限 | `INVALID_ARGUMENT` |
130
+ | 口令强度门未过 | `WEAK_PASSWORD`(`details` 带 `score` 与 `reasons`) |
131
+
132
+ **"口令错"与"密文被改"共用一个码、共用一句 message**,这是有意的(SDK 方案 §3.3.1):
133
+ 一旦能区分,攻击者就拿到了"这份密文是否被正确改写"的判定预言机。
134
+
135
+ ### 3.4 无 WASM 环境的行为(唯一允许的退化)
136
+
137
+ | 场景 | 行为 |
138
+ |---|---|
139
+ | **创建**时没有 WASM | 退到 PBKDF2-HMAC-SHA256(≥60 万次),并把 `kdf: 'pbkdf2'` 落盘 |
140
+ | **解锁** `argon2id` 密文时没有 WASM | **报 `UNSUPPORTED_KDF`,不回退** —— 回退会派生另一把密钥,把"环境不支持"伪装成"口令错" |
141
+
142
+ 探测是**真跑一次**(8 KiB / 1 迭代)而不只是 `typeof WebAssembly`:CSP 缺
143
+ `wasm-unsafe-eval` 时 `WebAssembly.instantiate` 才抛错,只查存在性会朝"能创建、
144
+ 将来打不开"的方向失败。`test/keystore-no-wasm.test.ts` 用删全局 + mock `hash-wasm` 两条路测。
145
+
146
+ ---
147
+
148
+ ## 4. 双轨解锁(生物识别优先、口令回退)
149
+
150
+ SDK 方案 §3.3.5 写的是「HKDF(PRF 秘密) 包住 keystore 的加密密钥」。要让它成立,
151
+ "被包住的东西"必须**独立于口令存在**,否则两条路算不出同一个值。于是引入一把
152
+ 不落盘的数据密钥:
153
+
154
+ ```
155
+ DEK(32 字节随机,只存在于内存,用完即清)
156
+ ├─ payload:账户密钥材料(私钥 / BIP39 种子)在 DEK 下加密
157
+ ├─ passwordTrack:DEK 在口令下加密(就是上面的 keystore v1)
158
+ └─ prfTrack:DEK 在 HKDF(PRF 秘密) 派生的 KEK 下包装
159
+ ```
160
+
161
+ ```ts
162
+ import { createUnlockEnvelope, attachPrfTrack, detachPrfTrack, unlockEnvelope } from '@trustbaseai/account';
163
+
164
+ // 建户:口令轨(§3.3.7 要求"强制用户设置口令",所以口令轨始终存在)
165
+ let envelope = await createUnlockEnvelope(privateKey, password, { minStrength: 3 });
166
+
167
+ // 之后开启生物识别:**不需要重新加密、不需要重新抄助记词**(要一次口令解出 DEK)
168
+ envelope = await attachPrfTrack(envelope, { password, prfSecret, credentialId });
169
+
170
+ // 两条路解出**同一份**密钥材料
171
+ const a = await unlockEnvelope(envelope, { strategy: 'password', password });
172
+ const b = await unlockEnvelope(envelope, { strategy: 'prf', prfSecret });
173
+ // a 与 b 逐字节相同(test/envelope.test.ts 用 toEqual 钉住)
174
+
175
+ // 撤销:去掉 PRF 轨,payload 与口令轨原封不动
176
+ const onlyPassword = await detachPrfTrack(envelope);
177
+ ```
178
+
179
+ 三条直接好处:**两条轨等价**(可无缝切换)、**PRF 轨可以后加**(不必重新加密)、
180
+ **PRF 轨可以撤销**(PRF 不是根,助记词与口令才是)。
181
+
182
+ ---
183
+
184
+ ## 5. PRF 硬件解锁:能做什么、不能做什么
185
+
186
+ ✅ **能做**:把"解锁 keystore 的钥匙"绑到**硬件 + 生物识别/PIN**。秘密由 Secure Enclave /
187
+ Android Keystore(StrongBox)持有、**永不导出**,取它要过生物识别。我们不存它、也导不出它。
188
+
189
+ ❌ **不能做(别往这个方向设计)**:拿 SE/Keystore 直接签链上交易。浏览器只有 WebAuthn,
190
+ 而 WebAuthn 断言签的是「认证器数据 + 客户端数据哈希」,**不能对任意 payload 签名**
191
+ —— 拿不到一把能签 Cosmos 交易的密钥(SDK 方案 §3.3.5 的 ⚠️ 段)。
192
+
193
+ ### 流程要点(照 WebAuthn PRF 扩展的实际行为写)
194
+
195
+ 1. **注册**:`create()` 时带 `extensions.prf.eval.first = salt`。**很多平台在 `create()`
196
+ 的返回值里不给 `results`**(只给 `enabled`),所以 `registerWebAuthnPrfCredential()`
197
+ 注册后**立刻再 `get()` 一次断言**去取 `results.first`
198
+ 2. **解锁**:`get()` 时带 `extensions.prf.eval.first = salt`,`allowCredentials` 放凭据的 `rawId`
199
+ 3. **salt 必须与注册时一致**(PRF 的定义就是"同 salt 同输出"),所以它存在我们的包装 blob 里
200
+ 4. **换设备 / 重装 / 重置生物识别 → 秘密变了或凭据没了** → 认证失败 → 回退口令或助记词。
201
+ 这是设计内行为,不是 bug(SDK 方案 §3.3.7 第 2 条)
202
+
203
+ `src/prf-webauthn.ts` 是**骨架**:结构照规范写,但没有 Node 单测覆盖成功路径
204
+ (Node 里没有 `navigator.credentials`)。理由与真机验证清单见下面 §9。
205
+
206
+ ---
207
+
208
+ ## 6. 口令强度(估算,不是复杂度规则)
209
+
210
+ ```ts
211
+ import { estimatePasswordStrength, isBlocklisted } from '@trustbaseai/account';
212
+
213
+ const strength = estimatePasswordStrength(password, { extraBlocklist: ['acme-store'] });
214
+ // { score: 0..4, reasons: ['这是最常见的十个口令之一', …], acceptable: boolean,
215
+ // guessesLog10, blocklisted, tooLong }
216
+ ```
217
+
218
+ | 项 | 口径 |
219
+ |---|---|
220
+ | 估算器 | `zxcvbn-ts`,评分 0..4 + **中文理由**(库默认返回 key 如 `topTen`,本包自己映射文案,不引英文文案包) |
221
+ | 通过线 | `score >= 3` **且**不在本地黑名单(`acceptableScore` 可调) |
222
+ | 黑名单 | **纯本地、不联网**:zxcvbn 自带字典(`passwords-common` 49233 条 + `diceware-common` 7776 条)+ `APP_BLOCKLIST`(产品/域名/币种/示例域名/中文产品词) |
223
+ | 匹配规则 | 归一化(小写 + 去非字母数字 + NFC)后**精确命中**,或**包含命中**(拉丁词要求 ≥5 字符,CJK 词 ≥2 字符) |
224
+ | 长度 | 上限 128 字符(SDK 方案 §3.3.6 的防 DoS 条款);超长**短路拒绝且不做估算** |
225
+ | **不做** | 大小写/数字/符号的复杂度规则(规则只会逼出 `Password1!` 这种"看着合规实际上榜"的口令);也不做 HIBP 之类的联网查询 |
226
+
227
+ **已知局限(照实说)**:由多个常见词拼成的长短语(如 `correct horse battery staple`)
228
+ 会被估成高分 —— 这是评分模型的性质,不是 bug。这类口令仍受 Argon2id 与硬件门控保护。
229
+ 另外,`score` 的具体数值会随 zxcvbn 数据版本漂移,所以测试里断言的是**性质**
230
+ (弱口令必须拒绝、强口令必须通过、同分不同理由)而不是固定分值。
231
+
232
+ ---
233
+
234
+ ## 7. 助记词与抄写门
235
+
236
+ | API | 语义 |
237
+ |---|---|
238
+ | `generateMnemonic(strength?)` | 生成 **128 / 256 bit**(12 / 24 词);随机源走 `crypto.getRandomValues`,可注入测试替身 |
239
+ | `validateMnemonic(mnemonic)` | 校验 BIP39(词表 + 校验和 + 词数);**宽进**:12..24 词都收 |
240
+ | `entropyToMnemonic` / `mnemonicToEntropy` | 熵 ↔ 词(只收 16 / 32 字节),用来跑官方向量 |
241
+ | `splitMnemonicWords` / `normalizeMnemonic` | 空白归一化(多空格/换行折成单空格 + NFKD) |
242
+ | `pickTranscriptionIndices(words, n, rng?)` | 抄写门抽词:**0 基下标**、升序去重、拒绝采样(无模偏差) |
243
+ | `checkTranscription(words, indices, answers)` | 校验回填:给 `{ok, failed, missing}`,**不返回正确答案**(否则抄写门就成了"帮你填回去"的工具) |
244
+
245
+ **两处有意的不对称,都写进注释与测试**:
246
+
247
+ 1. **生成严出、校验宽进**:我们只造 12/24 词(少一种长度 = 少一类"抄错还没发现"),
248
+ 但校验接受任何合法 BIP39 —— 用户拿来的旧纸密钥可能是 15/18/21 词的,
249
+ 那是**用户的钱**,不因为我们不喜欢这个长度就拒收
250
+ 2. **空白归一化**:BIP39 的种子是对**助记词字符串**做 PBKDF2,多一个空格就是另一份种子。
251
+ 用户从手机备忘录粘贴几乎一定带多余空白,不归一会造成"抄对了却打不开"这种最伤人的失败
252
+
253
+ ---
254
+
255
+ ## 8. 内存卫生(说清楚能做到什么)
256
+
257
+ ```ts
258
+ import { wipe, wipeRandom } from '@trustbaseai/account';
259
+ ```
260
+
261
+ **能做到**:本包自己分配的 `Uint8Array`(派生密钥、KDF 输入字节、DEK)在 `finally` 里清零;
262
+ WebCrypto 的密钥句柄以 `extractable: false` 导入(少一份可导出的副本);
263
+ **不提供任何把明文写进 `localStorage` / `sessionStorage` 的路径**(同构守卫也会拦这类 API)。
264
+
265
+ **做不到(不夸大)**:
266
+
267
+ - **JS 字符串不可清零**:口令与助记词都是不可变字符串,只能等 GC
268
+ - **引擎/运行时内部副本不可达**:WASM 线性内存里 Argon2id 的输入、WebCrypto 内部持有的
269
+ 密钥副本、V8 的 rope/cons 字符串中间态 —— 拿不到指针,清不了
270
+ - 调用方自己 `slice()` / 展开 / 转 string 产生的副本,也不在本函数的射程内
271
+
272
+ 所以 `wipe()` 的定位是**降低窗口**,不是保证抹除。真正的防线是短生命周期 + 不落明文 +
273
+ 严格 CSP 且不引第三方脚本(SDK 方案 §3.3.3 第 2 条)。
274
+
275
+ ---
276
+
277
+ ## 9. 未验证清单(别当已实现)
278
+
279
+ | 项 | 状态 |
280
+ |---|---|
281
+ | **WebAuthn PRF 真机路径**(`src/prf-webauthn.ts`) | ❌ **未在真机验证**。只覆盖了"没有 WebAuthn 时优雅降级"这一支;`create()`/`get()` 的成功路径必须在真机浏览器(iOS Safari 18+ / Android Chrome / 桌面 Chrome 116+)逐个验证:① 注册是否返回 `prf.enabled` ② 断言是否给 `results.first` ③ `allowCredentials` 是否要配 `evalByCredential` ④ 生物识别被拒/取消时的错误形态 |
282
+ | **`navigator.credentials` 之外的平台差异** | ❌ 未做 iOS/Android/桌面三端矩阵测试(无真机环境) |
283
+ | **浏览器里的 Argon2id 实测耗时** | ⚠️ 只在 Node 的 hash-wasm 上测过(~40 ms @ 19 MiB/t2)。手机浏览器(尤其低端机)会慢数倍,上生产前应实测并考虑调参 |
284
+ | **空闲锁定 / 二次解锁**(§3.3.2 的 15 分钟) | ❌ 不属本包:设备账户的"空闲即锁"在 `pwa-kit` 的会话层,商户侧的 15 分钟在守护进程 —— 本包只提供原语与 `wipe()` |
285
+ | **IndexedDB / 文件持久化** | ❌ 不属本包(只定义密文格式):`pwa-kit` 与守护进程各自落盘 |
286
+ | **配对(异机登录商户账户,§3.2.2)** | ❌ planned(需要 P2P 与守护进程,见 SDK 方案 §3.2.2) |
287
+ | **OS 凭据库集成**(`systemd-creds` / DPAPI / Keychain,§3.3.5) | ❌ planned(属守护进程 `trustbased`) |
288
+ | **ESM 产物** | ❌ 当前只有 CJS(与 `@trustbase/protocol` 一致,浏览器经打包器可用;动态 `import('hash-wasm')` 在 CJS 产物里会被打包器转成 `require`,仅延迟执行、不减少体积) |
289
+ | **账户面板 UI**(§3.2.4) | ❌ planned(`@trustbase/shop-ui` 的 AccountPanel) |
290
+
291
+ ### 关于交叉验证用的 cosmjs 版本
292
+
293
+ devDependency 用的是 `@cosmjs/*@0.36.x`(**全 CJS**),而不是仓库别处出现的 0.38.1:
294
+ 0.38 的 CJS 产物依赖 **ESM-only** 的 `@scure/base@2`,`jest` 在 Node 20/22 上无法
295
+ `require` 它(需要 Node ≥ 24.9 且开 `--experimental-vm-modules`),会让本包测试在本仓库
296
+ 声明的 `engines: node >= 20` 上直接红。0.36 的地址派生逻辑与 0.38 完全一致
297
+ (BIP39 → BIP32/SLIP-10 → secp256k1 → ripemd160(sha256) → bech32),交叉验证的意义不变。
298
+ 另外也用 0.38.1 在纯 Node 下复算过一遍,结论相同(记在 `test-vectors/address.golden.json`
299
+ 的 `verifiedAgainst` 字段里)。
300
+
301
+ ---
302
+
303
+ ## 10. 构建与测试
304
+
305
+ ```bash
306
+ npm run build # tsc → dist/(CommonJS + .d.ts)
307
+ npm test # jest(10 个套件 / 334 个用例)
308
+ npm run typecheck # tsc --noEmit
309
+ ```
310
+
311
+ 测试的分工(为什么这么分):
312
+
313
+ | 套件 | 守什么 |
314
+ |---|---|
315
+ | `test/address.test.ts` | 口径正确性:公开向量 + `node:crypto` 独立复算 + cosmjs 交叉验证 + HRP/大小写/长度边界 |
316
+ | `test/hd.test.ts` | 助记词→地址全链路与 cosmjs 一致(含随机助记词 5 轮) |
317
+ | `test/keystore.test.ts` | 往返、四类失败(口令错/密文改/头改/参数改)、OpenSSL 双向对照、参数下限、强度门 |
318
+ | `test/keystore-no-wasm.test.ts` | 无 WASM 时"创建退 PBKDF2、解锁拒绝"两条语义 |
319
+ | `test/prf.test.ts` | HKDF(RFC 5869 向量 + OpenSSL 对照)、包装/拆包、AAD 覆盖、假 provider |
320
+ | `test/envelope.test.ts` | **双轨解出同一份材料**、可后加/可撤销、失败路径 |
321
+ | `test/password.test.ts` | 弱口令必拒/强口令必过、同分不同理由、黑名单、长度上限 |
322
+ | `test/mnemonic.test.ts` | BIP39 官方向量(双向)、改一个词必失败、抄写门抽位与校验 |
323
+ | `test/isomorphic.test.ts` | 同构纪律(Node-only 与浏览器-only API 都拦)、依赖清单、禁用 `Math.random` |
324
+ | `test/constants-drift.test.ts` | 与 `@trustbase/protocol` 的 `BECH32_PREFIX` / `HD_PATH` 不漂移 |
325
+
326
+ 金标准向量在 `test-vectors/`(三个文件,每个都标了**来源**与"是外部权威还是本实现冻结")。
327
+
328
+ ---
329
+
330
+ ## 11. 相对设计文档的加固与取舍(供评审)
331
+
332
+ 设计文档是口径来源,下面这些是**实现层**的补充,写在这里免得评审时以为是偷偷改方案:
333
+
334
+ | # | 改动 | 为什么 | 是否影响兼容 |
335
+ |---|---|---|---|
336
+ | 1 | 明文头作为 GCM 的 AAD | 不认证参数 = KDF 参数可被改(§3.1) | 是**格式的一部分**,改它要 bump `KEYSTORE_VERSION` |
337
+ | 2 | 引入 DEK 信封(`envelope.ts`) | §3.3.5 的"PRF 包住 keystore 密钥"需要一份独立于口令的材料才成立 | 新增文件格式,不动 keystore v1 |
338
+ | 3 | 口令做 NFC 归一化 | 输入法差异会造成"输对了打不开";NFKC 才是真改口令,所以只用 NFC | 影响跨实现互通,必须一致 |
339
+ | 4 | 助记词空白归一化 | 同上(种子是对字符串做 PBKDF2) | 必须一致 |
340
+ | 5 | `createKeystore` 的 `minStrength` 是**可选**的 | 它是密码学原语,不该替调用方做产品策略;但 PWA 建户应当传 `3`(§3.3.6 的"低于则拒绝") | 否 |
341
+ | 6 | 黑名单短词(<5 拉丁字符)只做精确匹配,CJK 放宽到 2 字符 | 避免 `mytct123` 这类误伤;评分仍是主防线 | 否 |
342
+ | 7 | 解析时**拒绝未知字段** | v1 形状冻结;多出来的字段意味着这不是我们的密文 | 是(v1 内不允许加字段) |
343
+ | 8 | 不可信 KDF 参数有上限(内存 ≤1 GiB、迭代 ≤1000 万…) | 防"一个 blob 就是一次 DoS" | 否(远高于任何合理参数) |
344
+ | 9 | 密码学原语的错误一律可分支(见 §3.3 表) | 调用方要能区分"环境不支持"与"口令错",否则只能给用户含糊提示 | 否 |
345
+
346
+ ## 12. 尚未做(本包内的 planned)
347
+
348
+ - **多凭据 PRF**:当前 `prfTrack` 只存一条(一个凭据)。多设备/多凭据要扩成数组(格式 v2)
349
+ - **流式/大文件 keystore**:本包定位是"密钥库",密钥材料上限 256 字节(不当时通用加密工具)
350
+ - **口令强度实时输入反馈的 UI 组件**:本包只给数据(`score` / `reasons`),组件在 `shop-ui`
351
+ - **`extraBlocklist` 的字典加载器**(从文件/网络批量导入商户自定义黑名单):接口已留,加载器未做
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 账户地址派生 —— **本包最不能写错的一段**。
3
+ *
4
+ * ## 口径(唯一,来自入口文档 §8.3.2 ①② 与 SDK 方案 §3.2.3)
5
+ *
6
+ * ```
7
+ * address = bech32( hrp='tct', ripemd160( sha256( compressed_secp256k1_pubkey ) ) )
8
+ * └────────────── 20 字节 payload ──────────────┘
9
+ * ```
10
+ *
11
+ * 三个"必须一致"的点,错一个都会在链上静默失败:
12
+ *
13
+ * | # | 要点 | 错了会怎样 |
14
+ * |---|---|---|
15
+ * | 1 | 公钥必须是**压缩**形态(33 字节) | 用非压缩公钥派生 → 得出另一个地址:能收款的地址和签名者地址不是同一个 |
16
+ * | 2 | 顺序是 `ripemd160(sha256(pubkey))`,不是反过来、也不是只 sha256 | 地址不是链上认的账户,`AccAddressFromBech32` 过不了或收款无人可退 |
17
+ * | 3 | 编码是 **bech32**(BIP-173),HRP = `tct` | bech32m / 别的 HRP → 别的钱包解不开、节点拒收 |
18
+ *
19
+ * 出处证据(链侧):`x/order/keeper/msg_server.go` 的 escrow 分支就是
20
+ * `AccAddressFromBech32(buyer)` 从 buyer 扣款,退款/结算按 `order.Buyer` 打款
21
+ * —— 所以"买家标识"必须是**合法账户地址**,不能是任意字符串(入口文档 §8.3.2 ①)。
22
+ *
23
+ * ## 与链一致性的验证方式
24
+ *
25
+ * 单测里既跑公开向量(BIP-173 / 比特币"私钥=1"的 hash160),也跑与 `@cosmjs/*`
26
+ * 的**逐字符交叉验证**(随机助记词派生 + 固定助记词派生),两套独立实现必须
27
+ * 给出同一个字符串。cosmjs 只作 devDependency,不出现在 `src/` 里。
28
+ */
29
+ /**
30
+ * 账户地址的 HRP。与 `@trustbase/protocol` 的 `BECH32_PREFIX` 同值
31
+ * (漂移由 `test/constants-drift.test.ts` 静态核对,避免"一个数据两个来源")。
32
+ */
33
+ export declare const ACCOUNT_ADDRESS_HRP = "tct";
34
+ /** 账户地址 payload 长度(Cosmos 惯例:20 字节 = ripemd160 输出) */
35
+ export declare const ACCOUNT_ADDRESS_BYTES = 20;
36
+ /** 20 字节地址 payload = `ripemd160(sha256(compressed_pubkey))` */
37
+ export declare function accountAddressBytes(publicKey: Uint8Array): Uint8Array;
38
+ /** 公钥(33B 压缩或 65B 非压缩)→ `tct1…` 地址 */
39
+ export declare function accountAddressFromPublicKey(publicKey: Uint8Array, hrp?: string): string;
40
+ /** 私钥 → `tct1…` 地址(内部先压缩公钥,调用方不需要关心形态) */
41
+ export declare function accountAddressFromPrivateKey(privateKey: Uint8Array, hrp?: string): string;
42
+ export interface ParsedAccountAddress {
43
+ /** 归一为小写的 HRP */
44
+ hrp: string;
45
+ /** 20 字节 payload */
46
+ bytes: Uint8Array;
47
+ /** 归一为小写的完整地址字符串(大写输入在这里被规整回来,便于做等值比对) */
48
+ address: string;
49
+ }
50
+ /**
51
+ * 解析账户地址:校验 bech32 校验和、HRP、payload 长度。
52
+ *
53
+ * 任何不合法都抛错(`INVALID_BLOB` 类)。需要"只问是不是"的场景用
54
+ * `isAccountAddress()` —— 它不抛。
55
+ */
56
+ export declare function parseAccountAddress(value: string, hrp?: string): ParsedAccountAddress;
57
+ /**
58
+ * 是不是一个合法的账户地址(不抛异常,适合校验用户输入/链上返回值)。
59
+ *
60
+ * 注意:`address === parseAccountAddress(address).address` 这条额外要求意味着
61
+ * **全大写写法也算合法**(bech32 允许),但比较两个地址是否相等时应当先归一化:
62
+ * 用 `normalizeAccountAddress()`。
63
+ */
64
+ export declare function isAccountAddress(value: unknown, hrp?: string): value is string;
65
+ /**
66
+ * 归一化账户地址(大小写 → 小写 + 重新编码),非法则抛错。
67
+ * 地址相等比较一律走这里,别用 `===` 比原始输入。
68
+ */
69
+ export declare function normalizeAccountAddress(value: string, hrp?: string): string;
70
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC,sDAAsD;AACtD,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,+DAA+D;AAC/D,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAErE;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,GAAE,MAA4B,GAAG,MAAM,CAE5G;AAED,0CAA0C;AAC1C,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,GAAE,MAA4B,GAAG,MAAM,CAE9G;AAED,MAAM,WAAW,oBAAoB;IACnC,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,KAAK,EAAE,UAAU,CAAC;IAClB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAA4B,GAAG,oBAAoB,CAa1G;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,GAAE,MAA4B,GAAG,KAAK,IAAI,MAAM,CAQnG;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAA4B,GAAG,MAAM,CAEhG"}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ /**
3
+ * 账户地址派生 —— **本包最不能写错的一段**。
4
+ *
5
+ * ## 口径(唯一,来自入口文档 §8.3.2 ①② 与 SDK 方案 §3.2.3)
6
+ *
7
+ * ```
8
+ * address = bech32( hrp='tct', ripemd160( sha256( compressed_secp256k1_pubkey ) ) )
9
+ * └────────────── 20 字节 payload ──────────────┘
10
+ * ```
11
+ *
12
+ * 三个"必须一致"的点,错一个都会在链上静默失败:
13
+ *
14
+ * | # | 要点 | 错了会怎样 |
15
+ * |---|---|---|
16
+ * | 1 | 公钥必须是**压缩**形态(33 字节) | 用非压缩公钥派生 → 得出另一个地址:能收款的地址和签名者地址不是同一个 |
17
+ * | 2 | 顺序是 `ripemd160(sha256(pubkey))`,不是反过来、也不是只 sha256 | 地址不是链上认的账户,`AccAddressFromBech32` 过不了或收款无人可退 |
18
+ * | 3 | 编码是 **bech32**(BIP-173),HRP = `tct` | bech32m / 别的 HRP → 别的钱包解不开、节点拒收 |
19
+ *
20
+ * 出处证据(链侧):`x/order/keeper/msg_server.go` 的 escrow 分支就是
21
+ * `AccAddressFromBech32(buyer)` 从 buyer 扣款,退款/结算按 `order.Buyer` 打款
22
+ * —— 所以"买家标识"必须是**合法账户地址**,不能是任意字符串(入口文档 §8.3.2 ①)。
23
+ *
24
+ * ## 与链一致性的验证方式
25
+ *
26
+ * 单测里既跑公开向量(BIP-173 / 比特币"私钥=1"的 hash160),也跑与 `@cosmjs/*`
27
+ * 的**逐字符交叉验证**(随机助记词派生 + 固定助记词派生),两套独立实现必须
28
+ * 给出同一个字符串。cosmjs 只作 devDependency,不出现在 `src/` 里。
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.ACCOUNT_ADDRESS_BYTES = exports.ACCOUNT_ADDRESS_HRP = void 0;
32
+ exports.accountAddressBytes = accountAddressBytes;
33
+ exports.accountAddressFromPublicKey = accountAddressFromPublicKey;
34
+ exports.accountAddressFromPrivateKey = accountAddressFromPrivateKey;
35
+ exports.parseAccountAddress = parseAccountAddress;
36
+ exports.isAccountAddress = isAccountAddress;
37
+ exports.normalizeAccountAddress = normalizeAccountAddress;
38
+ const ripemd160_1 = require("@noble/hashes/ripemd160");
39
+ const sha256_1 = require("@noble/hashes/sha256");
40
+ const bech32_1 = require("./bech32");
41
+ const errors_1 = require("./errors");
42
+ const keys_1 = require("./keys");
43
+ /**
44
+ * 账户地址的 HRP。与 `@trustbase/protocol` 的 `BECH32_PREFIX` 同值
45
+ * (漂移由 `test/constants-drift.test.ts` 静态核对,避免"一个数据两个来源")。
46
+ */
47
+ exports.ACCOUNT_ADDRESS_HRP = 'tct';
48
+ /** 账户地址 payload 长度(Cosmos 惯例:20 字节 = ripemd160 输出) */
49
+ exports.ACCOUNT_ADDRESS_BYTES = 20;
50
+ /** 20 字节地址 payload = `ripemd160(sha256(compressed_pubkey))` */
51
+ function accountAddressBytes(publicKey) {
52
+ return (0, ripemd160_1.ripemd160)((0, sha256_1.sha256)((0, keys_1.compressPublicKey)(publicKey)));
53
+ }
54
+ /** 公钥(33B 压缩或 65B 非压缩)→ `tct1…` 地址 */
55
+ function accountAddressFromPublicKey(publicKey, hrp = exports.ACCOUNT_ADDRESS_HRP) {
56
+ return (0, bech32_1.encodeBech32Address)(hrp, accountAddressBytes(publicKey));
57
+ }
58
+ /** 私钥 → `tct1…` 地址(内部先压缩公钥,调用方不需要关心形态) */
59
+ function accountAddressFromPrivateKey(privateKey, hrp = exports.ACCOUNT_ADDRESS_HRP) {
60
+ return accountAddressFromPublicKey((0, keys_1.publicKeyFromPrivateKey)(privateKey), hrp);
61
+ }
62
+ /**
63
+ * 解析账户地址:校验 bech32 校验和、HRP、payload 长度。
64
+ *
65
+ * 任何不合法都抛错(`INVALID_BLOB` 类)。需要"只问是不是"的场景用
66
+ * `isAccountAddress()` —— 它不抛。
67
+ */
68
+ function parseAccountAddress(value, hrp = exports.ACCOUNT_ADDRESS_HRP) {
69
+ const decoded = (0, bech32_1.decodeBech32Address)(value, hrp);
70
+ if (decoded.payload.length !== exports.ACCOUNT_ADDRESS_BYTES) {
71
+ throw new errors_1.AccountError('INVALID_BLOB', `账户地址的 payload 必须是 ${exports.ACCOUNT_ADDRESS_BYTES} 字节(实际 ${decoded.payload.length})`);
72
+ }
73
+ return {
74
+ hrp: decoded.hrp,
75
+ bytes: decoded.payload,
76
+ address: (0, bech32_1.encodeBech32Address)(decoded.hrp, decoded.payload),
77
+ };
78
+ }
79
+ /**
80
+ * 是不是一个合法的账户地址(不抛异常,适合校验用户输入/链上返回值)。
81
+ *
82
+ * 注意:`address === parseAccountAddress(address).address` 这条额外要求意味着
83
+ * **全大写写法也算合法**(bech32 允许),但比较两个地址是否相等时应当先归一化:
84
+ * 用 `normalizeAccountAddress()`。
85
+ */
86
+ function isAccountAddress(value, hrp = exports.ACCOUNT_ADDRESS_HRP) {
87
+ if (typeof value !== 'string')
88
+ return false;
89
+ try {
90
+ parseAccountAddress(value, hrp);
91
+ return true;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * 归一化账户地址(大小写 → 小写 + 重新编码),非法则抛错。
99
+ * 地址相等比较一律走这里,别用 `===` 比原始输入。
100
+ */
101
+ function normalizeAccountAddress(value, hrp = exports.ACCOUNT_ADDRESS_HRP) {
102
+ return parseAccountAddress(value, hrp).address;
103
+ }
104
+ //# sourceMappingURL=address.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAmBH,kDAEC;AAGD,kEAEC;AAGD,oEAEC;AAiBD,kDAaC;AASD,4CAQC;AAMD,0DAEC;AApFD,uDAAoD;AACpD,iDAA8C;AAE9C,qCAAoE;AACpE,qCAAwC;AACxC,iCAAoE;AAEpE;;;GAGG;AACU,QAAA,mBAAmB,GAAG,KAAK,CAAC;AAEzC,sDAAsD;AACzC,QAAA,qBAAqB,GAAG,EAAE,CAAC;AAExC,+DAA+D;AAC/D,SAAgB,mBAAmB,CAAC,SAAqB;IACvD,OAAO,IAAA,qBAAS,EAAC,IAAA,eAAM,EAAC,IAAA,wBAAiB,EAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,sCAAsC;AACtC,SAAgB,2BAA2B,CAAC,SAAqB,EAAE,MAAc,2BAAmB;IAClG,OAAO,IAAA,4BAAmB,EAAC,GAAG,EAAE,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,0CAA0C;AAC1C,SAAgB,4BAA4B,CAAC,UAAsB,EAAE,MAAc,2BAAmB;IACpG,OAAO,2BAA2B,CAAC,IAAA,8BAAuB,EAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC;AAC/E,CAAC;AAWD;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,KAAa,EAAE,MAAc,2BAAmB;IAClF,MAAM,OAAO,GAAG,IAAA,4BAAmB,EAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChD,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,6BAAqB,EAAE,CAAC;QACrD,MAAM,IAAI,qBAAY,CACpB,cAAc,EACd,qBAAqB,6BAAqB,UAAU,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAC9E,CAAC;IACJ,CAAC;IACD,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,OAAO,CAAC,OAAO;QACtB,OAAO,EAAE,IAAA,4BAAmB,EAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC;KAC3D,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,KAAc,EAAE,MAAc,2BAAmB;IAChF,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,CAAC;QACH,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,uBAAuB,CAAC,KAAa,EAAE,MAAc,2BAAmB;IACtF,OAAO,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC;AACjD,CAAC"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * bech32(BIP-173)纯 JS 实现 —— 只做编码/解码,不做 bech32m。
3
+ *
4
+ * ## 为什么自己写
5
+ *
6
+ * 地址口径是**红线**(入口文档 §8.3.2 ①②:「买家标识必须是合法账户地址」),
7
+ * 而 bech32 的输入只有 20 字节 payload + 一个 HRP。自己实现的代码量约 80 行,
8
+ * 换来的是:零 Node 依赖(浏览器/Node 同一份)、零打包体积负担、以及
9
+ * **可以被公开向量与 cosmjs 双侧对着测**。反过来引一个通用 bech32 库,
10
+ * 它默认可能给的是 bech32m、可能对大小写做沉默归一 —— 这两件事都会让
11
+ * "派生出的地址链上不认",而症状只在转账时才出现。
12
+ *
13
+ * ## 与 BIP-173 的取舍(照抄标准,不做宽容)
14
+ *
15
+ * | 规则 | 本实现 |
16
+ * |---|---|
17
+ * | 校验和常量 | `1`(bech32)。**不用 bech32m 的 `0x2bc830a3`** —— Cosmos 账户地址是 bech32 |
18
+ * | 大小写 | 全小写或全大写都收(解码后归一为小写),**混用直接拒**(BIP-173 明确禁止) |
19
+ * | 长度 | 总长 ≤ 90 字符(BIP-173 的硬上限) |
20
+ * | 数据部分 | 只能是 32 个字符集内的字符;`1` 是分隔符,不出现在数据里 |
21
+ * | 填充位 | `convertBits(..., pad=false)` 时校验剩余位必须为 0(否则是"非规范编码",拒) |
22
+ *
23
+ * 参考实现出处:BIP-173 附录的 C 参考代码(Sipa 的 `ref/python/segwit_addr.py`
24
+ * 与 `ref/c/segwit_addr.c`,均为 MIT/公有领域),逻辑等价、改写为 TS +
25
+ * `Uint8Array`,并把"非法输入直接抛错"改成显式错误码。
26
+ *
27
+ * ## 来源与真源
28
+ *
29
+ * HRP 的取值以 `@trustbase/protocol` 的 `BECH32_PREFIX` 为准(本包同值声明,
30
+ * 漂移由 `test/constants-drift.test.ts` 静态守着)。
31
+ */
32
+ /** BIP-173 的总长度上限(含 HRP、分隔符、数据、校验和) */
33
+ export declare const BECH32_MAX_LENGTH = 90;
34
+ /** bech32 数据字符集(32 个字符,索引即 5 bit 值) */
35
+ export declare const BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
36
+ /** BIP-173 的 checksum 多项式求值(输入是 5 bit 值序列) */
37
+ export declare function bech32Polymod(values: readonly number[]): number;
38
+ /** HRP 展开:每个字符取高 3 bit,插入 0,再取低 5 bit */
39
+ export declare function bech32HrpExpand(hrp: string): number[];
40
+ /**
41
+ * bech32 编码(BIP-173)。`data` 是 5 bit 值序列(每项 0..31)。
42
+ *
43
+ * 输入是大写 HRP 或含非法值时抛 `INVALID_ARGUMENT`。输出恒为小写。
44
+ */
45
+ export declare function bech32Encode(hrp: string, data: readonly number[]): string;
46
+ export interface Bech32Decoded {
47
+ /** 已归一为小写的人类可读前缀 */
48
+ hrp: string;
49
+ /** 数据部分(不含校验和),每项 0..31 */
50
+ data: number[];
51
+ }
52
+ /**
53
+ * bech32 解码。校验和不匹配 / 大小写混用 / 超长 / 缺分隔符 / 数据含非法字符
54
+ * 一律抛 `INVALID_BLOB`(这是"外来字符串不合法"的类别,与"编程错误"分开)。
55
+ */
56
+ export declare function bech32Decode(value: string): Bech32Decoded;
57
+ /**
58
+ * 8 bit ↔ 5 bit 重新分组(BIP-173 `convertbits`)。
59
+ *
60
+ * `pad=true` 时末组左移补零(编码方向);`pad=false` 时要求末组不使用填充位
61
+ * 且剩余位为 0,否则抛错(解码方向)—— 放宽这条会让同一段 payload 有多种
62
+ * 等价编码,等于给地址留了别名。
63
+ */
64
+ export declare function convertBits(data: Uint8Array | readonly number[], from: number, to: number, pad: boolean): number[];
65
+ /**
66
+ * 账户地址编码:`hrp1<8→5 bit 重分组><校验和>`。
67
+ * 不做长度假设(20 字节是账户地址的约定,由 address.ts 把关)。
68
+ */
69
+ export declare function encodeBech32Address(hrp: string, payload: Uint8Array): string;
70
+ /**
71
+ * 账户地址解码:返回 HRP 与原始字节 payload。
72
+ * 若给了 `expectedHrp`,HRP 不匹配时抛错(大小写已归一,故不会被大写骗过)。
73
+ */
74
+ export declare function decodeBech32Address(value: string, expectedHrp?: string): {
75
+ hrp: string;
76
+ payload: Uint8Array;
77
+ };
78
+ //# sourceMappingURL=bech32.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bech32.d.ts","sourceRoot":"","sources":["../src/bech32.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,uCAAuC;AACvC,eAAO,MAAM,cAAc,qCAAqC,CAAC;AAcjE,8CAA8C;AAC9C,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAY/D;AAED,yCAAyC;AACzC,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAUrD;AA+BD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAkBzE;AAED,MAAM,WAAW,aAAa;IAC5B,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,2BAA2B;IAC3B,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CA2CzD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAuBlH;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,CAK5E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAe7G"}