@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,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebAuthn PRF 适配器 —— ⚠️ **骨架,未在 Node 单测覆盖,必须在真机浏览器验证**。
|
|
4
|
+
*
|
|
5
|
+
* ## 这个文件为什么不能靠单测放心
|
|
6
|
+
*
|
|
7
|
+
* `navigator.credentials` 在 Node 里不存在,认证器的 PRF 输出又依赖具体平台
|
|
8
|
+
* 实现(iOS Safari / Android Chrome / 桌面浏览器并不一致)。所以:
|
|
9
|
+
*
|
|
10
|
+
* - 本文件**没有任何 Node 测试覆盖它的成功路径**;单测只覆盖"没有 WebAuthn 时
|
|
11
|
+
* 应当优雅降级"这一支(`isWebAuthnPrfSupported()` 返回 false、
|
|
12
|
+
* `getPrfSecret()` 抛 `PRF_UNAVAILABLE`)
|
|
13
|
+
* - 真机验证清单写在包 README「未验证清单」里;**上生产前必须走一遍真机**
|
|
14
|
+
* - 本文件刻意不开 DOM lib(本包只开 `lib: ["ES2022"]`,见 webcrypto.ts 文件头),
|
|
15
|
+
* 所以下面是本文件需要的几个最小声明,参数对象是普通字面量。**不要把这里
|
|
16
|
+
* 当成"类型已经保证正确"的代码**
|
|
17
|
+
*
|
|
18
|
+
* ## 流程要点(照 WebAuthn PRF 扩展的规范与各平台现状写)
|
|
19
|
+
*
|
|
20
|
+
* 1. **注册**:`create()` 时带 `extensions.prf.eval.first = salt`。
|
|
21
|
+
* ⚠️ 很多平台在 `create()` 的返回值里**不给** `prf.results`(只给
|
|
22
|
+
* `prf.enabled`),所以正确做法是注册后**立刻再 `get()` 一次断言**去取
|
|
23
|
+
* `results.first` —— 本文件的 `registerWebAuthnPrfCredential()` 就是这么做的。
|
|
24
|
+
* 2. **解锁**:`get()` 时带 `extensions.prf.eval.first = salt`(或
|
|
25
|
+
* `evalByCredential`),`allowCredentials` 里放凭据的 **rawId 字节**。
|
|
26
|
+
* 3. **salt 必须与注册时一致**,否则认证器按不同 salt 给不同秘密(这是 PRF 的
|
|
27
|
+
* 定义)。所以 salt 存在我们的包装 blob 里(见 prf.ts 的 `salt` 字段)。
|
|
28
|
+
* 4. **换设备/重装/重置生物识别 → 秘密就变了**(或凭据没了)→ 认证失败 →
|
|
29
|
+
* 回退口令或助记词。这是设计内行为,不是 bug(SDK 方案 §3.3.7 第 2 条)。
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.isWebAuthnPrfSupported = isWebAuthnPrfSupported;
|
|
33
|
+
exports.base64UrlToBytes = base64UrlToBytes;
|
|
34
|
+
exports.bytesToBase64Url = bytesToBase64Url;
|
|
35
|
+
exports.registerWebAuthnPrfCredential = registerWebAuthnPrfCredential;
|
|
36
|
+
exports.createWebAuthnPrfProvider = createWebAuthnPrfProvider;
|
|
37
|
+
exports.wipePrfSecret = wipePrfSecret;
|
|
38
|
+
const bytes_1 = require("./bytes");
|
|
39
|
+
const errors_1 = require("./errors");
|
|
40
|
+
const prf_1 = require("./prf");
|
|
41
|
+
const webcrypto_1 = require("./webcrypto");
|
|
42
|
+
function webAuthn() {
|
|
43
|
+
const nav = globalThis.navigator;
|
|
44
|
+
if (!nav || typeof nav !== 'object')
|
|
45
|
+
return undefined;
|
|
46
|
+
return nav;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 这台设备/这个浏览器有没有可用的 WebAuthn 平台认证器(**只探测,不弹窗**)。
|
|
50
|
+
*
|
|
51
|
+
* 返回 false 的情况包括:Node 环境、非安全上下文(http 非 localhost)、
|
|
52
|
+
* 桌面浏览器没有平台认证器、隐私模式等。调用方据此选择"口令轨"。
|
|
53
|
+
*/
|
|
54
|
+
async function isWebAuthnPrfSupported() {
|
|
55
|
+
const api = webAuthn();
|
|
56
|
+
if (!api || typeof api.credentials?.create !== 'function' || typeof api.credentials.get !== 'function') {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (typeof api.isUserVerifyingPlatformAuthenticatorAvailable !== 'function')
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
return await api.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** base64url → 字节(WebAuthn 的凭据 ID 是 base64url,不带填充) */
|
|
69
|
+
function base64UrlToBytes(value) {
|
|
70
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
71
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', 'base64url 输入必须是非空字符串');
|
|
72
|
+
}
|
|
73
|
+
const standard = value.replace(/-/gu, '+').replace(/_/gu, '/');
|
|
74
|
+
return (0, bytes_1.base64Decode)(standard);
|
|
75
|
+
}
|
|
76
|
+
/** 字节 → base64url(无填充),用于把凭据 ID 交给下游 */
|
|
77
|
+
function bytesToBase64Url(bytes) {
|
|
78
|
+
return (0, bytes_1.base64Encode)(bytes).replace(/\+/gu, '-').replace(/\//gu, '_').replace(/=+$/u, '');
|
|
79
|
+
}
|
|
80
|
+
function toUint8(value) {
|
|
81
|
+
if (!value)
|
|
82
|
+
return null;
|
|
83
|
+
return value instanceof Uint8Array ? Uint8Array.from(value) : new Uint8Array(value);
|
|
84
|
+
}
|
|
85
|
+
function extractPrfSecret(credential, context) {
|
|
86
|
+
const results = credential.getClientExtensionResults?.().prf;
|
|
87
|
+
const first = toUint8(results?.results?.first);
|
|
88
|
+
if (!first) {
|
|
89
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', `${context}:认证器没有返回 PRF 输出(results.first 缺失)。` +
|
|
90
|
+
'可能是该平台不支持 PRF 扩展,或需要在断言时用 evalByCredential 指定凭据。');
|
|
91
|
+
}
|
|
92
|
+
if (first.length !== prf_1.PRF_SECRET_LENGTH) {
|
|
93
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', `${context}:认证器返回的 PRF 输出是 ${first.length} 字节(本实现按 ${prf_1.PRF_SECRET_LENGTH} 字节设计)`);
|
|
94
|
+
}
|
|
95
|
+
return first;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 注册一个 PRF 凭据并取回它的秘密。
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ 会产生**真实的 WebAuthn 弹窗**(生物识别/PIN)。Node 里调用会抛
|
|
101
|
+
* `PRF_UNAVAILABLE`。真机验证前不要在生产流程里打开它。
|
|
102
|
+
*/
|
|
103
|
+
async function registerWebAuthnPrfCredential(options) {
|
|
104
|
+
const api = webAuthn();
|
|
105
|
+
if (!api || typeof api.credentials?.create !== 'function' || typeof api.credentials.get !== 'function') {
|
|
106
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '本环境没有 WebAuthn(navigator.credentials)—— 无法注册生物识别解锁');
|
|
107
|
+
}
|
|
108
|
+
if (!options.userName) {
|
|
109
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', 'userName 不能为空');
|
|
110
|
+
}
|
|
111
|
+
if (!(options.salt instanceof Uint8Array) || options.salt.length === 0) {
|
|
112
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', 'salt 不能为空');
|
|
113
|
+
}
|
|
114
|
+
const timeout = options.timeoutMs ?? 60_000;
|
|
115
|
+
const userVerification = options.userVerification ?? 'required';
|
|
116
|
+
const rpId = options.rpId;
|
|
117
|
+
const created = (await api.credentials.create({
|
|
118
|
+
publicKey: {
|
|
119
|
+
...(rpId ? { rp: { id: rpId, name: 'TrustBase' } } : { rp: { name: 'TrustBase' } }),
|
|
120
|
+
user: {
|
|
121
|
+
// user.id 必须是随机的、不可猜测的字节串(规范要求 ≥ 16 字节);这里用 salt 派生
|
|
122
|
+
id: options.salt,
|
|
123
|
+
name: options.userName,
|
|
124
|
+
displayName: options.userDisplayName ?? options.userName,
|
|
125
|
+
},
|
|
126
|
+
pubKeyCredParams: [
|
|
127
|
+
{ type: 'public-key', alg: -7 }, // ES256(P-256):平台认证器的通用算法
|
|
128
|
+
{ type: 'public-key', alg: -257 }, // RS256(兜底)
|
|
129
|
+
],
|
|
130
|
+
authenticatorSelection: { userVerification, residentKey: 'preferred' },
|
|
131
|
+
timeout,
|
|
132
|
+
attestation: 'none',
|
|
133
|
+
// 关键:注册时就要求评估 PRF(部分平台只在注册时把 PRF 绑到凭据上)
|
|
134
|
+
extensions: { prf: { eval: { first: options.salt } } },
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
if (!created || typeof created.id !== 'string' || created.id.length === 0) {
|
|
138
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '注册未返回凭据(用户取消或认证器拒绝)');
|
|
139
|
+
}
|
|
140
|
+
const enabled = created.getClientExtensionResults?.().prf?.enabled === true;
|
|
141
|
+
// 很多平台在 create() 的返回值里不给 results,所以立刻做一次断言去取。
|
|
142
|
+
const asserted = (await api.credentials.get({
|
|
143
|
+
publicKey: {
|
|
144
|
+
...(rpId ? { rpId } : {}),
|
|
145
|
+
challenge: (0, webcrypto_1.randomBytes)(32),
|
|
146
|
+
allowCredentials: [{ type: 'public-key', id: base64UrlToBytes(created.id) }],
|
|
147
|
+
userVerification,
|
|
148
|
+
timeout,
|
|
149
|
+
extensions: { prf: { eval: { first: options.salt } } },
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
if (!asserted) {
|
|
153
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '注册后的断言未返回(用户取消或认证器拒绝)');
|
|
154
|
+
}
|
|
155
|
+
const prfSecret = extractPrfSecret(asserted, '注册后断言');
|
|
156
|
+
return { credentialId: asserted.id ?? created.id, prfSecret, prfEnabled: enabled };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 浏览器 PRF 提供方(可注入给 `envelope.attachPrfTrack()` 与解锁流程)。
|
|
160
|
+
*
|
|
161
|
+
* ⚠️ 未在 Node 单测覆盖(见文件头)。它做的唯一一件事是:
|
|
162
|
+
* 触发一次生物识别授权,把认证器给的 PRF 秘密取回来。
|
|
163
|
+
*/
|
|
164
|
+
function createWebAuthnPrfProvider(options = {}) {
|
|
165
|
+
const timeout = options.timeoutMs ?? 60_000;
|
|
166
|
+
const userVerification = options.userVerification ?? 'required';
|
|
167
|
+
return {
|
|
168
|
+
kind: 'webauthn',
|
|
169
|
+
async isAvailable() {
|
|
170
|
+
return isWebAuthnPrfSupported();
|
|
171
|
+
},
|
|
172
|
+
async getPrfSecret(credentialId, salt) {
|
|
173
|
+
const api = webAuthn();
|
|
174
|
+
if (!api || typeof api.credentials?.get !== 'function') {
|
|
175
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '本环境没有 WebAuthn(navigator.credentials.get)');
|
|
176
|
+
}
|
|
177
|
+
if (typeof credentialId !== 'string' || credentialId.length === 0) {
|
|
178
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', 'credentialId 不能为空');
|
|
179
|
+
}
|
|
180
|
+
if (!(salt instanceof Uint8Array) || salt.length === 0) {
|
|
181
|
+
throw new errors_1.AccountError('INVALID_ARGUMENT', 'salt 不能为空');
|
|
182
|
+
}
|
|
183
|
+
const assertion = (await api.credentials.get({
|
|
184
|
+
publicKey: {
|
|
185
|
+
...(options.rpId ? { rpId: options.rpId } : {}),
|
|
186
|
+
challenge: (0, webcrypto_1.randomBytes)(32),
|
|
187
|
+
allowCredentials: [{ type: 'public-key', id: base64UrlToBytes(credentialId) }],
|
|
188
|
+
userVerification,
|
|
189
|
+
timeout,
|
|
190
|
+
extensions: { prf: { eval: { first: salt } } },
|
|
191
|
+
},
|
|
192
|
+
}));
|
|
193
|
+
if (!assertion) {
|
|
194
|
+
throw new errors_1.AccountError('PRF_UNAVAILABLE', '断言未返回(用户取消或认证器拒绝)');
|
|
195
|
+
}
|
|
196
|
+
return extractPrfSecret(assertion, '断言');
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** 给调用方一个"用完清掉秘密"的显式动作(成功路径与失败路径都要走) */
|
|
201
|
+
function wipePrfSecret(secret) {
|
|
202
|
+
(0, bytes_1.wipe)(secret);
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=prf-webauthn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prf-webauthn.js","sourceRoot":"","sources":["../src/prf-webauthn.ts"],"names":[],"mappings":"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"}
|
package/dist/prf.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRF 硬件解锁(SDK 方案 §3.3.5)—— 用平台认证器持有的秘密包住解锁密钥。
|
|
3
|
+
*
|
|
4
|
+
* ## 它解决什么问题、不解决什么问题
|
|
5
|
+
*
|
|
6
|
+
* ✅ 解决:把"解锁 keystore 的钥匙"绑到**硬件 + 生物识别/PIN**。秘密由
|
|
7
|
+
* Secure Enclave / Android Keystore(StrongBox)持有,**永不导出**,
|
|
8
|
+
* 每次取都要过生物识别。我们不存它、也导不出它。
|
|
9
|
+
*
|
|
10
|
+
* ❌ 不解决(**别往这个方向设计**):拿 SE/Keystore 直接签链上交易。
|
|
11
|
+
* 浏览器只有 WebAuthn,而 WebAuthn 断言签的是"认证器数据 + 客户端数据哈希",
|
|
12
|
+
* **不能对任意 payload 签名** —— 拿不到一把能签 Cosmos 交易的密钥。
|
|
13
|
+
* (出处:SDK 方案 §3.3.5 的 ⚠️ 段,这是本节最容易被想歪的地方。)
|
|
14
|
+
*
|
|
15
|
+
* ## 包装格式(v1,跨语言可复算)
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* KEK = HKDF-SHA256(ikm = PRF 秘密, salt = salt, info = info) → 32 字节
|
|
19
|
+
* wrapped = AES-256-GCM(key = KEK, iv = 12 随机字节, plaintext = 被包的密钥材料)
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ```jsonc
|
|
23
|
+
* {
|
|
24
|
+
* "version": 1,
|
|
25
|
+
* "kdf": "hkdf-sha256",
|
|
26
|
+
* "credentialId": "<WebAuthn 凭据 ID(base64url)>",
|
|
27
|
+
* "salt": "<base64>", // 同时是 HKDF 的 salt 与 AAD 的一部分
|
|
28
|
+
* "info": "trustbase/prf-wrap/v1",
|
|
29
|
+
* "cipher": "aes-256-gcm",
|
|
30
|
+
* "iv": "<base64, 12 字节>",
|
|
31
|
+
* "ciphertext": "<base64>",
|
|
32
|
+
* "tag": "<base64>"
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* AAD(防"参数被改"):
|
|
37
|
+
*
|
|
38
|
+
* ```
|
|
39
|
+
* trustbase-prf-wrap-v1|kdf=hkdf-sha256|credentialId=<id>|info=<info>|salt=<b64>|cipher=aes-256-gcm|iv=<b64>
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* 于是:**换设备/重装 → PRF 秘密变了 → 认证失败 → 回退口令或助记词**
|
|
43
|
+
* (SDK 方案 §3.3.7 第 2 条的取舍:PRF 是"首选体验",不是唯一出路;
|
|
44
|
+
* 双轨的编排见 envelope.ts)。
|
|
45
|
+
*
|
|
46
|
+
* ## 本文件与 prf-webauthn.ts 的分工
|
|
47
|
+
*
|
|
48
|
+
* 本文件是**纯密码学 + 接口**,在 Node 里能完整单测(用 `createStaticPrfProvider`
|
|
49
|
+
* 造假秘密)。真正调 `navigator.credentials` 的适配器在 prf-webauthn.ts,
|
|
50
|
+
* 那个文件**没有 Node 单测覆盖**,必须在真机浏览器验证 —— 那里写得很明确,别混。
|
|
51
|
+
*/
|
|
52
|
+
/** 包装格式版本 */
|
|
53
|
+
export declare const PRF_WRAP_VERSION = 1;
|
|
54
|
+
/** HKDF 的 info:域分隔串(带版本;换格式必须换它,否则新旧包会互相解开对方的东西) */
|
|
55
|
+
export declare const PRF_WRAP_INFO = "trustbase/prf-wrap/v1";
|
|
56
|
+
/** AAD 前缀(版本化) */
|
|
57
|
+
export declare const PRF_WRAP_AAD_PREFIX = "trustbase-prf-wrap-v1";
|
|
58
|
+
/** HKDF-SHA256 在 WebAuthn PRF 下的典型输出长度(认证器返回 32 字节) */
|
|
59
|
+
export declare const PRF_SECRET_LENGTH = 32;
|
|
60
|
+
/** 可接受的 PRF 秘密长度区间(给非标准认证器留余量,但拒绝明显不对的输入) */
|
|
61
|
+
export declare const PRF_SECRET_MIN_LENGTH = 16;
|
|
62
|
+
export declare const PRF_SECRET_MAX_LENGTH = 64;
|
|
63
|
+
/** HKDF salt 长度区间;默认 16 字节随机 */
|
|
64
|
+
export declare const PRF_SALT_MIN_LENGTH = 8;
|
|
65
|
+
export declare const PRF_SALT_MAX_LENGTH = 64;
|
|
66
|
+
export declare const PRF_SALT_DEFAULT_LENGTH = 16;
|
|
67
|
+
/** 唯一支持的对称加密 */
|
|
68
|
+
export declare const PRF_WRAP_CIPHER = "aes-256-gcm";
|
|
69
|
+
/** 唯一支持的 KDF */
|
|
70
|
+
export declare const PRF_WRAP_KDF = "hkdf-sha256";
|
|
71
|
+
/** 解锁策略:生物识别(PRF)优先,口令回退 —— 两条路解出**同一份**密钥材料 */
|
|
72
|
+
export type UnlockStrategy = 'prf' | 'password';
|
|
73
|
+
export interface PrfWrappedKey {
|
|
74
|
+
version: typeof PRF_WRAP_VERSION;
|
|
75
|
+
kdf: typeof PRF_WRAP_KDF;
|
|
76
|
+
/** WebAuthn 凭据 ID(base64url;`-`/`_` 是合法字符) */
|
|
77
|
+
credentialId: string;
|
|
78
|
+
/** HKDF 的 salt(base64);同时参与 AAD */
|
|
79
|
+
salt: string;
|
|
80
|
+
/** HKDF 的 info(域分隔串) */
|
|
81
|
+
info: string;
|
|
82
|
+
cipher: typeof PRF_WRAP_CIPHER;
|
|
83
|
+
/** IV(base64,12 字节) */
|
|
84
|
+
iv: string;
|
|
85
|
+
ciphertext: string;
|
|
86
|
+
tag: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* PRF 秘密的提供方。**接口是给浏览器适配器与测试假实现共用的**:
|
|
90
|
+
* 一个真实现(prf-webauthn.ts)与一个假实现(`createStaticPrfProvider`)。
|
|
91
|
+
*/
|
|
92
|
+
export interface PrfProvider {
|
|
93
|
+
/** 标明实现来源,便于日志与排查(如 `'webauthn'` / `'static'`) */
|
|
94
|
+
readonly kind: string;
|
|
95
|
+
/** 这台设备/这个浏览器**是否具备** PRF 能力(只探测,不应弹任何 UI) */
|
|
96
|
+
isAvailable(): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* 取回某个凭据下、给定 salt 的 PRF 秘密。
|
|
99
|
+
* 真实现会在这里触发生物识别/PIN 授权;用户取消/失败应当抛错(不要返回全 0)。
|
|
100
|
+
*/
|
|
101
|
+
getPrfSecret(credentialId: string, salt: Uint8Array): Promise<Uint8Array>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* HKDF-SHA256:`PRF 秘密 → 32 字节 KEK`(RFC 5869)。
|
|
105
|
+
*
|
|
106
|
+
* 用 `@noble/hashes/hkdf` 而不是 WebCrypto 的 HKDF:这条路径必须**确定性可复算**
|
|
107
|
+
* (`test/prf.test.ts` 里用 RFC 5869 的公开向量与 `node:crypto.hkdfSync` 双侧对照),
|
|
108
|
+
* 而且 PRF 本身已经是硬件门控的,不差平台这一道。
|
|
109
|
+
*/
|
|
110
|
+
export declare function derivePrfKek(prfSecret: Uint8Array, salt: Uint8Array, info?: string): Uint8Array;
|
|
111
|
+
/** 被包的头的 AAD 文本(格式见文件头;由测试用字面量钉住) */
|
|
112
|
+
export declare function prfWrapAadText(wrapped: Pick<PrfWrappedKey, 'kdf' | 'credentialId' | 'info' | 'salt' | 'cipher' | 'iv'>): string;
|
|
113
|
+
export interface WrapKeyWithPrfOptions {
|
|
114
|
+
/** WebAuthn 凭据 ID(注册时拿到的那串 base64url) */
|
|
115
|
+
credentialId: string;
|
|
116
|
+
/**
|
|
117
|
+
* HKDF salt。**生产环境应当传注册时用的同一份 salt**(它是"这份包装属于
|
|
118
|
+
* 哪个凭据/哪台设备"的一部分);不传则随机生成一份并写进 blob。
|
|
119
|
+
*/
|
|
120
|
+
salt?: Uint8Array;
|
|
121
|
+
/** HKDF info(默认 `trustbase/prf-wrap/v1`);跨应用/跨用途要区分时传不同的值 */
|
|
122
|
+
info?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 用 PRF 秘密包住密钥材料(通常是账户的数据密钥 DEK)。
|
|
126
|
+
*
|
|
127
|
+
* **不修改 `keyMaterial`**(调用方的缓冲区);内部 KEK 在返回前清零。
|
|
128
|
+
*/
|
|
129
|
+
export declare function wrapKeyWithPrf(prfSecret: Uint8Array, keyMaterial: Uint8Array, options: WrapKeyWithPrfOptions): Promise<PrfWrappedKey>;
|
|
130
|
+
/**
|
|
131
|
+
* 用 PRF 秘密拆开包装,拿回密钥材料。
|
|
132
|
+
*
|
|
133
|
+
* PRF 秘密不对(换设备、重装、凭据被删)→ `AUTH_FAILED`,
|
|
134
|
+
* 与"口令错"同一句话:调用方应当据此提示"用口令或助记词解锁"。
|
|
135
|
+
*/
|
|
136
|
+
export declare function unwrapKeyWithPrf(wrapped: PrfWrappedKey | string, prfSecret: Uint8Array): Promise<Uint8Array>;
|
|
137
|
+
/** 校验并归一化一份 PRF 包装(接受对象或 JSON 字符串);不合法抛 `INVALID_BLOB` 等 */
|
|
138
|
+
export declare function parsePrfWrappedKey(value: PrfWrappedKey | string | unknown): PrfWrappedKey;
|
|
139
|
+
/** 只问"能不能当 PRF 包装用",不抛异常 */
|
|
140
|
+
export declare function isPrfWrappedKey(value: unknown): value is PrfWrappedKey;
|
|
141
|
+
/** 序列化(字段顺序固定,便于跨端逐字节比对) */
|
|
142
|
+
export declare function serializePrfWrappedKey(wrapped: PrfWrappedKey): string;
|
|
143
|
+
/**
|
|
144
|
+
* 用固定秘密伪造一个 `PrfProvider`。
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ **仅供测试与本地开发**:它没有任何硬件保护、不要求生物识别,秘密就在
|
|
147
|
+
* 进程内存里。生产环境必须用 prf-webauthn.ts 的真适配器(或原生侧的对应实现)。
|
|
148
|
+
* 之所以放在 src 而不是 test:它是"接口可注入"这条设计的证明,
|
|
149
|
+
* 也让上层(PWA 的降级路径、CLI 的自动化)有一个明确的替身可用。
|
|
150
|
+
*/
|
|
151
|
+
export declare function createStaticPrfProvider(secret: Uint8Array, options?: {
|
|
152
|
+
available?: boolean;
|
|
153
|
+
kind?: string;
|
|
154
|
+
}): PrfProvider;
|
|
155
|
+
/** 常数时间比对两份 PRF 秘密(给适配器做"秘密是否变化"自检用) */
|
|
156
|
+
export declare function isSameSecret(a: Uint8Array, b: Uint8Array): boolean;
|
|
157
|
+
//# sourceMappingURL=prf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prf.d.ts","sourceRoot":"","sources":["../src/prf.ts"],"names":[],"mappings":"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"}
|