@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
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * BIP39 助记词(纸密钥)与"抄写门"。
4
+ *
5
+ * ## 定位(SDK 方案 §3.3.3 第 1 条,必须写进产品文案)
6
+ *
7
+ * **助记词是唯一的"忘记密码"退路。** 我们不掌握任何解密材料,没有找回密码
8
+ * 服务,也没有云端解密备份 —— 这不是能力不足,而是设计:一旦我们能解密,
9
+ * 我们就成了资金保管方。所以"创建账户必须真的抄一遍助记词"(不是打个勾),
10
+ * 本文件提供抄写门需要的两个原语。
11
+ *
12
+ * ## 生成严出、校验宽进(有意的不对称)
13
+ *
14
+ * | 方向 | 口径 | 理由 |
15
+ * |---|---|---|
16
+ * | 生成 | 只允许 **128 / 256 bit**(12 / 24 词) | 我们只造这两种,少一种长度就少一类"用户抄错还没发现"的可能 |
17
+ * | 校验 | 接受任何合法 BIP39(12..24 词、词表内、校验和对) | 用户拿来的旧纸密钥可能是 15/18/21 词的。这里是**用户的钱**,不因为我们不喜欢这个长度就拒收 |
18
+ *
19
+ * 助记词派生(BIP39 种子 → BIP32 → secp256k1 → 地址)在 `hd.ts`。
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.DEFAULT_MNEMONIC_STRENGTH = exports.MNEMONIC_STRENGTHS = void 0;
23
+ exports.generateMnemonic = generateMnemonic;
24
+ exports.normalizeMnemonic = normalizeMnemonic;
25
+ exports.validateMnemonic = validateMnemonic;
26
+ exports.mnemonicToEntropy = mnemonicToEntropy;
27
+ exports.entropyToMnemonic = entropyToMnemonic;
28
+ exports.splitMnemonicWords = splitMnemonicWords;
29
+ exports.pickTranscriptionIndices = pickTranscriptionIndices;
30
+ exports.checkTranscription = checkTranscription;
31
+ const bip39_1 = require("@scure/bip39");
32
+ const bip39_2 = require("@scure/bip39");
33
+ const bip39_3 = require("@scure/bip39");
34
+ const english_1 = require("@scure/bip39/wordlists/english");
35
+ const errors_1 = require("./errors");
36
+ const webcrypto_1 = require("./webcrypto");
37
+ /** 我们**生成**的助记词强度(bit) */
38
+ exports.MNEMONIC_STRENGTHS = [128, 256];
39
+ /** 默认强度:128 bit(12 词)—— 手机上手抄得完 */
40
+ exports.DEFAULT_MNEMONIC_STRENGTH = 128;
41
+ /** 生成新助记词(默认 12 词)。随机源只用 `crypto.getRandomValues`(见 webcrypto.ts)。 */
42
+ function generateMnemonic(strength = exports.DEFAULT_MNEMONIC_STRENGTH, rng = webcrypto_1.randomBytes) {
43
+ if (strength !== 128 && strength !== 256) {
44
+ throw new errors_1.AccountError('INVALID_ARGUMENT', `只生成长度 ${exports.MNEMONIC_STRENGTHS.join(' / ')} bit 的助记词(12 / 24 词)`);
45
+ }
46
+ const entropy = rng(strength / 8);
47
+ if (!(entropy instanceof Uint8Array) || entropy.length !== strength / 8) {
48
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '随机源返回的字节数不对');
49
+ }
50
+ // 自己取熵、再交给 BIP39 编码(而不是用库内部的随机源):随机源只有一个入口,
51
+ // 测试也能注入确定性替身,跑出可复现的助记词
52
+ return (0, bip39_1.entropyToMnemonic)(entropy, english_1.wordlist);
53
+ }
54
+ /**
55
+ * 归一化助记词文本:去掉首尾空白、把**任意空白**(多空格/换行/制表符)折成
56
+ * 单个空格、再做 NFKD 归一(BIP39 规定)。
57
+ *
58
+ * 为什么必须做:BIP39 的种子是 `PBKDF2(助记词字符串)` —— 多一个空格就是
59
+ * **另一份种子、另一个账户**。用户从手机备忘录/邮件里粘贴时几乎一定带多余空白,
60
+ * 于是会出现"抄对了却打不开"这种最伤人的失败。归一化把这个失败模式从
61
+ * "用户的问题"变成"我们处理掉的问题"。
62
+ *
63
+ * 空白以外的字符**不**做宽容处理(不做大小写折叠、不做同义词):那是真的抄错了。
64
+ *
65
+ * @throws AccountError 输入为空(没有任何词)时
66
+ */
67
+ function normalizeMnemonic(mnemonic) {
68
+ return splitMnemonicWords(mnemonic).join(' ').normalize('NFKD');
69
+ }
70
+ /**
71
+ * 是不是合法助记词(词表 + 校验和 + 词数)。
72
+ * 宽进:12/15/18/21/24 词都接受(见文件头);空白先归一化(见 `normalizeMnemonic`)。
73
+ */
74
+ function validateMnemonic(mnemonic) {
75
+ if (typeof mnemonic !== 'string')
76
+ return false;
77
+ try {
78
+ return (0, bip39_3.validateMnemonic)(normalizeMnemonic(mnemonic), english_1.wordlist);
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** 助记词 → 熵(字节)。非法助记词抛 `INVALID_MNEMONIC` */
85
+ function mnemonicToEntropy(mnemonic) {
86
+ if (!validateMnemonic(mnemonic)) {
87
+ throw new errors_1.AccountError('INVALID_MNEMONIC', '助记词不合法(词不在 BIP39 英文词表内,或校验和不对 —— 通常意味着某个词抄错了)');
88
+ }
89
+ return (0, bip39_2.mnemonicToEntropy)(normalizeMnemonic(mnemonic), english_1.wordlist);
90
+ }
91
+ /** 熵 → 助记词。只接受 16 字节(128 bit)与 32 字节(256 bit) */
92
+ function entropyToMnemonic(entropy) {
93
+ if (!(entropy instanceof Uint8Array) || (entropy.length !== 16 && entropy.length !== 32)) {
94
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '熵必须是 16 字节(128 bit)或 32 字节(256 bit)');
95
+ }
96
+ return (0, bip39_1.entropyToMnemonic)(entropy, english_1.wordlist);
97
+ }
98
+ /** 把助记词切成词数组(按任意空白切;不做大小写/字符归一 —— 那是抄错,不是格式问题) */
99
+ function splitMnemonicWords(mnemonic) {
100
+ if (typeof mnemonic !== 'string') {
101
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '助记词必须是字符串');
102
+ }
103
+ const words = mnemonic
104
+ .trim()
105
+ .split(/\s+/u)
106
+ .filter((word) => word.length > 0);
107
+ if (words.length === 0) {
108
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '助记词为空');
109
+ }
110
+ return words;
111
+ }
112
+ function toWords(input) {
113
+ return typeof input === 'string' ? splitMnemonicWords(input) : [...input];
114
+ }
115
+ /**
116
+ * 无偏地取一个 `[0, limit)` 的随机下标。
117
+ *
118
+ * 用**拒绝采样**而不是 `byte % limit`:后者在 limit 不整除 256 时会让前几个
119
+ * 下标出现得更频繁(模偏差)。抽词位置这种"看起来无所谓"的地方正是模偏差
120
+ * 最爱藏身之处,而且它会让"随机抽 3 个词"变成"更爱抽前几个词"。
121
+ */
122
+ function randomIndexBelow(limit, rng) {
123
+ const bound = 256 - (256 % limit);
124
+ for (let attempt = 0; attempt < 1000; attempt++) {
125
+ const byte = rng(1)[0];
126
+ if (byte === undefined)
127
+ break;
128
+ if (byte < bound)
129
+ return byte % limit;
130
+ }
131
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '随机源异常(拒绝采样连续失败)—— 检查 rng 实现');
132
+ }
133
+ /**
134
+ * 抄写门:从助记词里随机抽 `count` 个位置,让用户回填。
135
+ *
136
+ * 返回**升序去重的 0 基下标**(数组下标语义,`words[i]` 直接可用)。
137
+ * UI 要显示"第几个词"时用 `index + 1`(人类从 1 数)—— 这个转换留在调用方,
138
+ * 因为它是文案问题,不是密码学问题。
139
+ */
140
+ function pickTranscriptionIndices(input, count, rng = webcrypto_1.randomBytes) {
141
+ const words = toWords(input);
142
+ if (!Number.isInteger(count) || count < 1 || count > words.length) {
143
+ throw new errors_1.AccountError('INVALID_ARGUMENT', `抽词数量必须是 1..${words.length} 的整数(收到 ${String(count)})`);
144
+ }
145
+ const picked = new Set();
146
+ while (picked.size < count) {
147
+ picked.add(randomIndexBelow(words.length, rng));
148
+ }
149
+ return [...picked].sort((a, b) => a - b);
150
+ }
151
+ /** 比对用的归一化:去空白 + 小写 + NFC(英文词表全小写,这样能容忍手抄的大小写与空格) */
152
+ function normalizeWord(value) {
153
+ return value.normalize('NFC').trim().toLowerCase();
154
+ }
155
+ /**
156
+ * 校验抄写结果。
157
+ *
158
+ * `answers` 既接受 `{index, word}[]`,也接受 `{ [index]: word }`(表单更顺手)。
159
+ * **不返回正确答案**(`failed`/`missing` 只给位置)—— 否则这个 API 就成了
160
+ * 一个"帮你把助记词填回去"的工具,抄写门也就白设了。
161
+ */
162
+ function checkTranscription(input, indices, answers) {
163
+ const words = toWords(input);
164
+ const answerMap = new Map();
165
+ if (Array.isArray(answers)) {
166
+ for (const answer of answers) {
167
+ if (!answer || !Number.isInteger(answer.index)) {
168
+ throw new errors_1.AccountError('INVALID_ARGUMENT', '答案的 index 必须是整数');
169
+ }
170
+ answerMap.set(answer.index, String(answer.word ?? ''));
171
+ }
172
+ }
173
+ else {
174
+ for (const [key, value] of Object.entries(answers)) {
175
+ const index = Number(key);
176
+ if (!Number.isInteger(index)) {
177
+ throw new errors_1.AccountError('INVALID_ARGUMENT', `答案的键必须是下标(收到 "${key}")`);
178
+ }
179
+ answerMap.set(index, String(value ?? ''));
180
+ }
181
+ }
182
+ const failed = [];
183
+ const missing = [];
184
+ for (const index of [...new Set(indices)].sort((a, b) => a - b)) {
185
+ if (!Number.isInteger(index) || index < 0 || index >= words.length) {
186
+ throw new errors_1.AccountError('INVALID_ARGUMENT', `下标越界:${String(index)}`);
187
+ }
188
+ const answer = answerMap.get(index);
189
+ if (answer === undefined || normalizeWord(answer).length === 0) {
190
+ missing.push(index);
191
+ continue;
192
+ }
193
+ if (normalizeWord(answer) !== normalizeWord(words[index] ?? '')) {
194
+ failed.push(index);
195
+ }
196
+ }
197
+ return { ok: failed.length === 0 && missing.length === 0, failed, missing };
198
+ }
199
+ //# sourceMappingURL=mnemonic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mnemonic.js","sourceRoot":"","sources":["../src/mnemonic.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 口令强度估算(SDK 方案 §3.3.6)—— **用估算,不用复杂度规则**。
3
+ *
4
+ * ## 为什么不搞"必须含大小写数字符号"
5
+ *
6
+ * 规则只会逼出 `Password1!` 这种"看着合规、实际上榜"的口令:它满足所有规则,
7
+ * 却是攻击者字典里的第一批。真正决定成本的是**可猜空间**,只有估算器能看到
8
+ * 这一点("这是常见口令"、"这是键盘序列"、"这像是日期")。
9
+ *
10
+ * ## 三件事的边界
11
+ *
12
+ * | 事 | 本模块怎么做 |
13
+ * |---|---|
14
+ * | 强度估算 | `zxcvbn-ts` 评分 0..4 + 我们翻译成中文的理由(库默认返回 key,如 `topTen`;我们不引它的英文文案包,见下) |
15
+ * | 通过线 | 默认 `score >= 3`(SDK 方案原文;约 50+ bits 熵量级)**且**不在本地黑名单 |
16
+ * | 黑名单 | **纯本地、不联网**:复用 zxcvbn 自带的常见口令字典(`@zxcvbn-ts/language-common` 的 `passwords-common` 49233 条 + `diceware-common` 7776 条)+ 一份小的应用相关词表(`APP_BLOCKLIST`),并支持 `extraBlocklist` 扩充 |
17
+ *
18
+ * **不做** HIBP 之类的联网查询:那是外部依赖,还会把口令哈希前缀发出去
19
+ * (SDK 方案 §3.3.6 明确排除)。
20
+ *
21
+ * ## 两个实现细节(都是坑)
22
+ *
23
+ * 1. **`userInputs` 里绝不能塞口令本身**。zxcvbn 会把 userInputs 当字典匹配,
24
+ * 如果口令自己进了字典,任何口令都会被判 0 分("自证"式的假阴性)。
25
+ * 这里只塞黑名单词与应用相关词。
26
+ * 2. **超长口令不估算**。`tooLong` 直接不可接受并短路返回:估算器本身是
27
+ * "把口令喂给一堆正则与字典",超过上限的输入没有估算价值(上限 128 是
28
+ * SDK 方案 §3.3.6 的防 DoS 条款)。
29
+ */
30
+ /** 通过线:评分 ≥ 3(SDK 方案 §3.3.6) */
31
+ export declare const ACCEPTABLE_SCORE = 3;
32
+ /** 评分类型(zxcvbn 的 0..4) */
33
+ export type PasswordScore = 0 | 1 | 2 | 3 | 4;
34
+ /**
35
+ * 应用相关词表("黑名单"的第二半)。
36
+ *
37
+ * 只放**公开的产品/域名/币种词**与占位示例域名 —— 这个仓库将来要开源,
38
+ * 不得出现真实内网地址、真实商户域名、任何密钥材料。
39
+ * 中文产品词也放几条:用户拿"私钥""助记词"当口令是真实存在的。
40
+ */
41
+ export declare const APP_BLOCKLIST: readonly string[];
42
+ export interface PasswordStrength {
43
+ /** zxcvbn 评分:0(极弱).. 4(强) */
44
+ score: PasswordScore;
45
+ /** 中文理由(可展示给用户;去重保序,最强的那条在最前) */
46
+ reasons: string[];
47
+ /** 是否可接受 = 评分达标 && 未命中黑名单 && 未超长 */
48
+ acceptable: boolean;
49
+ /** 估算的可猜空间(log10 次猜测);超长短路时为 null */
50
+ guessesLog10: number | null;
51
+ /** 命中本地黑名单(应用词表 / `extraBlocklist` 的精确或包含命中) */
52
+ blocklisted: boolean;
53
+ /** 超长(> 上限):没有做估算,直接不可接受 */
54
+ tooLong: boolean;
55
+ }
56
+ export interface PasswordStrengthOptions {
57
+ /** 追加黑名单(精确命中或长度达标时的包含命中都会被判黑) */
58
+ extraBlocklist?: readonly string[];
59
+ /** 额外"用户输入"(用户名/邮箱/手机号等)—— 传给 zxcvbn 当字典,**不要传口令自身** */
60
+ userInputs?: readonly string[];
61
+ /** 通过线,默认 3 */
62
+ acceptableScore?: number;
63
+ /** 长度上限,默认 128 */
64
+ maxLength?: number;
65
+ }
66
+ /**
67
+ * 是否命中本地黑名单:**精确命中**(归一化后相等)或**包含命中**
68
+ * (黑名单词达到 {@link containmentThreshold} 且被归一化口令包含,
69
+ * 如 `trustbase2026`、`Trust-Base-2026`、`私钥123`)。
70
+ */
71
+ export declare function isBlocklisted(password: string, extraBlocklist?: readonly string[]): boolean;
72
+ /**
73
+ * 估算口令强度(SDK 方案 §3.3.6)。
74
+ *
75
+ * 不做任何"必须含 X"的规则;`acceptable` 只看三件事:评分 ≥ 通过线、
76
+ * 不在黑名单、长度在 `1..maxLength` 之内。
77
+ */
78
+ export declare function estimatePasswordStrength(password: string, options?: PasswordStrengthOptions): PasswordStrength;
79
+ //# sourceMappingURL=password.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAOH,gCAAgC;AAChC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,0BAA0B;AAC1B,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE9C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,MAAM,EAe1C,CAAC;AAgBF,MAAM,WAAW,gBAAgB;IAC/B,6BAA6B;IAC7B,KAAK,EAAE,aAAa,CAAC;IACrB,iCAAiC;IACjC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oCAAoC;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gDAAgD;IAChD,WAAW,EAAE,OAAO,CAAC;IACrB,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,wDAAwD;IACxD,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,eAAe;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmFD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,GAAE,SAAS,MAAM,EAAO,GAAG,OAAO,CAW/F;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CA6DlB"}
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ /**
3
+ * 口令强度估算(SDK 方案 §3.3.6)—— **用估算,不用复杂度规则**。
4
+ *
5
+ * ## 为什么不搞"必须含大小写数字符号"
6
+ *
7
+ * 规则只会逼出 `Password1!` 这种"看着合规、实际上榜"的口令:它满足所有规则,
8
+ * 却是攻击者字典里的第一批。真正决定成本的是**可猜空间**,只有估算器能看到
9
+ * 这一点("这是常见口令"、"这是键盘序列"、"这像是日期")。
10
+ *
11
+ * ## 三件事的边界
12
+ *
13
+ * | 事 | 本模块怎么做 |
14
+ * |---|---|
15
+ * | 强度估算 | `zxcvbn-ts` 评分 0..4 + 我们翻译成中文的理由(库默认返回 key,如 `topTen`;我们不引它的英文文案包,见下) |
16
+ * | 通过线 | 默认 `score >= 3`(SDK 方案原文;约 50+ bits 熵量级)**且**不在本地黑名单 |
17
+ * | 黑名单 | **纯本地、不联网**:复用 zxcvbn 自带的常见口令字典(`@zxcvbn-ts/language-common` 的 `passwords-common` 49233 条 + `diceware-common` 7776 条)+ 一份小的应用相关词表(`APP_BLOCKLIST`),并支持 `extraBlocklist` 扩充 |
18
+ *
19
+ * **不做** HIBP 之类的联网查询:那是外部依赖,还会把口令哈希前缀发出去
20
+ * (SDK 方案 §3.3.6 明确排除)。
21
+ *
22
+ * ## 两个实现细节(都是坑)
23
+ *
24
+ * 1. **`userInputs` 里绝不能塞口令本身**。zxcvbn 会把 userInputs 当字典匹配,
25
+ * 如果口令自己进了字典,任何口令都会被判 0 分("自证"式的假阴性)。
26
+ * 这里只塞黑名单词与应用相关词。
27
+ * 2. **超长口令不估算**。`tooLong` 直接不可接受并短路返回:估算器本身是
28
+ * "把口令喂给一堆正则与字典",超过上限的输入没有估算价值(上限 128 是
29
+ * SDK 方案 §3.3.6 的防 DoS 条款)。
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.APP_BLOCKLIST = exports.ACCEPTABLE_SCORE = void 0;
33
+ exports.isBlocklisted = isBlocklisted;
34
+ exports.estimatePasswordStrength = estimatePasswordStrength;
35
+ const core_1 = require("@zxcvbn-ts/core");
36
+ const language_common_1 = require("@zxcvbn-ts/language-common");
37
+ const kdf_1 = require("./kdf");
38
+ /** 通过线:评分 ≥ 3(SDK 方案 §3.3.6) */
39
+ exports.ACCEPTABLE_SCORE = 3;
40
+ /**
41
+ * 应用相关词表("黑名单"的第二半)。
42
+ *
43
+ * 只放**公开的产品/域名/币种词**与占位示例域名 —— 这个仓库将来要开源,
44
+ * 不得出现真实内网地址、真实商户域名、任何密钥材料。
45
+ * 中文产品词也放几条:用户拿"私钥""助记词"当口令是真实存在的。
46
+ */
47
+ exports.APP_BLOCKLIST = [
48
+ 'trustbase',
49
+ 'trustbase.com',
50
+ 'trustchain',
51
+ 'trustcoin',
52
+ 'trust chain',
53
+ 'trustbase wallet',
54
+ 'tct',
55
+ 'utct',
56
+ 'shop.example.com',
57
+ 'example.com',
58
+ '信任链',
59
+ '私钥',
60
+ '助记词',
61
+ '钱包',
62
+ ];
63
+ /** 只有长度 ≥ 这个值的词才做"包含"判定(太短的词做包含判定会误伤,如 `tct`) */
64
+ const CONTAINMENT_MIN_LENGTH = 5;
65
+ /**
66
+ * 某个黑名单词做"包含"判定所需的最短长度。
67
+ *
68
+ * 拉丁字母的短串(`tct`)会自然地出现在随机口令里,做包含判定会误伤,
69
+ * 所以要求 ≥5 个字符;而 CJK 这类文字信息密度高,两个字就是一个完整词
70
+ * (`私钥`、`钱包`),做包含判定既不会误伤、又能挡住 `私钥123` 这种写法。
71
+ */
72
+ function containmentThreshold(term) {
73
+ return /[^\u0000-\u007f]/u.test(term) ? 2 : CONTAINMENT_MIN_LENGTH;
74
+ }
75
+ /** 警告 key → 中文理由(zxcvbn 默认给的是 key,不是文案) */
76
+ const WARNING_REASONS = {
77
+ straightRow: '这是键盘上相邻键位串(如 qwerty)',
78
+ keyPattern: '这是键盘图案(相邻键位组合)',
79
+ simpleRepeat: '这是重复的字符(如 aaaa)',
80
+ extendedRepeat: '这是重复的片段(如 abcabcabc)',
81
+ sequences: '这是连续序列(如 abcdef / 123456)',
82
+ recentYears: '这是近年份',
83
+ dates: '这像是日期',
84
+ topTen: '这是最常见的十个口令之一',
85
+ topHundred: '这是最常见的百个口令之一',
86
+ common: '这是常见口令',
87
+ similarToCommon: '与常见口令非常接近(只差一点改动)',
88
+ wordByItself: '这就是一个常见单词',
89
+ namesByThemselves: '这是常见人名',
90
+ commonNames: '这是常见姓名的组合',
91
+ userInputs: '包含应用名/产品名或你自己的信息',
92
+ pwned: '这个口令已出现在公开泄漏库里',
93
+ };
94
+ /** 匹配模式 → 中文理由(警告没覆盖到的结构,用这里补上) */
95
+ const PATTERN_REASONS = {
96
+ dictionary: '包含常见口令 / 常见词(字典命中)',
97
+ spatial: '这是键盘上相邻键位的组合',
98
+ repeat: '这是重复的字符或片段',
99
+ sequence: '这是连续序列(如 abcd / 1234)',
100
+ date: '这像是日期',
101
+ regex: '这像是年份或成串的数字',
102
+ wordSequence: '这是常见词的顺序组合',
103
+ separator: '这是用分隔符拼起来的常见词',
104
+ userInputs: '包含应用名/产品名或你自己的信息',
105
+ };
106
+ /** 某条警告已经说明了哪些模式(避免同一个理由说两遍) */
107
+ const WARNING_COVERS = {
108
+ straightRow: ['spatial'],
109
+ keyPattern: ['spatial'],
110
+ simpleRepeat: ['repeat'],
111
+ extendedRepeat: ['repeat'],
112
+ sequences: ['sequence'],
113
+ recentYears: ['regex', 'date'],
114
+ dates: ['date', 'regex', 'sequence'],
115
+ topTen: ['dictionary'],
116
+ topHundred: ['dictionary'],
117
+ common: ['dictionary'],
118
+ similarToCommon: ['dictionary'],
119
+ wordByItself: ['dictionary'],
120
+ namesByThemselves: ['dictionary'],
121
+ commonNames: ['dictionary', 'wordSequence'],
122
+ userInputs: ['dictionary', 'userInputs'],
123
+ };
124
+ /** 不产生理由的模式:`bruteforce` 是"没识别出结构"的兜底,不是缺点 */
125
+ const IGNORED_PATTERNS = ['bruteforce'];
126
+ let engine = null;
127
+ /**
128
+ * 惰性构造估算器(字典很大,构造一次约几十毫秒;模块顶层不做事,
129
+ * 保持"零副作用导入")。
130
+ */
131
+ function getEngine(maxLength) {
132
+ if (engine === null) {
133
+ engine = new core_1.ZxcvbnFactory({
134
+ dictionary: language_common_1.dictionary,
135
+ graphs: language_common_1.adjacencyGraphs,
136
+ // 上限与我们的口令长度上限一致:超长输入根本不该进估算器
137
+ maxLength,
138
+ });
139
+ }
140
+ return engine;
141
+ }
142
+ /** 归一化:小写 + 去掉非字母数字 + NFC(黑名单比对与"包含"判定都走它) */
143
+ function normalizeForBlocklist(value) {
144
+ return value
145
+ .normalize('NFC')
146
+ .toLowerCase()
147
+ .replace(/[^\p{L}\p{N}]+/gu, '');
148
+ }
149
+ /**
150
+ * 是否命中本地黑名单:**精确命中**(归一化后相等)或**包含命中**
151
+ * (黑名单词达到 {@link containmentThreshold} 且被归一化口令包含,
152
+ * 如 `trustbase2026`、`Trust-Base-2026`、`私钥123`)。
153
+ */
154
+ function isBlocklisted(password, extraBlocklist = []) {
155
+ if (typeof password !== 'string' || password.length === 0)
156
+ return false;
157
+ const normalized = normalizeForBlocklist(password);
158
+ if (normalized.length === 0)
159
+ return false;
160
+ for (const term of [...exports.APP_BLOCKLIST, ...extraBlocklist]) {
161
+ const candidate = normalizeForBlocklist(term);
162
+ if (candidate.length === 0)
163
+ continue;
164
+ if (normalized === candidate)
165
+ return true;
166
+ if (candidate.length >= containmentThreshold(candidate) && normalized.includes(candidate))
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ /**
172
+ * 估算口令强度(SDK 方案 §3.3.6)。
173
+ *
174
+ * 不做任何"必须含 X"的规则;`acceptable` 只看三件事:评分 ≥ 通过线、
175
+ * 不在黑名单、长度在 `1..maxLength` 之内。
176
+ */
177
+ function estimatePasswordStrength(password, options = {}) {
178
+ const maxLength = options.maxLength ?? kdf_1.MAX_PASSWORD_LENGTH;
179
+ const acceptableScore = options.acceptableScore ?? exports.ACCEPTABLE_SCORE;
180
+ if (typeof password !== 'string' || password.length === 0) {
181
+ return {
182
+ score: 0,
183
+ reasons: ['口令为空'],
184
+ acceptable: false,
185
+ guessesLog10: null,
186
+ blocklisted: false,
187
+ tooLong: false,
188
+ };
189
+ }
190
+ if (password.length > maxLength) {
191
+ return {
192
+ score: 0,
193
+ reasons: [`口令长度超过上限 ${maxLength} 个字符(上限是防 DoS 用的,不是强度要求)`],
194
+ acceptable: false,
195
+ guessesLog10: null,
196
+ blocklisted: false,
197
+ tooLong: true,
198
+ };
199
+ }
200
+ const blocklist = [...exports.APP_BLOCKLIST, ...(options.extraBlocklist ?? [])];
201
+ const userInputs = [...blocklist, ...(options.userInputs ?? [])];
202
+ const result = getEngine(maxLength).check(password, userInputs);
203
+ const blocklisted = isBlocklisted(password, options.extraBlocklist ?? []);
204
+ const reasons = [];
205
+ const warning = result.feedback.warning;
206
+ if (typeof warning === 'string' && warning.length > 0) {
207
+ reasons.push(WARNING_REASONS[warning] ?? '这是可预测的口令模式');
208
+ }
209
+ const covered = new Set(warning ? (WARNING_COVERS[warning] ?? []) : []);
210
+ const patterns = [];
211
+ for (const match of result.sequence) {
212
+ if (!patterns.includes(match.pattern))
213
+ patterns.push(match.pattern);
214
+ }
215
+ for (const pattern of patterns) {
216
+ if (IGNORED_PATTERNS.includes(pattern) || covered.has(pattern))
217
+ continue;
218
+ const reason = PATTERN_REASONS[pattern];
219
+ if (reason && !reasons.includes(reason))
220
+ reasons.push(reason);
221
+ }
222
+ if (blocklisted) {
223
+ reasons.push('包含应用名/产品名/示例域名等本地黑名单词');
224
+ }
225
+ if (reasons.length === 0 && result.score < acceptableScore) {
226
+ reasons.push(`可猜空间偏小(估算约需 10^${result.guessesLog10.toFixed(1)} 次尝试)`);
227
+ }
228
+ return {
229
+ score: result.score,
230
+ reasons,
231
+ acceptable: result.score >= acceptableScore && !blocklisted,
232
+ guessesLog10: result.guessesLog10,
233
+ blocklisted,
234
+ tooLong: false,
235
+ };
236
+ }
237
+ //# sourceMappingURL=password.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"password.js","sourceRoot":"","sources":["../src/password.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * WebAuthn PRF 适配器 —— ⚠️ **骨架,未在 Node 单测覆盖,必须在真机浏览器验证**。
3
+ *
4
+ * ## 这个文件为什么不能靠单测放心
5
+ *
6
+ * `navigator.credentials` 在 Node 里不存在,认证器的 PRF 输出又依赖具体平台
7
+ * 实现(iOS Safari / Android Chrome / 桌面浏览器并不一致)。所以:
8
+ *
9
+ * - 本文件**没有任何 Node 测试覆盖它的成功路径**;单测只覆盖"没有 WebAuthn 时
10
+ * 应当优雅降级"这一支(`isWebAuthnPrfSupported()` 返回 false、
11
+ * `getPrfSecret()` 抛 `PRF_UNAVAILABLE`)
12
+ * - 真机验证清单写在包 README「未验证清单」里;**上生产前必须走一遍真机**
13
+ * - 本文件刻意不开 DOM lib(本包只开 `lib: ["ES2022"]`,见 webcrypto.ts 文件头),
14
+ * 所以下面是本文件需要的几个最小声明,参数对象是普通字面量。**不要把这里
15
+ * 当成"类型已经保证正确"的代码**
16
+ *
17
+ * ## 流程要点(照 WebAuthn PRF 扩展的规范与各平台现状写)
18
+ *
19
+ * 1. **注册**:`create()` 时带 `extensions.prf.eval.first = salt`。
20
+ * ⚠️ 很多平台在 `create()` 的返回值里**不给** `prf.results`(只给
21
+ * `prf.enabled`),所以正确做法是注册后**立刻再 `get()` 一次断言**去取
22
+ * `results.first` —— 本文件的 `registerWebAuthnPrfCredential()` 就是这么做的。
23
+ * 2. **解锁**:`get()` 时带 `extensions.prf.eval.first = salt`(或
24
+ * `evalByCredential`),`allowCredentials` 里放凭据的 **rawId 字节**。
25
+ * 3. **salt 必须与注册时一致**,否则认证器按不同 salt 给不同秘密(这是 PRF 的
26
+ * 定义)。所以 salt 存在我们的包装 blob 里(见 prf.ts 的 `salt` 字段)。
27
+ * 4. **换设备/重装/重置生物识别 → 秘密就变了**(或凭据没了)→ 认证失败 →
28
+ * 回退口令或助记词。这是设计内行为,不是 bug(SDK 方案 §3.3.7 第 2 条)。
29
+ */
30
+ import { type PrfProvider } from './prf';
31
+ export interface WebAuthnPrfProviderOptions {
32
+ /**
33
+ * RP ID(依赖方标识)。默认用当前页面 origin 的主机名 —— 传错会导致
34
+ * "凭据属于另一个 RP"从而断言失败,所以除非明确知道为什么,不要传。
35
+ */
36
+ rpId?: string;
37
+ /** 认证器超时(毫秒),默认 60000 */
38
+ timeoutMs?: number;
39
+ /** 是否要求用户验证(生物识别/PIN),默认 `'required'`(硬件门控就是这个功能的意义) */
40
+ userVerification?: 'required' | 'preferred' | 'discouraged';
41
+ }
42
+ /**
43
+ * 这台设备/这个浏览器有没有可用的 WebAuthn 平台认证器(**只探测,不弹窗**)。
44
+ *
45
+ * 返回 false 的情况包括:Node 环境、非安全上下文(http 非 localhost)、
46
+ * 桌面浏览器没有平台认证器、隐私模式等。调用方据此选择"口令轨"。
47
+ */
48
+ export declare function isWebAuthnPrfSupported(): Promise<boolean>;
49
+ /** base64url → 字节(WebAuthn 的凭据 ID 是 base64url,不带填充) */
50
+ export declare function base64UrlToBytes(value: string): Uint8Array;
51
+ /** 字节 → base64url(无填充),用于把凭据 ID 交给下游 */
52
+ export declare function bytesToBase64Url(bytes: Uint8Array): string;
53
+ export interface RegisterWebAuthnPrfOptions extends WebAuthnPrfProviderOptions {
54
+ /** 展示给用户的账户名(如 `tct1…` 或用户名;userName 不能是空串) */
55
+ userName: string;
56
+ /** 展示名(可选) */
57
+ userDisplayName?: string;
58
+ /** PRF salt —— **必须与将来解锁时用的那一份相同**(存进 prf.ts 的包装里) */
59
+ salt: Uint8Array;
60
+ }
61
+ export interface RegisteredWebAuthnPrfCredential {
62
+ /** base64url 凭据 ID(存进我们的设备记录里) */
63
+ credentialId: string;
64
+ /** 注册后立刻断言取回的 PRF 秘密(32 字节;调用方用完必须 `wipe()`) */
65
+ prfSecret: Uint8Array;
66
+ /** 凭据创建时的 `prf.enabled` 标记(仅供参考,不能当作"拿到了秘密") */
67
+ prfEnabled: boolean;
68
+ }
69
+ /**
70
+ * 注册一个 PRF 凭据并取回它的秘密。
71
+ *
72
+ * ⚠️ 会产生**真实的 WebAuthn 弹窗**(生物识别/PIN)。Node 里调用会抛
73
+ * `PRF_UNAVAILABLE`。真机验证前不要在生产流程里打开它。
74
+ */
75
+ export declare function registerWebAuthnPrfCredential(options: RegisterWebAuthnPrfOptions): Promise<RegisteredWebAuthnPrfCredential>;
76
+ /**
77
+ * 浏览器 PRF 提供方(可注入给 `envelope.attachPrfTrack()` 与解锁流程)。
78
+ *
79
+ * ⚠️ 未在 Node 单测覆盖(见文件头)。它做的唯一一件事是:
80
+ * 触发一次生物识别授权,把认证器给的 PRF 秘密取回来。
81
+ */
82
+ export declare function createWebAuthnPrfProvider(options?: WebAuthnPrfProviderOptions): PrfProvider;
83
+ /** 给调用方一个"用完清掉秘密"的显式动作(成功路径与失败路径都要走) */
84
+ export declare function wipePrfSecret(secret: Uint8Array | null | undefined): void;
85
+ //# sourceMappingURL=prf-webauthn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prf-webauthn.d.ts","sourceRoot":"","sources":["../src/prf-webauthn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC;AA8B5D,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,CAAC;CAC7D;AAQD;;;;;GAKG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAW/D;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAM1D;AAED,wCAAwC;AACxC,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAE1D;AA0BD,MAAM,WAAW,0BAA2B,SAAQ,0BAA0B;IAC5E,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,+BAA+B;IAC9C,kCAAkC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,UAAU,CAAC;IACtB,gDAAgD;IAChD,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,+BAA+B,CAAC,CA0D1C;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,0BAA+B,GAAG,WAAW,CAmC/F;AAED,wCAAwC;AACxC,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAEzE"}