@cryptforge/auth 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 +914 -0
- package/dist/index.d.mts +372 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +1198 -0
- package/dist/index.mjs +1174 -0
- package/package.json +44 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1174 @@
|
|
|
1
|
+
// src/AuthClient.ts
|
|
2
|
+
import {
|
|
3
|
+
generateMnemonic,
|
|
4
|
+
validateMnemonic,
|
|
5
|
+
mnemonicToSeedSync
|
|
6
|
+
} from "@scure/bip39";
|
|
7
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
8
|
+
import { HDKey } from "@scure/bip32";
|
|
9
|
+
var AuthClient = class {
|
|
10
|
+
state = {
|
|
11
|
+
identity: null,
|
|
12
|
+
keys: null,
|
|
13
|
+
currentChain: null,
|
|
14
|
+
isLocked: true
|
|
15
|
+
};
|
|
16
|
+
listeners = /* @__PURE__ */ new Set();
|
|
17
|
+
lockTimer;
|
|
18
|
+
decryptedMnemonic = null;
|
|
19
|
+
// Blockchain adapter registry
|
|
20
|
+
adapters = /* @__PURE__ */ new Map();
|
|
21
|
+
currentAdapter = null;
|
|
22
|
+
/**
|
|
23
|
+
* Register a blockchain adapter for a specific chain
|
|
24
|
+
* @param chainId - Unique chain identifier (e.g., 'ethereum', 'bitcoin')
|
|
25
|
+
* @param adapter - BlockchainAdapter implementation
|
|
26
|
+
*/
|
|
27
|
+
registerAdapter = (chainId, adapter) => {
|
|
28
|
+
this.adapters.set(chainId, adapter);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get the adapter for a specific chain
|
|
32
|
+
* @param chainId - Chain identifier
|
|
33
|
+
* @returns BlockchainAdapter instance
|
|
34
|
+
* @throws Error if no adapter is registered for the chain
|
|
35
|
+
*/
|
|
36
|
+
getAdapter(chainId) {
|
|
37
|
+
const adapter = this.adapters.get(chainId);
|
|
38
|
+
if (!adapter) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`No adapter registered for chain: ${chainId}. Please register an adapter using registerAdapter().`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return adapter;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get all registered chain IDs
|
|
47
|
+
* @returns Array of registered chain IDs
|
|
48
|
+
*/
|
|
49
|
+
getRegisteredChains = () => {
|
|
50
|
+
return Array.from(this.adapters.keys());
|
|
51
|
+
};
|
|
52
|
+
// Identity Creation & Restoration
|
|
53
|
+
/**
|
|
54
|
+
* Generates a BIP39 mnemonic phrase (12 or 24 words).
|
|
55
|
+
* @param options - Optional configuration for word count
|
|
56
|
+
* @returns BIP39 mnemonic phrase
|
|
57
|
+
*/
|
|
58
|
+
generateMnemonic = (options) => {
|
|
59
|
+
const wordCount = options?.wordCount || 12;
|
|
60
|
+
const strength = wordCount === 24 ? 256 : 128;
|
|
61
|
+
return generateMnemonic(wordlist, strength);
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Creates a new identity from a mnemonic and encrypts it with a password.
|
|
65
|
+
* Optionally unlocks with a specific blockchain.
|
|
66
|
+
* @param options - Identity creation options including mnemonic, password, and optional chainId
|
|
67
|
+
* @returns Promise resolving to the created identity and keys
|
|
68
|
+
* @throws {Error} If mnemonic is invalid
|
|
69
|
+
*/
|
|
70
|
+
createIdentity = async (options) => {
|
|
71
|
+
const { mnemonic, password, label, metadata = {}, chainId } = options;
|
|
72
|
+
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
73
|
+
throw new Error("Invalid mnemonic");
|
|
74
|
+
}
|
|
75
|
+
const fingerprint = getMasterFingerprint(mnemonic);
|
|
76
|
+
const publicKey = getMasterPublicKey(mnemonic);
|
|
77
|
+
const id = "identity_" + fingerprint;
|
|
78
|
+
const keystoreJson = await encryptMnemonic(mnemonic, password);
|
|
79
|
+
const storedIdentity = {
|
|
80
|
+
id,
|
|
81
|
+
fingerprint,
|
|
82
|
+
publicKey,
|
|
83
|
+
label: label || "Unnamed Wallet",
|
|
84
|
+
metadata,
|
|
85
|
+
keystore: keystoreJson,
|
|
86
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
87
|
+
lastAccess: (/* @__PURE__ */ new Date()).toISOString()
|
|
88
|
+
};
|
|
89
|
+
await saveIdentityToDB(storedIdentity);
|
|
90
|
+
const identity = {
|
|
91
|
+
id: storedIdentity.id,
|
|
92
|
+
publicKey: storedIdentity.publicKey,
|
|
93
|
+
fingerprint: storedIdentity.fingerprint,
|
|
94
|
+
label: storedIdentity.label,
|
|
95
|
+
metadata: storedIdentity.metadata,
|
|
96
|
+
createdAt: new Date(storedIdentity.createdAt),
|
|
97
|
+
lastAccess: storedIdentity.lastAccess ? new Date(storedIdentity.lastAccess) : void 0
|
|
98
|
+
};
|
|
99
|
+
this.state.identity = identity;
|
|
100
|
+
this.decryptedMnemonic = mnemonic;
|
|
101
|
+
let keys = null;
|
|
102
|
+
let chain = null;
|
|
103
|
+
if (chainId) {
|
|
104
|
+
const adapter = this.getAdapter(chainId);
|
|
105
|
+
keys = await this.deriveKeysWithAdapter(
|
|
106
|
+
mnemonic,
|
|
107
|
+
chainId,
|
|
108
|
+
adapter,
|
|
109
|
+
identity
|
|
110
|
+
);
|
|
111
|
+
chain = {
|
|
112
|
+
id: chainId,
|
|
113
|
+
name: adapter.chainData.name,
|
|
114
|
+
symbol: adapter.chainData.symbol
|
|
115
|
+
};
|
|
116
|
+
this.state.keys = keys;
|
|
117
|
+
this.state.currentChain = chain;
|
|
118
|
+
this.currentAdapter = adapter;
|
|
119
|
+
this.state.isLocked = false;
|
|
120
|
+
} else {
|
|
121
|
+
this.state.isLocked = true;
|
|
122
|
+
}
|
|
123
|
+
this.notifyListeners("IDENTITY_CREATED", keys);
|
|
124
|
+
return { identity, keys };
|
|
125
|
+
};
|
|
126
|
+
// Key Management
|
|
127
|
+
/**
|
|
128
|
+
* Unlocks the wallet by decrypting the keystore and deriving keys for a blockchain.
|
|
129
|
+
* @param options - Unlock options including password, chainId, and optional duration
|
|
130
|
+
* @returns Promise resolving to the derived keys
|
|
131
|
+
* @throws {Error} If password is incorrect or no identity is selected
|
|
132
|
+
*/
|
|
133
|
+
unlock = async (options) => {
|
|
134
|
+
const { password, identityId, chainId, duration } = options;
|
|
135
|
+
let stored;
|
|
136
|
+
if (identityId) {
|
|
137
|
+
stored = await getIdentityFromDB(identityId);
|
|
138
|
+
} else if (this.state.identity) {
|
|
139
|
+
stored = await getIdentityFromDB(this.state.identity.id);
|
|
140
|
+
}
|
|
141
|
+
if (!stored) throw new Error("No identity selected");
|
|
142
|
+
const mnemonic = await decryptMnemonic(stored.keystore, password);
|
|
143
|
+
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
144
|
+
throw new Error("Decrypted mnemonic is invalid");
|
|
145
|
+
}
|
|
146
|
+
this.decryptedMnemonic = mnemonic;
|
|
147
|
+
const identity = {
|
|
148
|
+
id: stored.id,
|
|
149
|
+
publicKey: stored.publicKey,
|
|
150
|
+
fingerprint: stored.fingerprint,
|
|
151
|
+
label: stored.label,
|
|
152
|
+
metadata: stored.metadata,
|
|
153
|
+
createdAt: new Date(stored.createdAt),
|
|
154
|
+
lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
|
|
155
|
+
};
|
|
156
|
+
if (!chainId) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"chainId is required to unlock. Please specify which blockchain to use."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const adapter = this.getAdapter(chainId);
|
|
162
|
+
const keys = await this.deriveKeysWithAdapter(
|
|
163
|
+
mnemonic,
|
|
164
|
+
chainId,
|
|
165
|
+
adapter,
|
|
166
|
+
identity,
|
|
167
|
+
duration
|
|
168
|
+
// Pass duration to keys
|
|
169
|
+
);
|
|
170
|
+
const targetChain = {
|
|
171
|
+
id: chainId,
|
|
172
|
+
name: adapter.chainData.name,
|
|
173
|
+
symbol: adapter.chainData.symbol
|
|
174
|
+
};
|
|
175
|
+
this.state.identity = identity;
|
|
176
|
+
this.state.keys = keys;
|
|
177
|
+
this.state.currentChain = targetChain;
|
|
178
|
+
this.currentAdapter = adapter;
|
|
179
|
+
this.state.isLocked = false;
|
|
180
|
+
stored.lastAccess = (/* @__PURE__ */ new Date()).toISOString();
|
|
181
|
+
await saveIdentityToDB(stored);
|
|
182
|
+
if (duration) {
|
|
183
|
+
this.setAutoLockTimer(duration);
|
|
184
|
+
}
|
|
185
|
+
this.notifyListeners("UNLOCKED", keys);
|
|
186
|
+
return { keys };
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Locks the wallet and clears all keys from memory.
|
|
190
|
+
* The encrypted keystore remains in IndexedDB.
|
|
191
|
+
* @returns Promise that resolves when wallet is locked
|
|
192
|
+
*/
|
|
193
|
+
lock = async () => {
|
|
194
|
+
if (this.decryptedMnemonic) {
|
|
195
|
+
this.decryptedMnemonic = null;
|
|
196
|
+
}
|
|
197
|
+
this.state.keys = null;
|
|
198
|
+
this.state.isLocked = true;
|
|
199
|
+
if (this.lockTimer) {
|
|
200
|
+
clearTimeout(this.lockTimer);
|
|
201
|
+
this.lockTimer = void 0;
|
|
202
|
+
}
|
|
203
|
+
this.notifyListeners("LOCKED", null);
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Switches to a different blockchain while preserving the same identity.
|
|
207
|
+
* @param chainId - Target blockchain identifier
|
|
208
|
+
* @param password - Optional password if wallet is locked
|
|
209
|
+
* @returns Promise resolving to keys for the new chain
|
|
210
|
+
* @throws {Error} If locked without password or adapter not registered
|
|
211
|
+
*/
|
|
212
|
+
switchChain = async (chainId, password) => {
|
|
213
|
+
if (this.state.isLocked && !password) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Identity is locked. Password required to switch chains."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
let mnemonic = this.decryptedMnemonic;
|
|
219
|
+
if (!mnemonic && password && this.state.identity) {
|
|
220
|
+
const stored = await getIdentityFromDB(this.state.identity.id);
|
|
221
|
+
if (!stored) throw new Error("Identity not found");
|
|
222
|
+
mnemonic = await decryptMnemonic(stored.keystore, password);
|
|
223
|
+
this.decryptedMnemonic = mnemonic;
|
|
224
|
+
}
|
|
225
|
+
if (!mnemonic || !this.state.identity) {
|
|
226
|
+
throw new Error("No active identity or mnemonic");
|
|
227
|
+
}
|
|
228
|
+
const adapter = this.getAdapter(chainId);
|
|
229
|
+
const duration = this.state.keys ? this.state.keys.expiresAt.getTime() - Date.now() : void 0;
|
|
230
|
+
const keys = await this.deriveKeysWithAdapter(
|
|
231
|
+
mnemonic,
|
|
232
|
+
chainId,
|
|
233
|
+
adapter,
|
|
234
|
+
this.state.identity,
|
|
235
|
+
duration && duration > 0 ? duration : void 0
|
|
236
|
+
);
|
|
237
|
+
const newChain = {
|
|
238
|
+
id: chainId,
|
|
239
|
+
name: adapter.chainData.name,
|
|
240
|
+
symbol: adapter.chainData.symbol
|
|
241
|
+
};
|
|
242
|
+
this.state.keys = keys;
|
|
243
|
+
this.state.currentChain = newChain;
|
|
244
|
+
this.currentAdapter = adapter;
|
|
245
|
+
this.state.isLocked = false;
|
|
246
|
+
this.notifyListeners("CHAIN_SWITCHED", keys);
|
|
247
|
+
return { keys };
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Rotates to a new set of keys at the next address index or custom path.
|
|
251
|
+
* Preserves key expiration times from current session.
|
|
252
|
+
* @param newDerivationPath - Optional custom BIP44 path (defaults to next index)
|
|
253
|
+
* @returns Promise resolving to the new keys
|
|
254
|
+
* @throws {Error} If wallet is locked
|
|
255
|
+
*/
|
|
256
|
+
rotateKeys = async (newDerivationPath) => {
|
|
257
|
+
if (this.state.isLocked || !this.decryptedMnemonic || !this.state.identity || !this.currentAdapter || !this.state.currentChain) {
|
|
258
|
+
throw new Error("Identity must be unlocked to rotate keys");
|
|
259
|
+
}
|
|
260
|
+
if (newDerivationPath) {
|
|
261
|
+
const keyData2 = await this.currentAdapter.deriveKeysAtPath(
|
|
262
|
+
this.decryptedMnemonic,
|
|
263
|
+
newDerivationPath
|
|
264
|
+
);
|
|
265
|
+
const expiresAt2 = this.state.keys?.expiresAt || new Date(Date.now() + 36e5);
|
|
266
|
+
const expiresIn2 = this.state.keys?.expiresIn || 3600;
|
|
267
|
+
const keys2 = {
|
|
268
|
+
privateKey: keyData2.privateKey,
|
|
269
|
+
privateKeyHex: keyData2.privateKeyHex,
|
|
270
|
+
publicKey: keyData2.publicKey,
|
|
271
|
+
publicKeyHex: keyData2.publicKeyHex,
|
|
272
|
+
address: keyData2.address,
|
|
273
|
+
derivationPath: keyData2.path,
|
|
274
|
+
chain: this.state.currentChain,
|
|
275
|
+
expiresAt: expiresAt2,
|
|
276
|
+
expiresIn: expiresIn2,
|
|
277
|
+
identity: this.state.identity
|
|
278
|
+
};
|
|
279
|
+
this.state.keys = keys2;
|
|
280
|
+
this.notifyListeners("KEYS_ROTATED", keys2);
|
|
281
|
+
return { keys: keys2 };
|
|
282
|
+
}
|
|
283
|
+
const currentPath = this.state.keys?.derivationPath || "";
|
|
284
|
+
const pathParts = currentPath.split("/");
|
|
285
|
+
const lastIndex = parseInt(pathParts[pathParts.length - 1]) || 0;
|
|
286
|
+
const newIndex = lastIndex + 1;
|
|
287
|
+
const keyData = await this.currentAdapter.deriveKeysAtIndex(
|
|
288
|
+
this.decryptedMnemonic,
|
|
289
|
+
newIndex
|
|
290
|
+
);
|
|
291
|
+
const expiresAt = this.state.keys?.expiresAt || new Date(Date.now() + 36e5);
|
|
292
|
+
const expiresIn = this.state.keys?.expiresIn || 3600;
|
|
293
|
+
const keys = {
|
|
294
|
+
privateKey: keyData.privateKey,
|
|
295
|
+
privateKeyHex: keyData.privateKeyHex,
|
|
296
|
+
publicKey: keyData.publicKey,
|
|
297
|
+
publicKeyHex: keyData.publicKeyHex,
|
|
298
|
+
address: keyData.address,
|
|
299
|
+
derivationPath: keyData.path,
|
|
300
|
+
chain: this.state.currentChain,
|
|
301
|
+
expiresAt,
|
|
302
|
+
expiresIn,
|
|
303
|
+
identity: this.state.identity
|
|
304
|
+
};
|
|
305
|
+
this.state.keys = keys;
|
|
306
|
+
this.notifyListeners("KEYS_ROTATED", keys);
|
|
307
|
+
return { keys };
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Derives a one-time key at a custom BIP44 path without storing it in the session.
|
|
311
|
+
* Useful for signing with different addresses or accounts.
|
|
312
|
+
* @param options - Derivation options with custom path
|
|
313
|
+
* @returns Promise resolving to the derived key data
|
|
314
|
+
* @throws {Error} If wallet is locked
|
|
315
|
+
*/
|
|
316
|
+
deriveKey = async (options) => {
|
|
317
|
+
if (this.state.isLocked || !this.decryptedMnemonic || !this.currentAdapter) {
|
|
318
|
+
throw new Error("Identity must be unlocked to derive keys");
|
|
319
|
+
}
|
|
320
|
+
const keyData = await this.currentAdapter.deriveKeysAtPath(
|
|
321
|
+
this.decryptedMnemonic,
|
|
322
|
+
options.path
|
|
323
|
+
);
|
|
324
|
+
return {
|
|
325
|
+
privateKey: keyData.privateKeyHex,
|
|
326
|
+
publicKey: keyData.publicKeyHex,
|
|
327
|
+
address: keyData.address,
|
|
328
|
+
path: keyData.path
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Derives a deterministic document ID using BIP44-style hierarchical derivation.
|
|
333
|
+
* Returns a hex-encoded ID (32 bytes / 64 hex characters).
|
|
334
|
+
* Path: m/44'/[appId]'/[account]'/[purpose]/[index]
|
|
335
|
+
* @param options - BIP44 derivation parameters (appId required, others default to 0)
|
|
336
|
+
* @returns Promise resolving to hex-encoded document ID (64 characters)
|
|
337
|
+
* @throws {Error} If wallet is locked or parameters are out of range
|
|
338
|
+
*/
|
|
339
|
+
deriveBIP44DocumentID = async (options) => {
|
|
340
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Wallet is locked. Call unlock() first to derive document IDs."
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const { appId, account = 0, purpose = 0, index = 0 } = options;
|
|
346
|
+
if (appId === void 0 || appId === null) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
"appId is required but was undefined. Please provide a valid appId number."
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (typeof appId !== "number" || isNaN(appId)) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`appId must be a valid number, received: ${typeof appId}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const MAX_HARDENED = 2147483647;
|
|
357
|
+
if (appId < 0 || appId > MAX_HARDENED) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Invalid appId: ${appId}. Must be between 0 and ${MAX_HARDENED}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (account < 0 || account > MAX_HARDENED) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Invalid account: ${account}. Must be between 0 and ${MAX_HARDENED}`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (purpose < 0 || purpose > MAX_HARDENED) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Invalid purpose: ${purpose}. Must be between 0 and ${MAX_HARDENED}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (index < 0 || index > MAX_HARDENED) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Invalid index: ${index}. Must be between 0 and ${MAX_HARDENED}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const path = `m/44'/${appId}'/${account}'/${purpose}/${index}`;
|
|
378
|
+
const seed = mnemonicToSeedSync(this.decryptedMnemonic);
|
|
379
|
+
const masterKey = HDKey.fromMasterSeed(seed);
|
|
380
|
+
const derivedKey = masterKey.derive(path);
|
|
381
|
+
if (!derivedKey.publicKey) {
|
|
382
|
+
throw new Error(`Failed to derive key at path: ${path}`);
|
|
383
|
+
}
|
|
384
|
+
const publicKeyBuffer = new Uint8Array(derivedKey.publicKey);
|
|
385
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", publicKeyBuffer);
|
|
386
|
+
const hash = new Uint8Array(hashBuffer);
|
|
387
|
+
return bufferToHex(hash);
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Derives a data encryption key using HKDF for encrypting/decrypting data.
|
|
391
|
+
* This key is deterministic (derived from mnemonic) and chain-independent.
|
|
392
|
+
* Perfect for document encryption, file storage, etc.
|
|
393
|
+
*
|
|
394
|
+
* @param options - Derivation options including purpose, version, algorithm
|
|
395
|
+
* @returns CryptoKey ready for use with Web Crypto API
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* const key = await auth.deriveDataEncryptionKey({
|
|
400
|
+
* purpose: 'automerge-documents',
|
|
401
|
+
* version: 1,
|
|
402
|
+
* });
|
|
403
|
+
*
|
|
404
|
+
* // Use with Web Crypto API
|
|
405
|
+
* const encrypted = await crypto.subtle.encrypt(
|
|
406
|
+
* { name: 'AES-GCM', iv },
|
|
407
|
+
* key,
|
|
408
|
+
* data
|
|
409
|
+
* );
|
|
410
|
+
* ```
|
|
411
|
+
*/
|
|
412
|
+
deriveDataEncryptionKey = async (options) => {
|
|
413
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
"Wallet is locked. Call unlock() first to derive data encryption keys."
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const purpose = options.purpose;
|
|
419
|
+
const version = options.version || 1;
|
|
420
|
+
const algorithm = options.algorithm || "AES-GCM";
|
|
421
|
+
const length = options.length || 256;
|
|
422
|
+
const extractable = options.extractable || false;
|
|
423
|
+
const seed = mnemonicToSeedSync(this.decryptedMnemonic);
|
|
424
|
+
const info = new TextEncoder().encode(`CryptForge-${purpose}-v${version}`);
|
|
425
|
+
const seedBuffer = new Uint8Array(seed);
|
|
426
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
427
|
+
"raw",
|
|
428
|
+
seedBuffer,
|
|
429
|
+
"HKDF",
|
|
430
|
+
false,
|
|
431
|
+
["deriveKey"]
|
|
432
|
+
);
|
|
433
|
+
const key = await crypto.subtle.deriveKey(
|
|
434
|
+
{
|
|
435
|
+
name: "HKDF",
|
|
436
|
+
hash: "SHA-256",
|
|
437
|
+
salt: new Uint8Array(),
|
|
438
|
+
// Empty salt (could be customized if needed)
|
|
439
|
+
info
|
|
440
|
+
},
|
|
441
|
+
keyMaterial,
|
|
442
|
+
{
|
|
443
|
+
name: algorithm,
|
|
444
|
+
length
|
|
445
|
+
},
|
|
446
|
+
extractable,
|
|
447
|
+
["encrypt", "decrypt"]
|
|
448
|
+
);
|
|
449
|
+
return key;
|
|
450
|
+
};
|
|
451
|
+
/**
|
|
452
|
+
* Gets the address for a specific blockchain at a given index.
|
|
453
|
+
* @param chainId - Blockchain identifier
|
|
454
|
+
* @param index - Address index (default: 0)
|
|
455
|
+
* @returns Promise resolving to address, public key, and derivation path
|
|
456
|
+
* @throws {Error} If wallet is locked or adapter not registered
|
|
457
|
+
*/
|
|
458
|
+
getAddressForChain = async (chainId, index = 0) => {
|
|
459
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
460
|
+
throw new Error("Identity must be unlocked to get addresses");
|
|
461
|
+
}
|
|
462
|
+
const adapter = this.getAdapter(chainId);
|
|
463
|
+
const result = await adapter.getAddressAtIndex(
|
|
464
|
+
this.decryptedMnemonic,
|
|
465
|
+
index
|
|
466
|
+
);
|
|
467
|
+
return {
|
|
468
|
+
address: result.address,
|
|
469
|
+
publicKey: result.publicKey,
|
|
470
|
+
derivationPath: result.path
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* Verify password without unlocking wallet
|
|
475
|
+
* Useful for transaction confirmation
|
|
476
|
+
* @param password - Password to verify
|
|
477
|
+
* @returns Promise resolving to true if password is correct
|
|
478
|
+
*/
|
|
479
|
+
verifyPassword = async (password) => {
|
|
480
|
+
if (!this.state.identity) {
|
|
481
|
+
throw new Error("No identity selected");
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const stored = await getIdentityFromDB(this.state.identity.id);
|
|
485
|
+
if (!stored) return false;
|
|
486
|
+
await decryptMnemonic(stored.keystore, password);
|
|
487
|
+
return true;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
// Cryptographic Operations
|
|
493
|
+
/**
|
|
494
|
+
* Signs a message using the current or specified private key.
|
|
495
|
+
* Performs soft expiration check (warns but doesn't block).
|
|
496
|
+
* @param options - Message and optional derivation path
|
|
497
|
+
* @returns Promise resolving to signature, address, and public key
|
|
498
|
+
* @throws {Error} If wallet is locked
|
|
499
|
+
*/
|
|
500
|
+
signMessage = async (options) => {
|
|
501
|
+
if (this.state.isLocked || !this.state.keys || !this.currentAdapter) {
|
|
502
|
+
throw new Error("Identity must be unlocked to sign messages");
|
|
503
|
+
}
|
|
504
|
+
this.checkKeysExpiration();
|
|
505
|
+
let privateKey = this.state.keys.privateKey;
|
|
506
|
+
let address = this.state.keys.address;
|
|
507
|
+
let publicKey = this.state.keys.publicKeyHex;
|
|
508
|
+
if (options.derivationPath && this.decryptedMnemonic) {
|
|
509
|
+
const keyData = await this.currentAdapter.deriveKeysAtPath(
|
|
510
|
+
this.decryptedMnemonic,
|
|
511
|
+
options.derivationPath
|
|
512
|
+
);
|
|
513
|
+
privateKey = keyData.privateKey;
|
|
514
|
+
address = keyData.address;
|
|
515
|
+
publicKey = keyData.publicKeyHex;
|
|
516
|
+
}
|
|
517
|
+
const result = await this.currentAdapter.signMessage(
|
|
518
|
+
privateKey,
|
|
519
|
+
options.message
|
|
520
|
+
);
|
|
521
|
+
return {
|
|
522
|
+
signature: result.signature,
|
|
523
|
+
address,
|
|
524
|
+
publicKey
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* Signs a blockchain transaction using the current or specified private key.
|
|
529
|
+
* Performs soft expiration check (warns but doesn't block).
|
|
530
|
+
* @param options - Transaction object and optional derivation path
|
|
531
|
+
* @returns Promise resolving to signed transaction and signature
|
|
532
|
+
* @throws {Error} If wallet is locked
|
|
533
|
+
*/
|
|
534
|
+
signTransaction = async (options) => {
|
|
535
|
+
if (this.state.isLocked || !this.state.keys || !this.currentAdapter) {
|
|
536
|
+
throw new Error("Identity must be unlocked to sign transactions");
|
|
537
|
+
}
|
|
538
|
+
this.checkKeysExpiration();
|
|
539
|
+
let privateKey = this.state.keys.privateKey;
|
|
540
|
+
if (options.derivationPath && this.decryptedMnemonic) {
|
|
541
|
+
const keyData = await this.currentAdapter.deriveKeysAtPath(
|
|
542
|
+
this.decryptedMnemonic,
|
|
543
|
+
options.derivationPath
|
|
544
|
+
);
|
|
545
|
+
privateKey = keyData.privateKey;
|
|
546
|
+
}
|
|
547
|
+
const result = await this.currentAdapter.signTransaction(
|
|
548
|
+
privateKey,
|
|
549
|
+
options.transaction
|
|
550
|
+
);
|
|
551
|
+
return {
|
|
552
|
+
signedTransaction: result.signedTransaction,
|
|
553
|
+
signature: result.signature
|
|
554
|
+
};
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Verifies a signature against a message and public key using the current adapter.
|
|
558
|
+
* @param message - Message that was signed (string or bytes)
|
|
559
|
+
* @param signature - Signature to verify
|
|
560
|
+
* @param publicKey - Public key to verify against
|
|
561
|
+
* @returns Promise resolving to true if signature is valid
|
|
562
|
+
* @throws {Error} If no adapter is selected
|
|
563
|
+
*/
|
|
564
|
+
verifySignature = async (message, signature, publicKey) => {
|
|
565
|
+
if (!this.currentAdapter) {
|
|
566
|
+
throw new Error("No adapter selected. Please unlock with a chain first.");
|
|
567
|
+
}
|
|
568
|
+
return this.currentAdapter.verifySignature(message, signature, publicKey);
|
|
569
|
+
};
|
|
570
|
+
// Identity Management
|
|
571
|
+
/**
|
|
572
|
+
* Lists all identities stored in IndexedDB.
|
|
573
|
+
* @returns Promise resolving to array of all identities
|
|
574
|
+
*/
|
|
575
|
+
listIdentities = async () => {
|
|
576
|
+
const stored = await getAllIdentitiesFromDB();
|
|
577
|
+
return stored.map((s) => ({
|
|
578
|
+
id: s.id,
|
|
579
|
+
publicKey: s.publicKey,
|
|
580
|
+
fingerprint: s.fingerprint,
|
|
581
|
+
label: s.label,
|
|
582
|
+
metadata: s.metadata,
|
|
583
|
+
createdAt: new Date(s.createdAt),
|
|
584
|
+
lastAccess: s.lastAccess ? new Date(s.lastAccess) : void 0
|
|
585
|
+
}));
|
|
586
|
+
};
|
|
587
|
+
/**
|
|
588
|
+
* Switches to a different identity. Locks the current wallet first.
|
|
589
|
+
* @param identityId - ID of the identity to switch to
|
|
590
|
+
* @returns Promise resolving to the selected identity
|
|
591
|
+
* @throws {Error} If identity not found
|
|
592
|
+
*/
|
|
593
|
+
switchIdentity = async (identityId) => {
|
|
594
|
+
const stored = await getIdentityFromDB(identityId);
|
|
595
|
+
if (!stored) throw new Error("Identity not found");
|
|
596
|
+
await this.lock();
|
|
597
|
+
const identity = {
|
|
598
|
+
id: stored.id,
|
|
599
|
+
publicKey: stored.publicKey,
|
|
600
|
+
fingerprint: stored.fingerprint,
|
|
601
|
+
label: stored.label,
|
|
602
|
+
metadata: stored.metadata,
|
|
603
|
+
createdAt: new Date(stored.createdAt),
|
|
604
|
+
lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
|
|
605
|
+
};
|
|
606
|
+
this.state.identity = identity;
|
|
607
|
+
this.notifyListeners("IDENTITY_SWITCHED", null);
|
|
608
|
+
return { identity };
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* Updates the label or metadata for an identity.
|
|
612
|
+
* @param identityId - ID of the identity to update
|
|
613
|
+
* @param updates - Label and/or metadata to update
|
|
614
|
+
* @returns Promise resolving to the updated identity
|
|
615
|
+
* @throws {Error} If identity not found
|
|
616
|
+
*/
|
|
617
|
+
updateIdentity = async (identityId, updates) => {
|
|
618
|
+
const stored = await getIdentityFromDB(identityId);
|
|
619
|
+
if (!stored) throw new Error("Identity not found");
|
|
620
|
+
if (updates.label !== void 0) {
|
|
621
|
+
stored.label = updates.label;
|
|
622
|
+
}
|
|
623
|
+
if (updates.metadata !== void 0) {
|
|
624
|
+
stored.metadata = { ...stored.metadata, ...updates.metadata };
|
|
625
|
+
}
|
|
626
|
+
await saveIdentityToDB(stored);
|
|
627
|
+
const identity = {
|
|
628
|
+
id: stored.id,
|
|
629
|
+
publicKey: stored.publicKey,
|
|
630
|
+
fingerprint: stored.fingerprint,
|
|
631
|
+
label: stored.label,
|
|
632
|
+
metadata: stored.metadata,
|
|
633
|
+
createdAt: new Date(stored.createdAt),
|
|
634
|
+
lastAccess: stored.lastAccess ? new Date(stored.lastAccess) : void 0
|
|
635
|
+
};
|
|
636
|
+
if (this.state.identity?.id === identityId) {
|
|
637
|
+
this.state.identity = identity;
|
|
638
|
+
}
|
|
639
|
+
this.notifyListeners("IDENTITY_UPDATED", this.state.keys);
|
|
640
|
+
return { identity };
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Permanently deletes an identity from IndexedDB. Requires password verification.
|
|
644
|
+
* Locks wallet if deleting current identity.
|
|
645
|
+
* @param identityId - ID of the identity to delete
|
|
646
|
+
* @param password - Password to verify ownership
|
|
647
|
+
* @returns Promise that resolves when deleted
|
|
648
|
+
* @throws {Error} If identity not found or password incorrect
|
|
649
|
+
*/
|
|
650
|
+
deleteIdentity = async (identityId, password) => {
|
|
651
|
+
const stored = await getIdentityFromDB(identityId);
|
|
652
|
+
if (!stored) throw new Error("Identity not found");
|
|
653
|
+
try {
|
|
654
|
+
await decryptMnemonic(stored.keystore, password);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
throw new Error("Invalid password");
|
|
657
|
+
}
|
|
658
|
+
if (this.state.identity?.id === identityId) {
|
|
659
|
+
await this.lock();
|
|
660
|
+
this.state.identity = null;
|
|
661
|
+
}
|
|
662
|
+
await deleteIdentityFromDB(identityId);
|
|
663
|
+
this.notifyListeners("IDENTITY_DELETED", this.state.keys);
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* Changes the password for an identity's encrypted keystore.
|
|
667
|
+
* Re-encrypts the mnemonic with the new password.
|
|
668
|
+
* @param identityId - ID of the identity
|
|
669
|
+
* @param oldPassword - Current password
|
|
670
|
+
* @param newPassword - New password
|
|
671
|
+
* @returns Promise that resolves when password is changed
|
|
672
|
+
* @throws {Error} If identity not found or old password incorrect
|
|
673
|
+
*/
|
|
674
|
+
changePassword = async (identityId, oldPassword, newPassword) => {
|
|
675
|
+
const stored = await getIdentityFromDB(identityId);
|
|
676
|
+
if (!stored) throw new Error("Identity not found");
|
|
677
|
+
const mnemonic = await decryptMnemonic(stored.keystore, oldPassword);
|
|
678
|
+
const newKeystore = await encryptMnemonic(mnemonic, newPassword);
|
|
679
|
+
stored.keystore = newKeystore;
|
|
680
|
+
await saveIdentityToDB(stored);
|
|
681
|
+
this.notifyListeners("PASSWORD_CHANGED", this.state.keys);
|
|
682
|
+
};
|
|
683
|
+
// Import/Export
|
|
684
|
+
/**
|
|
685
|
+
* Exports the mnemonic phrase for an identity.
|
|
686
|
+
* Requires password verification to decrypt the keystore.
|
|
687
|
+
* ⚠️ Security: Only export mnemonics to secure locations (password managers, paper backup).
|
|
688
|
+
* @param identityId - ID of the identity to export
|
|
689
|
+
* @param password - Password to decrypt the keystore
|
|
690
|
+
* @returns Promise resolving to the mnemonic phrase
|
|
691
|
+
* @throws {Error} If identity not found or password incorrect
|
|
692
|
+
*/
|
|
693
|
+
exportMnemonic = async (identityId, password) => {
|
|
694
|
+
const stored = await getIdentityFromDB(identityId);
|
|
695
|
+
if (!stored) throw new Error("Identity not found");
|
|
696
|
+
const mnemonic = await decryptMnemonic(stored.keystore, password);
|
|
697
|
+
return mnemonic;
|
|
698
|
+
};
|
|
699
|
+
/**
|
|
700
|
+
* Exports the encrypted keystore JSON for an identity.
|
|
701
|
+
* No password required - keystore is already encrypted.
|
|
702
|
+
* Perfect for encrypted backup files.
|
|
703
|
+
* @param identityId - ID of the identity to export
|
|
704
|
+
* @returns Promise resolving to encrypted keystore JSON string
|
|
705
|
+
* @throws {Error} If identity not found
|
|
706
|
+
*/
|
|
707
|
+
exportKeystore = async (identityId) => {
|
|
708
|
+
const stored = await getIdentityFromDB(identityId);
|
|
709
|
+
if (!stored) throw new Error("Identity not found");
|
|
710
|
+
return stored.keystore;
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Exports the complete StoredIdentity object including metadata.
|
|
714
|
+
* No password required - keystore within is already encrypted.
|
|
715
|
+
* Perfect for migrating identities between devices/browsers with all metadata intact.
|
|
716
|
+
* @param identityId - ID of the identity to export
|
|
717
|
+
* @returns Promise resolving to complete StoredIdentity object
|
|
718
|
+
* @throws {Error} If identity not found
|
|
719
|
+
*/
|
|
720
|
+
exportIdentity = async (identityId) => {
|
|
721
|
+
const stored = await getIdentityFromDB(identityId);
|
|
722
|
+
if (!stored) throw new Error("Identity not found");
|
|
723
|
+
return stored;
|
|
724
|
+
};
|
|
725
|
+
/**
|
|
726
|
+
* Imports an identity from a mnemonic phrase.
|
|
727
|
+
* Creates and encrypts a new identity in IndexedDB with the provided password.
|
|
728
|
+
* @param mnemonic - BIP39 mnemonic phrase (12 or 24 words)
|
|
729
|
+
* @param password - Password to encrypt the new identity
|
|
730
|
+
* @param label - Optional label for the wallet (default: "Imported Wallet")
|
|
731
|
+
* @returns Promise resolving to the created identity
|
|
732
|
+
* @throws {Error} If mnemonic is invalid
|
|
733
|
+
*/
|
|
734
|
+
importMnemonic = async (mnemonic, password, label) => {
|
|
735
|
+
const trimmedMnemonic = mnemonic.trim();
|
|
736
|
+
if (!validateMnemonic(trimmedMnemonic, wordlist)) {
|
|
737
|
+
throw new Error("Invalid mnemonic phrase");
|
|
738
|
+
}
|
|
739
|
+
const result = await this.createIdentity({
|
|
740
|
+
mnemonic: trimmedMnemonic,
|
|
741
|
+
password,
|
|
742
|
+
label: label || "Imported Wallet"
|
|
743
|
+
});
|
|
744
|
+
return { identity: result.identity };
|
|
745
|
+
};
|
|
746
|
+
/**
|
|
747
|
+
* Imports an identity from an encrypted keystore JSON.
|
|
748
|
+
* Decrypts the keystore and creates a new identity in IndexedDB.
|
|
749
|
+
* @param keystoreJson - Encrypted keystore JSON string
|
|
750
|
+
* @param password - Password to decrypt the keystore
|
|
751
|
+
* @param label - Optional label for the wallet (default: "Imported Wallet")
|
|
752
|
+
* @returns Promise resolving to the created identity
|
|
753
|
+
* @throws {Error} If keystore is invalid or password incorrect
|
|
754
|
+
*/
|
|
755
|
+
importKeystore = async (keystoreJson, password, label) => {
|
|
756
|
+
const mnemonic = await decryptMnemonic(keystoreJson, password);
|
|
757
|
+
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
758
|
+
throw new Error("Decrypted mnemonic is invalid");
|
|
759
|
+
}
|
|
760
|
+
const result = await this.createIdentity({
|
|
761
|
+
mnemonic,
|
|
762
|
+
password,
|
|
763
|
+
label: label || "Imported Wallet"
|
|
764
|
+
});
|
|
765
|
+
return { identity: result.identity };
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Imports a complete StoredIdentity object.
|
|
769
|
+
* No password required - keystore is already encrypted.
|
|
770
|
+
* Perfect for migrating identities between devices/browsers with all metadata intact.
|
|
771
|
+
* Note: This will overwrite any existing identity with the same ID.
|
|
772
|
+
* @param storedIdentity - Complete StoredIdentity object to import
|
|
773
|
+
* @returns Promise resolving to the imported identity
|
|
774
|
+
* @throws {Error} If stored identity data is invalid
|
|
775
|
+
*/
|
|
776
|
+
importIdentity = async (storedIdentity) => {
|
|
777
|
+
if (!storedIdentity.id || !storedIdentity.keystore || !storedIdentity.fingerprint) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"Invalid StoredIdentity object - missing required fields"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
await saveIdentityToDB(storedIdentity);
|
|
783
|
+
const identity = {
|
|
784
|
+
id: storedIdentity.id,
|
|
785
|
+
publicKey: storedIdentity.publicKey,
|
|
786
|
+
fingerprint: storedIdentity.fingerprint,
|
|
787
|
+
label: storedIdentity.label,
|
|
788
|
+
metadata: storedIdentity.metadata,
|
|
789
|
+
createdAt: new Date(storedIdentity.createdAt),
|
|
790
|
+
lastAccess: storedIdentity.lastAccess ? new Date(storedIdentity.lastAccess) : void 0
|
|
791
|
+
};
|
|
792
|
+
this.notifyListeners("IDENTITY_IMPORTED", null);
|
|
793
|
+
return { identity };
|
|
794
|
+
};
|
|
795
|
+
// Address Management
|
|
796
|
+
/**
|
|
797
|
+
* Gets multiple addresses for a blockchain starting from a specific index.
|
|
798
|
+
* @param chainId - Blockchain identifier
|
|
799
|
+
* @param start - Starting index (default: 0)
|
|
800
|
+
* @param count - Number of addresses to generate (default: 20)
|
|
801
|
+
* @returns Promise resolving to array of addresses with paths
|
|
802
|
+
* @throws {Error} If wallet is locked or adapter not registered
|
|
803
|
+
*/
|
|
804
|
+
getAddresses = async (chainId, start = 0, count = 20) => {
|
|
805
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
806
|
+
throw new Error("Identity must be unlocked to get addresses");
|
|
807
|
+
}
|
|
808
|
+
const adapter = this.getAdapter(chainId);
|
|
809
|
+
return adapter.getAddresses(this.decryptedMnemonic, start, count);
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* Finds all used addresses by checking balances with BIP44 gap limit of 20.
|
|
813
|
+
* Useful for account discovery when restoring wallets.
|
|
814
|
+
* @param chainId - Blockchain identifier
|
|
815
|
+
* @param checkBalance - Function that returns true if address has balance
|
|
816
|
+
* @returns Promise resolving to array of used addresses
|
|
817
|
+
* @throws {Error} If wallet is locked or adapter not registered
|
|
818
|
+
*/
|
|
819
|
+
findUsedAddresses = async (chainId, checkBalance) => {
|
|
820
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
821
|
+
throw new Error("Identity must be unlocked to find addresses");
|
|
822
|
+
}
|
|
823
|
+
const adapter = this.getAdapter(chainId);
|
|
824
|
+
const usedAddresses = [];
|
|
825
|
+
let gapCount = 0;
|
|
826
|
+
const gapLimit = 20;
|
|
827
|
+
let index = 0;
|
|
828
|
+
while (gapCount < gapLimit) {
|
|
829
|
+
const addresses = await adapter.getAddresses(
|
|
830
|
+
this.decryptedMnemonic,
|
|
831
|
+
index,
|
|
832
|
+
1
|
|
833
|
+
);
|
|
834
|
+
const addressData = addresses[0];
|
|
835
|
+
const hasBalance = await checkBalance(addressData.address);
|
|
836
|
+
if (hasBalance) {
|
|
837
|
+
usedAddresses.push(addressData);
|
|
838
|
+
gapCount = 0;
|
|
839
|
+
} else {
|
|
840
|
+
gapCount++;
|
|
841
|
+
}
|
|
842
|
+
index++;
|
|
843
|
+
}
|
|
844
|
+
return usedAddresses;
|
|
845
|
+
};
|
|
846
|
+
// State Management
|
|
847
|
+
/**
|
|
848
|
+
* Subscribes to authentication state changes.
|
|
849
|
+
* @param callback - Function called on each auth event with event type and keys
|
|
850
|
+
* @returns Unsubscribe function
|
|
851
|
+
*/
|
|
852
|
+
onAuthStateChange = (callback) => {
|
|
853
|
+
this.listeners.add(callback);
|
|
854
|
+
return () => {
|
|
855
|
+
this.listeners.delete(callback);
|
|
856
|
+
};
|
|
857
|
+
};
|
|
858
|
+
// Getters for current state
|
|
859
|
+
/** Gets the current active identity, or null if none selected. */
|
|
860
|
+
get currentIdentity() {
|
|
861
|
+
return this.state.identity;
|
|
862
|
+
}
|
|
863
|
+
/** Gets the current derived keys, or null if wallet is locked. */
|
|
864
|
+
get currentKeys() {
|
|
865
|
+
return this.state.keys;
|
|
866
|
+
}
|
|
867
|
+
/** Gets the current blockchain, or null if none selected. */
|
|
868
|
+
get currentChain() {
|
|
869
|
+
return this.state.currentChain;
|
|
870
|
+
}
|
|
871
|
+
/** Gets the current blockchain address, or null if locked. */
|
|
872
|
+
get currentAddress() {
|
|
873
|
+
return this.state.keys?.address ?? null;
|
|
874
|
+
}
|
|
875
|
+
/** Gets the current public key as hex string, or null if locked. */
|
|
876
|
+
get currentPublicKey() {
|
|
877
|
+
return this.state.keys?.publicKeyHex ?? null;
|
|
878
|
+
}
|
|
879
|
+
/** Gets the key expiration date, or null if no expiration set. */
|
|
880
|
+
get currentExpiresAt() {
|
|
881
|
+
return this.state.keys?.expiresAt ?? null;
|
|
882
|
+
}
|
|
883
|
+
/** Gets the remaining seconds until key expiration, or null if no expiration set. */
|
|
884
|
+
get currentExpiresIn() {
|
|
885
|
+
if (!this.state.keys?.expiresAt) return null;
|
|
886
|
+
const remaining = Math.floor(
|
|
887
|
+
(this.state.keys.expiresAt.getTime() - Date.now()) / 1e3
|
|
888
|
+
);
|
|
889
|
+
return Math.max(0, remaining);
|
|
890
|
+
}
|
|
891
|
+
/** Returns true if wallet is locked (keys cleared from memory). */
|
|
892
|
+
get isLocked() {
|
|
893
|
+
return this.state.isLocked;
|
|
894
|
+
}
|
|
895
|
+
/** Returns true if wallet is unlocked with active keys. */
|
|
896
|
+
get isUnlocked() {
|
|
897
|
+
return !this.state.isLocked && this.state.keys !== null;
|
|
898
|
+
}
|
|
899
|
+
/** Returns true if an identity has been created or selected. */
|
|
900
|
+
get hasIdentity() {
|
|
901
|
+
return this.state.identity !== null;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Gets the chain-independent master public key (33 bytes, compressed secp256k1).
|
|
905
|
+
* Safe to expose publicly - does NOT include chaincode.
|
|
906
|
+
* Perfect for document ownership and cross-chain identity verification.
|
|
907
|
+
* @returns Hex-encoded master public key, or null if locked
|
|
908
|
+
*/
|
|
909
|
+
get masterPublicKey() {
|
|
910
|
+
if (this.state.isLocked || !this.decryptedMnemonic) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
const seed = mnemonicToSeedSync(this.decryptedMnemonic);
|
|
914
|
+
const masterNode = HDKey.fromMasterSeed(seed);
|
|
915
|
+
return bufferToHex(masterNode.publicKey);
|
|
916
|
+
}
|
|
917
|
+
// Private Methods
|
|
918
|
+
isKeysExpired() {
|
|
919
|
+
if (!this.state.keys?.expiresAt) return false;
|
|
920
|
+
return Date.now() >= this.state.keys.expiresAt.getTime();
|
|
921
|
+
}
|
|
922
|
+
checkKeysExpiration() {
|
|
923
|
+
if (this.isKeysExpired()) {
|
|
924
|
+
console.warn(
|
|
925
|
+
"CryptForge: Keys have expired. Consider unlocking again for security."
|
|
926
|
+
);
|
|
927
|
+
this.notifyListeners("KEYS_EXPIRED", null);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async deriveKeysWithAdapter(mnemonic, chainId, adapter, identity, duration) {
|
|
931
|
+
const keyData = await adapter.deriveKeys(mnemonic);
|
|
932
|
+
const durationMs = duration || 36e5;
|
|
933
|
+
const expiresAt = new Date(Date.now() + durationMs);
|
|
934
|
+
const expiresIn = Math.floor(durationMs / 1e3);
|
|
935
|
+
return {
|
|
936
|
+
privateKey: keyData.privateKey,
|
|
937
|
+
privateKeyHex: keyData.privateKeyHex,
|
|
938
|
+
publicKey: keyData.publicKey,
|
|
939
|
+
publicKeyHex: keyData.publicKeyHex,
|
|
940
|
+
address: keyData.address,
|
|
941
|
+
derivationPath: keyData.path,
|
|
942
|
+
chain: {
|
|
943
|
+
id: chainId,
|
|
944
|
+
name: adapter.chainData.name,
|
|
945
|
+
symbol: adapter.chainData.symbol
|
|
946
|
+
},
|
|
947
|
+
expiresAt,
|
|
948
|
+
expiresIn,
|
|
949
|
+
identity
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
notifyListeners(event, keys) {
|
|
953
|
+
this.listeners.forEach((callback) => callback(event, keys));
|
|
954
|
+
}
|
|
955
|
+
setAutoLockTimer(duration) {
|
|
956
|
+
if (this.lockTimer) {
|
|
957
|
+
window.clearTimeout(this.lockTimer);
|
|
958
|
+
}
|
|
959
|
+
this.lockTimer = window.setTimeout(() => this.lock(), duration);
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
var createAuthClient = () => {
|
|
963
|
+
return new AuthClient();
|
|
964
|
+
};
|
|
965
|
+
var getMasterNode = (mnemonic) => {
|
|
966
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
967
|
+
return HDKey.fromMasterSeed(seed);
|
|
968
|
+
};
|
|
969
|
+
var getMasterFingerprint = (mnemonic) => {
|
|
970
|
+
const root = getMasterNode(mnemonic);
|
|
971
|
+
return root.fingerprint.toString(16).padStart(8, "0").toUpperCase();
|
|
972
|
+
};
|
|
973
|
+
var getMasterPublicKey = (mnemonic) => {
|
|
974
|
+
const root = getMasterNode(mnemonic);
|
|
975
|
+
return bufferToHex(root.publicKey);
|
|
976
|
+
};
|
|
977
|
+
function bufferToHex(buf) {
|
|
978
|
+
return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
979
|
+
}
|
|
980
|
+
function concatUint8(a, b) {
|
|
981
|
+
const c = new Uint8Array(a.length + b.length);
|
|
982
|
+
c.set(a, 0);
|
|
983
|
+
c.set(b, a.length);
|
|
984
|
+
return c;
|
|
985
|
+
}
|
|
986
|
+
function hexToUint8(hex) {
|
|
987
|
+
if (hex.length % 2 !== 0) throw new Error("Invalid hex string");
|
|
988
|
+
const arr = new Uint8Array(hex.length / 2);
|
|
989
|
+
for (let i = 0; i < arr.length; i++) {
|
|
990
|
+
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
991
|
+
}
|
|
992
|
+
return arr;
|
|
993
|
+
}
|
|
994
|
+
function arraysEqual(a, b) {
|
|
995
|
+
if (a.length !== b.length) return false;
|
|
996
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
997
|
+
return true;
|
|
998
|
+
}
|
|
999
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
1000
|
+
var KEY_LENGTH = 32;
|
|
1001
|
+
function randomBytes(length) {
|
|
1002
|
+
const arr = new Uint8Array(length);
|
|
1003
|
+
crypto.getRandomValues(arr);
|
|
1004
|
+
return arr;
|
|
1005
|
+
}
|
|
1006
|
+
var encryptMnemonic = async (mnemonic, password) => {
|
|
1007
|
+
const salt = randomBytes(32);
|
|
1008
|
+
const iv = randomBytes(16);
|
|
1009
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
1010
|
+
"raw",
|
|
1011
|
+
new TextEncoder().encode(password),
|
|
1012
|
+
"PBKDF2",
|
|
1013
|
+
false,
|
|
1014
|
+
["deriveKey"]
|
|
1015
|
+
);
|
|
1016
|
+
const key = await crypto.subtle.deriveKey(
|
|
1017
|
+
{
|
|
1018
|
+
name: "PBKDF2",
|
|
1019
|
+
salt,
|
|
1020
|
+
iterations: PBKDF2_ITERATIONS,
|
|
1021
|
+
hash: "SHA-256"
|
|
1022
|
+
},
|
|
1023
|
+
keyMaterial,
|
|
1024
|
+
{ name: "AES-CBC", length: 256 },
|
|
1025
|
+
true,
|
|
1026
|
+
["encrypt", "decrypt"]
|
|
1027
|
+
);
|
|
1028
|
+
const encryptedBuffer = await crypto.subtle.encrypt(
|
|
1029
|
+
{ name: "AES-CBC", iv },
|
|
1030
|
+
key,
|
|
1031
|
+
new TextEncoder().encode(mnemonic)
|
|
1032
|
+
);
|
|
1033
|
+
const ciphertext = new Uint8Array(encryptedBuffer);
|
|
1034
|
+
const macKey = await crypto.subtle.importKey(
|
|
1035
|
+
"raw",
|
|
1036
|
+
await crypto.subtle.exportKey("raw", key),
|
|
1037
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1038
|
+
false,
|
|
1039
|
+
["sign"]
|
|
1040
|
+
);
|
|
1041
|
+
const macBuffer = await crypto.subtle.sign(
|
|
1042
|
+
"HMAC",
|
|
1043
|
+
macKey,
|
|
1044
|
+
concatUint8(ciphertext, iv)
|
|
1045
|
+
);
|
|
1046
|
+
const keystore = {
|
|
1047
|
+
version: 1,
|
|
1048
|
+
id: bufferToHex(randomBytes(16)),
|
|
1049
|
+
crypto: {
|
|
1050
|
+
cipher: "aes-256-cbc",
|
|
1051
|
+
ciphertext: bufferToHex(ciphertext),
|
|
1052
|
+
cipherparams: { iv: bufferToHex(iv) },
|
|
1053
|
+
kdf: "pbkdf2",
|
|
1054
|
+
kdfparams: {
|
|
1055
|
+
salt: bufferToHex(salt),
|
|
1056
|
+
iterations: PBKDF2_ITERATIONS,
|
|
1057
|
+
keylen: KEY_LENGTH
|
|
1058
|
+
},
|
|
1059
|
+
mac: bufferToHex(new Uint8Array(macBuffer))
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
return JSON.stringify(keystore);
|
|
1063
|
+
};
|
|
1064
|
+
async function decryptMnemonic(keystoreJson, password) {
|
|
1065
|
+
const keystore = JSON.parse(keystoreJson);
|
|
1066
|
+
const salt = hexToUint8(keystore.crypto.kdfparams.salt);
|
|
1067
|
+
const iv = hexToUint8(keystore.crypto.cipherparams.iv);
|
|
1068
|
+
const ciphertext = hexToUint8(keystore.crypto.ciphertext);
|
|
1069
|
+
const storedMac = hexToUint8(keystore.crypto.mac);
|
|
1070
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
1071
|
+
"raw",
|
|
1072
|
+
new TextEncoder().encode(password),
|
|
1073
|
+
"PBKDF2",
|
|
1074
|
+
false,
|
|
1075
|
+
["deriveKey"]
|
|
1076
|
+
);
|
|
1077
|
+
const key = await crypto.subtle.deriveKey(
|
|
1078
|
+
{
|
|
1079
|
+
name: "PBKDF2",
|
|
1080
|
+
salt,
|
|
1081
|
+
iterations: keystore.crypto.kdfparams.iterations,
|
|
1082
|
+
hash: "SHA-256"
|
|
1083
|
+
},
|
|
1084
|
+
keyMaterial,
|
|
1085
|
+
{ name: "AES-CBC", length: 256 },
|
|
1086
|
+
true,
|
|
1087
|
+
["encrypt", "decrypt"]
|
|
1088
|
+
);
|
|
1089
|
+
const macKey = await crypto.subtle.importKey(
|
|
1090
|
+
"raw",
|
|
1091
|
+
await crypto.subtle.exportKey("raw", key),
|
|
1092
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1093
|
+
false,
|
|
1094
|
+
["sign"]
|
|
1095
|
+
);
|
|
1096
|
+
const computedMacBuffer = await crypto.subtle.sign(
|
|
1097
|
+
"HMAC",
|
|
1098
|
+
macKey,
|
|
1099
|
+
concatUint8(ciphertext, iv)
|
|
1100
|
+
);
|
|
1101
|
+
const computedMac = new Uint8Array(computedMacBuffer);
|
|
1102
|
+
if (!arraysEqual(computedMac, storedMac)) {
|
|
1103
|
+
throw new Error("Invalid password - MAC verification failed");
|
|
1104
|
+
}
|
|
1105
|
+
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
1106
|
+
{ name: "AES-CBC", iv },
|
|
1107
|
+
key,
|
|
1108
|
+
ciphertext
|
|
1109
|
+
);
|
|
1110
|
+
const mnemonic = new TextDecoder().decode(decryptedBuffer);
|
|
1111
|
+
if (!mnemonic) {
|
|
1112
|
+
throw new Error("Invalid password - decryption failed");
|
|
1113
|
+
}
|
|
1114
|
+
return mnemonic;
|
|
1115
|
+
}
|
|
1116
|
+
var DB_NAME = "CryptoAuthDB";
|
|
1117
|
+
var STORE_NAME = "identities";
|
|
1118
|
+
var openDB = () => {
|
|
1119
|
+
return new Promise((resolve, reject) => {
|
|
1120
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
1121
|
+
request.onerror = () => reject(request.error);
|
|
1122
|
+
request.onsuccess = () => resolve(request.result);
|
|
1123
|
+
request.onupgradeneeded = (event) => {
|
|
1124
|
+
const db = event.target.result;
|
|
1125
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1126
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
var saveIdentityToDB = async (identity) => {
|
|
1132
|
+
const db = await openDB();
|
|
1133
|
+
return new Promise((resolve, reject) => {
|
|
1134
|
+
const transaction = db.transaction([STORE_NAME], "readwrite");
|
|
1135
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1136
|
+
const request = store.put(identity);
|
|
1137
|
+
request.onsuccess = () => resolve();
|
|
1138
|
+
request.onerror = () => reject(request.error);
|
|
1139
|
+
});
|
|
1140
|
+
};
|
|
1141
|
+
var getAllIdentitiesFromDB = async () => {
|
|
1142
|
+
const db = await openDB();
|
|
1143
|
+
return new Promise((resolve, reject) => {
|
|
1144
|
+
const transaction = db.transaction([STORE_NAME], "readonly");
|
|
1145
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1146
|
+
const request = store.getAll();
|
|
1147
|
+
request.onsuccess = () => resolve(request.result);
|
|
1148
|
+
request.onerror = () => reject(request.error);
|
|
1149
|
+
});
|
|
1150
|
+
};
|
|
1151
|
+
var getIdentityFromDB = async (id) => {
|
|
1152
|
+
const db = await openDB();
|
|
1153
|
+
return new Promise((resolve, reject) => {
|
|
1154
|
+
const transaction = db.transaction([STORE_NAME], "readonly");
|
|
1155
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1156
|
+
const request = store.get(id);
|
|
1157
|
+
request.onsuccess = () => resolve(request.result);
|
|
1158
|
+
request.onerror = () => reject(request.error);
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
var deleteIdentityFromDB = async (id) => {
|
|
1162
|
+
const db = await openDB();
|
|
1163
|
+
return new Promise((resolve, reject) => {
|
|
1164
|
+
const transaction = db.transaction([STORE_NAME], "readwrite");
|
|
1165
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1166
|
+
const request = store.delete(id);
|
|
1167
|
+
request.onsuccess = () => resolve();
|
|
1168
|
+
request.onerror = () => reject(request.error);
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
export {
|
|
1172
|
+
AuthClient,
|
|
1173
|
+
createAuthClient
|
|
1174
|
+
};
|