@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.
- package/README.md +351 -0
- package/dist/address.d.ts +70 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +104 -0
- package/dist/address.js.map +1 -0
- package/dist/bech32.d.ts +78 -0
- package/dist/bech32.d.ts.map +1 -0
- package/dist/bech32.js +246 -0
- package/dist/bech32.js.map +1 -0
- package/dist/bytes.d.ts +88 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +215 -0
- package/dist/bytes.js.map +1 -0
- package/dist/envelope.d.ts +116 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +237 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/hd.d.ts +85 -0
- package/dist/hd.d.ts.map +1 -0
- package/dist/hd.js +112 -0
- package/dist/hd.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +199 -0
- package/dist/index.js.map +1 -0
- package/dist/kdf.d.ts +163 -0
- package/dist/kdf.d.ts.map +1 -0
- package/dist/kdf.js +351 -0
- package/dist/kdf.js.map +1 -0
- package/dist/keys.d.ts +55 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +114 -0
- package/dist/keys.js.map +1 -0
- package/dist/keystore.d.ts +168 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +340 -0
- package/dist/keystore.js.map +1 -0
- package/dist/mnemonic.d.ts +84 -0
- package/dist/mnemonic.d.ts.map +1 -0
- package/dist/mnemonic.js +199 -0
- package/dist/mnemonic.js.map +1 -0
- package/dist/password.d.ts +79 -0
- package/dist/password.d.ts.map +1 -0
- package/dist/password.js +237 -0
- package/dist/password.js.map +1 -0
- package/dist/prf-webauthn.d.ts +85 -0
- package/dist/prf-webauthn.d.ts.map +1 -0
- package/dist/prf-webauthn.js +204 -0
- package/dist/prf-webauthn.js.map +1 -0
- package/dist/prf.d.ts +157 -0
- package/dist/prf.d.ts.map +1 -0
- package/dist/prf.js +318 -0
- package/dist/prf.js.map +1 -0
- package/dist/webcrypto.d.ts +93 -0
- package/dist/webcrypto.d.ts.map +1 -0
- package/dist/webcrypto.js +157 -0
- package/dist/webcrypto.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 双轨解锁信封(SDK 方案 §3.3.5 + §3.3.7 第 2 条)—— "生物识别优先、口令回退"。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这一层:PRF 包的不该是账户密钥,而是一把数据密钥
|
|
5
|
+
*
|
|
6
|
+
* 设计文档写的是「HKDF(PRF 秘密) 包住 keystore 的加密密钥」。要让它成立,
|
|
7
|
+
* 得先把"keystore 的加密密钥"变成一份**独立于口令存在**的东西,否则
|
|
8
|
+
* "PRF 包住它"和"口令派生它"就是两条算不出同一个值的路。于是:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* DEK(数据密钥,32 字节随机,永不落盘) ← 只存在于内存,用完即清
|
|
12
|
+
* ├─ 账户密钥材料(私钥/BIP39 种子)加密在 DEK 下 → payload
|
|
13
|
+
* ├─ 口令轨:DEK 用 Argon2id(PBKDF2) 派生密钥加密 → passwordTrack(就是 keystore v1)
|
|
14
|
+
* └─ PRF 轨:DEK 用 HKDF(PRF 秘密) 派生 KEK 包装 → prfTrack(可后加、可撤销)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* 三个直接好处:
|
|
18
|
+
* 1. **两条轨解出同一份 DEK** → 生物识别解锁与口令解锁的结果**逐字节相同**
|
|
19
|
+
* (这条被 `test/envelope.test.ts` 用 `toEqual` 钉住)
|
|
20
|
+
* 2. **可以后加 PRF**:用户在手机上第一次用口令建户,之后开了生物识别,
|
|
21
|
+
* 只需 `attachPrfTrack()` 包一份 DEK —— **不需要重新加密 payload**,
|
|
22
|
+
* 也不需要重新抄助记词
|
|
23
|
+
* 3. **可以撤销 PRF**:`detachPrfTrack()` 去掉那条轨,payload 与口令轨原封不动
|
|
24
|
+
*
|
|
25
|
+
* ## 为什么 attachPrfTrack 要一次口令
|
|
26
|
+
*
|
|
27
|
+
* 因为要用 DEK 去包(DEK 只在解锁后存在)。要求"开启生物识别时输一次口令"
|
|
28
|
+
* 同时也是一个安全直觉:把一条新的解锁路径绑到账户上,本身是敏感操作。
|
|
29
|
+
*/
|
|
30
|
+
import { KEYSTORE_CIPHER, type CreateKeystoreOptions, type KeystoreBlob } from './keystore';
|
|
31
|
+
import { type PrfWrappedKey, type UnlockStrategy } from './prf';
|
|
32
|
+
/** 信封格式版本 */
|
|
33
|
+
export declare const UNLOCK_ENVELOPE_VERSION = 1;
|
|
34
|
+
/** payload 的 AAD 前缀(版本化) */
|
|
35
|
+
export declare const UNLOCK_ENVELOPE_AAD_PREFIX = "trustbase-envelope-v1";
|
|
36
|
+
/** DEK 长度(AES-256) */
|
|
37
|
+
export declare const DATA_KEY_LENGTH = 32;
|
|
38
|
+
export interface EnvelopePayload {
|
|
39
|
+
cipher: typeof KEYSTORE_CIPHER;
|
|
40
|
+
/** payload 的 IV(base64,12 字节) */
|
|
41
|
+
iv: string;
|
|
42
|
+
ciphertext: string;
|
|
43
|
+
tag: string;
|
|
44
|
+
}
|
|
45
|
+
export interface UnlockEnvelope {
|
|
46
|
+
version: typeof UNLOCK_ENVELOPE_VERSION;
|
|
47
|
+
/** 账户密钥材料在 DEK 下的密文 */
|
|
48
|
+
payload: EnvelopePayload;
|
|
49
|
+
/** DEK 在口令下的密文(就是 keystore v1,可单独当备份用) */
|
|
50
|
+
passwordTrack: KeystoreBlob;
|
|
51
|
+
/** DEK 在 PRF 秘密下的包装(可选;用户开了生物识别才有) */
|
|
52
|
+
prfTrack?: PrfWrappedKey;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* payload 的 AAD 文本:`trustbase-envelope-v1|cipher=aes-256-gcm|iv=<b64>`
|
|
56
|
+
*
|
|
57
|
+
* 只吃 `cipher` + `iv` 两个字段(`ciphertext`/`tag` 是被认证的对象,不进 AAD),
|
|
58
|
+
* 所以创建时不必先造一个带空密文的假对象。
|
|
59
|
+
*/
|
|
60
|
+
export declare function envelopeAadText(header: Pick<EnvelopePayload, 'cipher' | 'iv'>): string;
|
|
61
|
+
/** 创建信封:生成一把随机 DEK,用它加密密钥材料,再用口令(以及可选的 PRF)包住 DEK */
|
|
62
|
+
export declare function createUnlockEnvelope(secret: Uint8Array, password: string, options?: CreateKeystoreOptions): Promise<UnlockEnvelope>;
|
|
63
|
+
export interface AttachPrfTrackRequest {
|
|
64
|
+
/** 当前口令(用来解出 DEK;见文件头"Why attachPrfTrack 要一次口令") */
|
|
65
|
+
password: string;
|
|
66
|
+
/** 从认证器取回的 PRF 秘密(32 字节;取它本身就该过生物识别) */
|
|
67
|
+
prfSecret: Uint8Array;
|
|
68
|
+
/** WebAuthn 凭据 ID */
|
|
69
|
+
credentialId: string;
|
|
70
|
+
/** HKDF salt(建议:注册凭据时用于 `prf.eval.first` 的那一份) */
|
|
71
|
+
salt?: Uint8Array;
|
|
72
|
+
/** HKDF info */
|
|
73
|
+
info?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 给已有信封**追加** PRF 轨(生物识别解锁)。
|
|
77
|
+
*
|
|
78
|
+
* payload 与口令轨都不动 —— 这正是"可以后加"的含义:用户先用口令建户,
|
|
79
|
+
* 之后开生物识别,不需要重新加密、也不需要重新抄助记词。
|
|
80
|
+
*/
|
|
81
|
+
export declare function attachPrfTrack(envelope: UnlockEnvelope | string, request: AttachPrfTrackRequest): Promise<UnlockEnvelope>;
|
|
82
|
+
/** 去掉 PRF 轨(撤销生物识别解锁)。payload 与口令轨不变,口令仍能解锁同一份材料。 */
|
|
83
|
+
export declare function detachPrfTrack(envelope: UnlockEnvelope | string): UnlockEnvelope;
|
|
84
|
+
/**
|
|
85
|
+
* 解锁请求:两条轨任选一条,解出的是**同一份**密钥材料。
|
|
86
|
+
*
|
|
87
|
+
* 两个分支的 `strategy` 直接取 `UnlockStrategy`(`'prf' | 'password'`),
|
|
88
|
+
* 所以"双轨"这件事是类型系统里的事实,不是约定。
|
|
89
|
+
*/
|
|
90
|
+
export type EnvelopeUnlockRequest = {
|
|
91
|
+
strategy: Extract<UnlockStrategy, 'password'>;
|
|
92
|
+
password: string;
|
|
93
|
+
} | {
|
|
94
|
+
strategy: Extract<UnlockStrategy, 'prf'>;
|
|
95
|
+
prfSecret: Uint8Array;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* 解锁信封。
|
|
99
|
+
*
|
|
100
|
+
* 返回的是**新分配的**密钥材料明文(调用方负责 `wipe()`);DEK 在返回前清零。
|
|
101
|
+
* 两条轨解出的结果逐字节相同(见文件头的"三个直接好处"第 1 条)。
|
|
102
|
+
*/
|
|
103
|
+
export declare function unlockEnvelope(envelope: UnlockEnvelope | string, request: EnvelopeUnlockRequest): Promise<Uint8Array>;
|
|
104
|
+
/** 校验并归一化信封(接受对象或 JSON 字符串) */
|
|
105
|
+
export declare function parseUnlockEnvelope(value: UnlockEnvelope | string | unknown): UnlockEnvelope;
|
|
106
|
+
/** 只问"能不能当信封用",不抛异常 */
|
|
107
|
+
export declare function isUnlockEnvelope(value: unknown): value is UnlockEnvelope;
|
|
108
|
+
/**
|
|
109
|
+
* 序列化(**紧凑 JSON,字段顺序固定**;`prfTrack` 未开启时不写该键)。
|
|
110
|
+
*
|
|
111
|
+
* 顺序固定是为了"同一份账户在任何实现里逐字节一致":备份文件、金标准向量、
|
|
112
|
+
* 跨端比对都靠它。嵌套对象用 `JSON.parse(serializeXxx())` 复用各模块自己的
|
|
113
|
+
* 定序序列化,避免在这里再抄一遍字段顺序。
|
|
114
|
+
*/
|
|
115
|
+
export declare function serializeUnlockEnvelope(envelope: UnlockEnvelope): string;
|
|
116
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EACL,eAAe,EAMf,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,OAAO,CAAC;AAGf,aAAa;AACb,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,4BAA4B;AAC5B,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAClE,sBAAsB;AACtB,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,eAAe,CAAC;IAC/B,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,uBAAuB,CAAC;IACxC,uBAAuB;IACvB,OAAO,EAAE,eAAe,CAAC;IACzB,0CAA0C;IAC1C,aAAa,EAAE,YAAY,CAAC;IAC5B,sCAAsC;IACtC,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,MAAM,CAEtF;AAYD,qDAAqD;AACrD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,MAAM,WAAW,qBAAqB;IACpC,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,SAAS,EAAE,UAAU,CAAC;IACtB,qBAAqB;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,gBAAgB;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,cAAc,GAAG,MAAM,EACjC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,GAAG,cAAc,CAGhF;AAED;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAAC,SAAS,EAAE,UAAU,CAAA;CAAE,CAAC;AAExE;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,cAAc,GAAG,MAAM,EACjC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,UAAU,CAAC,CAiBrB;AAaD,+BAA+B;AAC/B,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,cAAc,CA0D5F;AAED,uBAAuB;AACvB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAOxE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAkBxE"}
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 双轨解锁信封(SDK 方案 §3.3.5 + §3.3.7 第 2 条)—— "生物识别优先、口令回退"。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么需要这一层:PRF 包的不该是账户密钥,而是一把数据密钥
|
|
6
|
+
*
|
|
7
|
+
* 设计文档写的是「HKDF(PRF 秘密) 包住 keystore 的加密密钥」。要让它成立,
|
|
8
|
+
* 得先把"keystore 的加密密钥"变成一份**独立于口令存在**的东西,否则
|
|
9
|
+
* "PRF 包住它"和"口令派生它"就是两条算不出同一个值的路。于是:
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* DEK(数据密钥,32 字节随机,永不落盘) ← 只存在于内存,用完即清
|
|
13
|
+
* ├─ 账户密钥材料(私钥/BIP39 种子)加密在 DEK 下 → payload
|
|
14
|
+
* ├─ 口令轨:DEK 用 Argon2id(PBKDF2) 派生密钥加密 → passwordTrack(就是 keystore v1)
|
|
15
|
+
* └─ PRF 轨:DEK 用 HKDF(PRF 秘密) 派生 KEK 包装 → prfTrack(可后加、可撤销)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* 三个直接好处:
|
|
19
|
+
* 1. **两条轨解出同一份 DEK** → 生物识别解锁与口令解锁的结果**逐字节相同**
|
|
20
|
+
* (这条被 `test/envelope.test.ts` 用 `toEqual` 钉住)
|
|
21
|
+
* 2. **可以后加 PRF**:用户在手机上第一次用口令建户,之后开了生物识别,
|
|
22
|
+
* 只需 `attachPrfTrack()` 包一份 DEK —— **不需要重新加密 payload**,
|
|
23
|
+
* 也不需要重新抄助记词
|
|
24
|
+
* 3. **可以撤销 PRF**:`detachPrfTrack()` 去掉那条轨,payload 与口令轨原封不动
|
|
25
|
+
*
|
|
26
|
+
* ## 为什么 attachPrfTrack 要一次口令
|
|
27
|
+
*
|
|
28
|
+
* 因为要用 DEK 去包(DEK 只在解锁后存在)。要求"开启生物识别时输一次口令"
|
|
29
|
+
* 同时也是一个安全直觉:把一条新的解锁路径绑到账户上,本身是敏感操作。
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.DATA_KEY_LENGTH = exports.UNLOCK_ENVELOPE_AAD_PREFIX = exports.UNLOCK_ENVELOPE_VERSION = void 0;
|
|
33
|
+
exports.envelopeAadText = envelopeAadText;
|
|
34
|
+
exports.createUnlockEnvelope = createUnlockEnvelope;
|
|
35
|
+
exports.attachPrfTrack = attachPrfTrack;
|
|
36
|
+
exports.detachPrfTrack = detachPrfTrack;
|
|
37
|
+
exports.unlockEnvelope = unlockEnvelope;
|
|
38
|
+
exports.parseUnlockEnvelope = parseUnlockEnvelope;
|
|
39
|
+
exports.isUnlockEnvelope = isUnlockEnvelope;
|
|
40
|
+
exports.serializeUnlockEnvelope = serializeUnlockEnvelope;
|
|
41
|
+
const bytes_1 = require("./bytes");
|
|
42
|
+
const errors_1 = require("./errors");
|
|
43
|
+
const keystore_1 = require("./keystore");
|
|
44
|
+
const prf_1 = require("./prf");
|
|
45
|
+
const webcrypto_1 = require("./webcrypto");
|
|
46
|
+
/** 信封格式版本 */
|
|
47
|
+
exports.UNLOCK_ENVELOPE_VERSION = 1;
|
|
48
|
+
/** payload 的 AAD 前缀(版本化) */
|
|
49
|
+
exports.UNLOCK_ENVELOPE_AAD_PREFIX = 'trustbase-envelope-v1';
|
|
50
|
+
/** DEK 长度(AES-256) */
|
|
51
|
+
exports.DATA_KEY_LENGTH = 32;
|
|
52
|
+
/**
|
|
53
|
+
* payload 的 AAD 文本:`trustbase-envelope-v1|cipher=aes-256-gcm|iv=<b64>`
|
|
54
|
+
*
|
|
55
|
+
* 只吃 `cipher` + `iv` 两个字段(`ciphertext`/`tag` 是被认证的对象,不进 AAD),
|
|
56
|
+
* 所以创建时不必先造一个带空密文的假对象。
|
|
57
|
+
*/
|
|
58
|
+
function envelopeAadText(header) {
|
|
59
|
+
return [exports.UNLOCK_ENVELOPE_AAD_PREFIX, `cipher=${header.cipher}`, `iv=${header.iv}`].join('|');
|
|
60
|
+
}
|
|
61
|
+
function envelopeAad(header) {
|
|
62
|
+
return (0, bytes_1.utf8ToBytes)(envelopeAadText(header));
|
|
63
|
+
}
|
|
64
|
+
function assertSecretMaterial(secret) {
|
|
65
|
+
if (!(secret instanceof Uint8Array) || secret.length === 0) {
|
|
66
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', '密钥材料必须是非空 Uint8Array');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** 创建信封:生成一把随机 DEK,用它加密密钥材料,再用口令(以及可选的 PRF)包住 DEK */
|
|
70
|
+
async function createUnlockEnvelope(secret, password, options = {}) {
|
|
71
|
+
assertSecretMaterial(secret);
|
|
72
|
+
const dek = (0, webcrypto_1.randomBytes)(exports.DATA_KEY_LENGTH);
|
|
73
|
+
try {
|
|
74
|
+
const iv = (0, bytes_1.base64Encode)((0, webcrypto_1.randomBytes)(webcrypto_1.AES_GCM_IV_LENGTH));
|
|
75
|
+
const sealed = await (0, webcrypto_1.aesGcmEncrypt)(dek, (0, bytes_1.base64Decode)(iv), secret, envelopeAad({ cipher: keystore_1.KEYSTORE_CIPHER, iv }));
|
|
76
|
+
const payload = {
|
|
77
|
+
cipher: keystore_1.KEYSTORE_CIPHER,
|
|
78
|
+
iv,
|
|
79
|
+
ciphertext: (0, bytes_1.base64Encode)(sealed.ciphertext),
|
|
80
|
+
tag: (0, bytes_1.base64Encode)(sealed.tag),
|
|
81
|
+
};
|
|
82
|
+
const passwordTrack = await (0, keystore_1.createKeystore)(dek, password, options);
|
|
83
|
+
return { version: exports.UNLOCK_ENVELOPE_VERSION, payload, passwordTrack };
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
(0, bytes_1.wipe)(dek);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 给已有信封**追加** PRF 轨(生物识别解锁)。
|
|
91
|
+
*
|
|
92
|
+
* payload 与口令轨都不动 —— 这正是"可以后加"的含义:用户先用口令建户,
|
|
93
|
+
* 之后开生物识别,不需要重新加密、也不需要重新抄助记词。
|
|
94
|
+
*/
|
|
95
|
+
async function attachPrfTrack(envelope, request) {
|
|
96
|
+
const current = parseUnlockEnvelope(envelope);
|
|
97
|
+
const dek = await (0, keystore_1.unlockKeystore)(current.passwordTrack, request.password);
|
|
98
|
+
try {
|
|
99
|
+
const prfTrack = await (0, prf_1.wrapKeyWithPrf)(request.prfSecret, dek, {
|
|
100
|
+
credentialId: request.credentialId,
|
|
101
|
+
salt: request.salt,
|
|
102
|
+
info: request.info,
|
|
103
|
+
});
|
|
104
|
+
return { ...current, prfTrack };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
(0, bytes_1.wipe)(dek);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** 去掉 PRF 轨(撤销生物识别解锁)。payload 与口令轨不变,口令仍能解锁同一份材料。 */
|
|
111
|
+
function detachPrfTrack(envelope) {
|
|
112
|
+
const current = parseUnlockEnvelope(envelope);
|
|
113
|
+
return { version: current.version, payload: current.payload, passwordTrack: current.passwordTrack };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 解锁信封。
|
|
117
|
+
*
|
|
118
|
+
* 返回的是**新分配的**密钥材料明文(调用方负责 `wipe()`);DEK 在返回前清零。
|
|
119
|
+
* 两条轨解出的结果逐字节相同(见文件头的"三个直接好处"第 1 条)。
|
|
120
|
+
*/
|
|
121
|
+
async function unlockEnvelope(envelope, request) {
|
|
122
|
+
const current = parseUnlockEnvelope(envelope);
|
|
123
|
+
const dek = request.strategy === 'password'
|
|
124
|
+
? await (0, keystore_1.unlockKeystore)(current.passwordTrack, request.password)
|
|
125
|
+
: await unwrapDekWithPrf(current, request.prfSecret);
|
|
126
|
+
try {
|
|
127
|
+
return await (0, webcrypto_1.aesGcmDecrypt)(dek, (0, bytes_1.base64Decode)(current.payload.iv), (0, bytes_1.base64Decode)(current.payload.ciphertext), (0, bytes_1.base64Decode)(current.payload.tag), envelopeAad(current.payload));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
(0, bytes_1.wipe)(dek);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function unwrapDekWithPrf(envelope, prfSecret) {
|
|
134
|
+
if (!envelope.prfTrack) {
|
|
135
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '这份账户还没有开启生物识别解锁(没有 PRF 轨)—— 请用口令解锁,或先开启后再用');
|
|
136
|
+
}
|
|
137
|
+
return (0, prf_1.unwrapKeyWithPrf)(envelope.prfTrack, prfSecret);
|
|
138
|
+
}
|
|
139
|
+
const PAYLOAD_KEYS = ['cipher', 'iv', 'ciphertext', 'tag'];
|
|
140
|
+
/** 校验并归一化信封(接受对象或 JSON 字符串) */
|
|
141
|
+
function parseUnlockEnvelope(value) {
|
|
142
|
+
let raw = value;
|
|
143
|
+
if (typeof value === 'string') {
|
|
144
|
+
try {
|
|
145
|
+
raw = JSON.parse(value);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new errors_1.AccountError('INVALID_BLOB', '账户信封不是合法 JSON');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
152
|
+
throw new errors_1.AccountError('INVALID_BLOB', '账户信封必须是对象');
|
|
153
|
+
}
|
|
154
|
+
const record = raw;
|
|
155
|
+
for (const key of ['version', 'payload', 'passwordTrack']) {
|
|
156
|
+
if (!(key in record))
|
|
157
|
+
throw new errors_1.AccountError('INVALID_BLOB', `账户信封缺少字段:${key}`);
|
|
158
|
+
}
|
|
159
|
+
if (record.version !== exports.UNLOCK_ENVELOPE_VERSION) {
|
|
160
|
+
// 只有"整数且比本实现新"才算版本问题;1.5 / "1" 一律是结构非法
|
|
161
|
+
if (typeof record.version === 'number' &&
|
|
162
|
+
Number.isInteger(record.version) &&
|
|
163
|
+
record.version > exports.UNLOCK_ENVELOPE_VERSION) {
|
|
164
|
+
throw new errors_1.AccountError('UNSUPPORTED_VERSION', `账户信封版本 ${record.version} 本实现不认识`);
|
|
165
|
+
}
|
|
166
|
+
throw new errors_1.AccountError('INVALID_BLOB', `账户信封的 version 非法:${String(record.version)}`);
|
|
167
|
+
}
|
|
168
|
+
const payload = record.payload;
|
|
169
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
170
|
+
throw new errors_1.AccountError('INVALID_BLOB', '账户信封的 payload 必须是对象');
|
|
171
|
+
}
|
|
172
|
+
const payloadRecord = payload;
|
|
173
|
+
for (const key of PAYLOAD_KEYS) {
|
|
174
|
+
if (!(key in payloadRecord))
|
|
175
|
+
throw new errors_1.AccountError('INVALID_BLOB', `payload 缺少字段:${key}`);
|
|
176
|
+
}
|
|
177
|
+
if (payloadRecord.cipher !== keystore_1.KEYSTORE_CIPHER) {
|
|
178
|
+
throw new errors_1.AccountError('INVALID_BLOB', `payload 的 cipher 非法:${String(payloadRecord.cipher)}`);
|
|
179
|
+
}
|
|
180
|
+
(0, bytes_1.decodeBlobField)(payloadRecord.iv, 'payload.iv', { exact: webcrypto_1.AES_GCM_IV_LENGTH });
|
|
181
|
+
(0, bytes_1.decodeBlobField)(payloadRecord.tag, 'payload.tag', { exact: webcrypto_1.AES_GCM_TAG_LENGTH });
|
|
182
|
+
(0, bytes_1.decodeBlobField)(payloadRecord.ciphertext, 'payload.ciphertext', { min: 1 });
|
|
183
|
+
if (!(0, keystore_1.isKeystoreBlob)(record.passwordTrack)) {
|
|
184
|
+
throw new errors_1.AccountError('INVALID_BLOB', '账户信封的 passwordTrack 不是合法密钥库');
|
|
185
|
+
}
|
|
186
|
+
if (record.prfTrack !== undefined && !(0, prf_1.isPrfWrappedKey)(record.prfTrack)) {
|
|
187
|
+
throw new errors_1.AccountError('INVALID_BLOB', '账户信封的 prfTrack 不是合法的 PRF 包装');
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
version: exports.UNLOCK_ENVELOPE_VERSION,
|
|
191
|
+
payload: {
|
|
192
|
+
cipher: keystore_1.KEYSTORE_CIPHER,
|
|
193
|
+
iv: payloadRecord.iv,
|
|
194
|
+
ciphertext: payloadRecord.ciphertext,
|
|
195
|
+
tag: payloadRecord.tag,
|
|
196
|
+
},
|
|
197
|
+
passwordTrack: (0, keystore_1.parseKeystore)(record.passwordTrack),
|
|
198
|
+
...(record.prfTrack !== undefined ? { prfTrack: (0, prf_1.parsePrfWrappedKey)(record.prfTrack) } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** 只问"能不能当信封用",不抛异常 */
|
|
202
|
+
function isUnlockEnvelope(value) {
|
|
203
|
+
try {
|
|
204
|
+
parseUnlockEnvelope(value);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 序列化(**紧凑 JSON,字段顺序固定**;`prfTrack` 未开启时不写该键)。
|
|
213
|
+
*
|
|
214
|
+
* 顺序固定是为了"同一份账户在任何实现里逐字节一致":备份文件、金标准向量、
|
|
215
|
+
* 跨端比对都靠它。嵌套对象用 `JSON.parse(serializeXxx())` 复用各模块自己的
|
|
216
|
+
* 定序序列化,避免在这里再抄一遍字段顺序。
|
|
217
|
+
*/
|
|
218
|
+
function serializeUnlockEnvelope(envelope) {
|
|
219
|
+
const record = parseUnlockEnvelope(envelope);
|
|
220
|
+
const payload = {
|
|
221
|
+
cipher: record.payload.cipher,
|
|
222
|
+
iv: record.payload.iv,
|
|
223
|
+
ciphertext: record.payload.ciphertext,
|
|
224
|
+
tag: record.payload.tag,
|
|
225
|
+
};
|
|
226
|
+
const passwordTrack = JSON.parse((0, keystore_1.serializeKeystore)(record.passwordTrack));
|
|
227
|
+
if (record.prfTrack) {
|
|
228
|
+
return JSON.stringify({
|
|
229
|
+
version: record.version,
|
|
230
|
+
payload,
|
|
231
|
+
passwordTrack,
|
|
232
|
+
prfTrack: JSON.parse((0, prf_1.serializePrfWrappedKey)(record.prfTrack)),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return JSON.stringify({ version: record.version, payload, passwordTrack });
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@trustbase/account` 的错误类型。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么错误码要分得这么细,又故意分得不够细
|
|
5
|
+
*
|
|
6
|
+
* 有一条**安全约束**:口令错、密文被改、认证标签不匹配 —— 这三件事对用户必须
|
|
7
|
+
* 是同一句话(SDK 方案 §3.3.1「认证失败不区分'密码错'与'数据被改'以外的话术」)。
|
|
8
|
+
* 因为一旦能把它们区分开,攻击者就拿到了"这份密文是否被正确改写"的判定预言机。
|
|
9
|
+
* 所以三者一律 `AUTH_FAILED`,连 message 也不分。
|
|
10
|
+
*
|
|
11
|
+
* 另有一条**工程约束**:调用方得能分支。所以"blob 结构非法 / 版本不认识 / 环境
|
|
12
|
+
* 没有 WebCrypto / 口令超过长度上限"这些**不涉及秘密**的情况各给独立错误码
|
|
13
|
+
* —— 区分它们不泄漏任何关于口令与明文的信息。
|
|
14
|
+
*
|
|
15
|
+
* 结论:`AUTH_FAILED` 是唯一一个刻意含糊的码,其余都精确。
|
|
16
|
+
*/
|
|
17
|
+
export type AccountErrorCode =
|
|
18
|
+
/** 编程错误或参数非法(长度不对、类型不对、超出上限)—— 调用方写错了 */
|
|
19
|
+
'INVALID_ARGUMENT'
|
|
20
|
+
/** 密文 blob 结构非法(字段缺失/类型不对/取值越界)—— 不是本实现产出的形状 */
|
|
21
|
+
| 'INVALID_BLOB'
|
|
22
|
+
/** blob 的格式版本不认识(比本实现新)—— 拒绝,不要猜 */
|
|
23
|
+
| 'UNSUPPORTED_VERSION'
|
|
24
|
+
/** blob 声明的 KDF 未知,或本环境无法运行该 KDF(如无 WASM 时解 Argon2id 密文) */
|
|
25
|
+
| 'UNSUPPORTED_KDF'
|
|
26
|
+
/** 认证失败:口令错 / 密文被改 / PRF 秘密不匹配 —— 刻意不区分(见文件头) */
|
|
27
|
+
| 'AUTH_FAILED'
|
|
28
|
+
/** 运行环境缺少 WebCrypto(`globalThis.crypto.subtle` / `getRandomValues`) */
|
|
29
|
+
| 'CRYPTO_UNAVAILABLE'
|
|
30
|
+
/** 助记词不合法(词表外的词 / 校验和不对 / 长度不对) */
|
|
31
|
+
| 'INVALID_MNEMONIC'
|
|
32
|
+
/** 口令强度不达标(仅在显式要求强度门时抛出,见 keystore 的 `minStrength`) */
|
|
33
|
+
| 'WEAK_PASSWORD'
|
|
34
|
+
/** 该环境/该凭据没有 PRF 能力(WebAuthn PRF 扩展不可用) */
|
|
35
|
+
| 'PRF_UNAVAILABLE';
|
|
36
|
+
export declare class AccountError extends Error {
|
|
37
|
+
/** 机器可读错误码,测试与调用方按它断言,不按 message 断言 */
|
|
38
|
+
readonly code: AccountErrorCode;
|
|
39
|
+
/**
|
|
40
|
+
* 附带的结构化信息(如强度门失败时的 `{score, reasons}`)。
|
|
41
|
+
* **不得**放入口令、明文密钥材料、派生密钥 —— 只放"可以进日志"的东西。
|
|
42
|
+
*/
|
|
43
|
+
readonly details: Readonly<Record<string, unknown>> | undefined;
|
|
44
|
+
constructor(code: AccountErrorCode, message: string, details?: Record<string, unknown>);
|
|
45
|
+
}
|
|
46
|
+
/** 判断一个异常是不是本包的 `AccountError`;给了 `code` 时同时比对错误码。 */
|
|
47
|
+
export declare function isAccountError(value: unknown, code?: AccountErrorCode): value is AccountError;
|
|
48
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,gBAAgB;AAC1B,yCAAyC;AACvC,kBAAkB;AACpB,gDAAgD;GAC9C,cAAc;AAChB,oCAAoC;GAClC,qBAAqB;AACvB,4DAA4D;GAC1D,iBAAiB;AACnB,iDAAiD;GAC/C,aAAa;AACf,uEAAuE;GACrE,oBAAoB;AACtB,mCAAmC;GACjC,kBAAkB;AACpB,uDAAuD;GACrD,eAAe;AACjB,2CAA2C;GACzC,iBAAiB,CAAC;AAEtB,qBAAa,YAAa,SAAQ,KAAK;IACrC,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;gBAEpD,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAMvF;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,KAAK,IAAI,YAAY,CAE7F"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `@trustbase/account` 的错误类型。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么错误码要分得这么细,又故意分得不够细
|
|
6
|
+
*
|
|
7
|
+
* 有一条**安全约束**:口令错、密文被改、认证标签不匹配 —— 这三件事对用户必须
|
|
8
|
+
* 是同一句话(SDK 方案 §3.3.1「认证失败不区分'密码错'与'数据被改'以外的话术」)。
|
|
9
|
+
* 因为一旦能把它们区分开,攻击者就拿到了"这份密文是否被正确改写"的判定预言机。
|
|
10
|
+
* 所以三者一律 `AUTH_FAILED`,连 message 也不分。
|
|
11
|
+
*
|
|
12
|
+
* 另有一条**工程约束**:调用方得能分支。所以"blob 结构非法 / 版本不认识 / 环境
|
|
13
|
+
* 没有 WebCrypto / 口令超过长度上限"这些**不涉及秘密**的情况各给独立错误码
|
|
14
|
+
* —— 区分它们不泄漏任何关于口令与明文的信息。
|
|
15
|
+
*
|
|
16
|
+
* 结论:`AUTH_FAILED` 是唯一一个刻意含糊的码,其余都精确。
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.AccountError = void 0;
|
|
20
|
+
exports.isAccountError = isAccountError;
|
|
21
|
+
class AccountError extends Error {
|
|
22
|
+
/** 机器可读错误码,测试与调用方按它断言,不按 message 断言 */
|
|
23
|
+
code;
|
|
24
|
+
/**
|
|
25
|
+
* 附带的结构化信息(如强度门失败时的 `{score, reasons}`)。
|
|
26
|
+
* **不得**放入口令、明文密钥材料、派生密钥 —— 只放"可以进日志"的东西。
|
|
27
|
+
*/
|
|
28
|
+
details;
|
|
29
|
+
constructor(code, message, details) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'AccountError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.details = details;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.AccountError = AccountError;
|
|
37
|
+
/** 判断一个异常是不是本包的 `AccountError`;给了 `code` 时同时比对错误码。 */
|
|
38
|
+
function isAccountError(value, code) {
|
|
39
|
+
return value instanceof AccountError && (code === undefined || value.code === code);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAyCH,wCAEC;AArBD,MAAa,YAAa,SAAQ,KAAK;IACrC,uCAAuC;IAC9B,IAAI,CAAmB;IAEhC;;;OAGG;IACM,OAAO,CAAgD;IAEhE,YAAY,IAAsB,EAAE,OAAe,EAAE,OAAiC;QACpF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAhBD,oCAgBC;AAED,sDAAsD;AACtD,SAAgB,cAAc,CAAC,KAAc,EAAE,IAAuB;IACpE,OAAO,KAAK,YAAY,YAAY,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACtF,CAAC"}
|
package/dist/hd.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 助记词 → 账户(设备账户的"首次打开即建账户"路径,SDK 方案 §3.2.3 / §8.3.2 ②)。
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* BIP39 助记词 ──(PBKDF2-HMAC-SHA512, 2048 次, salt="mnemonic"+passphrase)──▶ 64 字节种子
|
|
6
|
+
* 种子 ──(BIP32/CIP-3 路径 m/44'/118'/0'/0/0)──▶ secp256k1 私钥(32 字节)
|
|
7
|
+
* 私钥 ──▶ 33 字节压缩公钥 ──▶ tct1… 地址(口径见 address.ts)
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* ## 为什么路径是 `m/44'/118'/0'/0/0`
|
|
11
|
+
*
|
|
12
|
+
* `118` 是 Cosmos 的 coin type(SLIP-44 登记值),后面 `0'/0/0` 是"第一个账户、
|
|
13
|
+
* 外部链、第一个地址"。这与 cosmjs 的 `DirectSecp256k1HdWallet` 默认值一致
|
|
14
|
+
* (`@trustbase/protocol` 的 `HD_PATH` 就是这一串),所以同一份助记词在本 SDK、
|
|
15
|
+
* 在 cosmjs、在链侧 keyring 里派生出的是**同一个地址**。改这个路径 = 让所有
|
|
16
|
+
* 已发出的纸密钥指向另一个地址,属于红线。
|
|
17
|
+
*
|
|
18
|
+
* ## 助记词口令(passphrase,BIP39 的"第 13/25 个词")
|
|
19
|
+
*
|
|
20
|
+
* 本 SDK v0 **不使用**它(不引导用户设置,避免多一层"忘了我还设过口令"的坑),
|
|
21
|
+
* 但参数保留并直通底层:将来要支持时不必改签名,且**必须**记住 BIP39 的交错
|
|
22
|
+
* 结构 —— 助记词带不带 passphrase 派生出的是两个完全不同的账户。
|
|
23
|
+
*/
|
|
24
|
+
import { type MnemonicStrength, type RandomSource } from './mnemonic';
|
|
25
|
+
/**
|
|
26
|
+
* 账户派生路径。与 `@trustbase/protocol` 的 `HD_PATH` 同值
|
|
27
|
+
* (漂移由 `test/constants-drift.test.ts` 静态核对)。
|
|
28
|
+
*/
|
|
29
|
+
export declare const ACCOUNT_DERIVATION_PATH = "m/44'/118'/0'/0/0";
|
|
30
|
+
export interface AccountKeyPair {
|
|
31
|
+
/** 私钥(32 字节)。调用方负责在用完后 `wipe()` */
|
|
32
|
+
privateKey: Uint8Array;
|
|
33
|
+
/** 压缩公钥(33 字节) */
|
|
34
|
+
publicKey: Uint8Array;
|
|
35
|
+
/** `tct1…` 账户地址 */
|
|
36
|
+
address: string;
|
|
37
|
+
}
|
|
38
|
+
export interface DerivedAccount extends AccountKeyPair {
|
|
39
|
+
/** 实际使用的派生路径 */
|
|
40
|
+
path: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DeriveAccountOptions {
|
|
43
|
+
/** 派生路径,默认 {@link ACCOUNT_DERIVATION_PATH} */
|
|
44
|
+
path?: string;
|
|
45
|
+
/** BIP39 passphrase(默认空字符串;见文件头) */
|
|
46
|
+
passphrase?: string;
|
|
47
|
+
/** 地址 HRP,默认 `tct` */
|
|
48
|
+
hrp?: string;
|
|
49
|
+
}
|
|
50
|
+
/** 助记词(+可选 passphrase)→ 64 字节 BIP39 种子。非法助记词抛 `INVALID_MNEMONIC` */
|
|
51
|
+
export declare function mnemonicToSeed(mnemonic: string, passphrase?: string): Uint8Array;
|
|
52
|
+
/**
|
|
53
|
+
* 助记词 → 账户密钥对 + 地址。
|
|
54
|
+
*
|
|
55
|
+
* 种子在函数内派生出私钥后立即清零(种子是我们自己分配的缓冲区)。
|
|
56
|
+
*/
|
|
57
|
+
export declare function deriveAccountFromMnemonic(mnemonic: string, options?: DeriveAccountOptions): DerivedAccount;
|
|
58
|
+
/**
|
|
59
|
+
* 已有私钥 → 账户(地址口径与助记词路径完全一致)。
|
|
60
|
+
*
|
|
61
|
+
* 用于备份恢复、测试向量、以及将来"把链上 keyring 里的密钥导进设备账户"。
|
|
62
|
+
*/
|
|
63
|
+
export declare function accountFromPrivateKey(privateKey: Uint8Array, options?: {
|
|
64
|
+
hrp?: string;
|
|
65
|
+
}): AccountKeyPair;
|
|
66
|
+
export interface CreateAccountOptions extends DeriveAccountOptions {
|
|
67
|
+
/** 助记词强度(默认 128 bit = 12 词) */
|
|
68
|
+
strength?: MnemonicStrength;
|
|
69
|
+
/** 随机源(测试可注入) */
|
|
70
|
+
rng?: RandomSource;
|
|
71
|
+
}
|
|
72
|
+
export interface CreatedAccount extends DerivedAccount {
|
|
73
|
+
/** 新生成的助记词(**只有这一次机会展示给用户抄写**;本包不持久化它) */
|
|
74
|
+
mnemonic: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 建一个新设备账户(助记词 + 密钥对 + 地址)。
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 调用方拿到 `mnemonic` 后应当立刻走"抄写门"(见 mnemonic.ts),
|
|
80
|
+
* 然后**只在内存里**留到写入加密 keystore 为止 —— 本包不落任何明文。
|
|
81
|
+
*/
|
|
82
|
+
export declare function createAccount(options?: CreateAccountOptions): CreatedAccount;
|
|
83
|
+
/** 助记词词表(界面上做拼写提示/校验用;只导出英文表,与生成口径一致) */
|
|
84
|
+
export declare const WORDLIST_ENGLISH: readonly string[];
|
|
85
|
+
//# sourceMappingURL=hd.d.ts.map
|
package/dist/hd.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hd.d.ts","sourceRoot":"","sources":["../src/hd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB;IAClB,SAAS,EAAE,UAAU,CAAC;IACtB,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sBAAsB;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,SAAK,GAAG,UAAU,CAO5E;AAQD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,cAAc,CA0B9G;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,cAAc,CAK5G;AAED,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,iBAAiB;IACjB,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,cAAc,CAIhF;AAED,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAAoB,CAAC"}
|