w3pk 0.10.2 → 0.10.4
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 +1 -8
- package/dist/chainlist/index.d.mts +96 -0
- package/dist/chainlist/index.d.ts +96 -0
- package/dist/index.d.mts +237 -113
- package/dist/index.d.ts +237 -113
- package/dist/index.js +31 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31 -31
- package/dist/index.mjs.map +1 -1
- package/dist/zk/index.js +3 -3
- package/dist/zk/index.js.map +1 -1
- package/dist/zk/index.mjs +4 -4
- package/dist/zk/index.mjs.map +1 -1
- package/dist/zk/utils.js +3 -3
- package/dist/zk/utils.js.map +1 -1
- package/dist/zk/utils.mjs +3 -3
- package/dist/zk/utils.mjs.map +1 -1
- package/docs/API_REFERENCE.md +191 -38
- package/docs/ARCHITECTURE.md +14 -13
- package/docs/BROWSER_COMPATIBILITY.md +24 -0
- package/docs/INTEGRATION_GUIDELINES.md +45 -20
- package/docs/PORTABILITY.md +66 -0
- package/docs/POST_QUANTUM.md +236 -1468
- package/docs/QUICK_START.md +4 -4
- package/docs/RECOVERY.md +93 -49
- package/docs/SECURITY.md +65 -58
- package/docs/notes/IFRAME_IMPLEMENTATION_PLAN.md +197 -0
- package/docs/notes/JULY_13_ROADMAP.md +29 -0
- package/docs/notes/NOTES.md +333 -0
- package/docs/notes/SIWE_IMPLEMENTATION.md +129 -0
- package/docs/notes/SYNCING_ACROSS_DEVICES.md +127 -0
- package/docs/notes/ethereum-post-quantum-roadmap.md +213 -0
- package/docs/notes/wulong-building-finding.md +109 -0
- package/package.json +43 -25
package/docs/API_REFERENCE.md
CHANGED
|
@@ -10,6 +10,7 @@ Complete reference for all methods, types, and utilities in the w3pk SDK.
|
|
|
10
10
|
- [Origin-Specific Address Derivation](#origin-specific-address-derivation)
|
|
11
11
|
- [`sendTransaction()`](#sendtransactiontx-options-promisetransactionresult)
|
|
12
12
|
- [`getEIP1193Provider()`](#geteip1193provideroptions-eip1193providerinterface)
|
|
13
|
+
- [`derivePrfSecret()`](#deriveprfsecrettag-string-promiseprfsecret)
|
|
13
14
|
- [Stealth Addresses (ERC-5564)](#stealth-addresses-erc-5564)
|
|
14
15
|
- [Zero-Knowledge Proofs](#zero-knowledge-proofs)
|
|
15
16
|
- [Backup & Recovery](#backup--recovery)
|
|
@@ -43,7 +44,7 @@ interface Web3PasskeyConfig {
|
|
|
43
44
|
debug?: boolean; // Enable debug mode (default: false)
|
|
44
45
|
onError?: (error: Web3PasskeyError) => void; // Global error handler
|
|
45
46
|
onAuthStateChanged?: (isAuthenticated: boolean, user?: UserInfo) => void; // Auth state callback
|
|
46
|
-
storage?: Storage; // Custom storage backend (default:
|
|
47
|
+
storage?: Storage; // Custom storage backend (default: localStorage)
|
|
47
48
|
sessionDuration?: number; // Session duration in hours (default: 1)
|
|
48
49
|
persistentSession?: Partial<PersistentSessionConfig>; // Enable "Remember Me" functionality
|
|
49
50
|
stealthAddresses?: StealthAddressConfig; // Enable stealth addresses
|
|
@@ -57,7 +58,7 @@ interface PersistentSessionConfig {
|
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
**PRF requirement:** persistent sessions are encrypted under a key derived from the WebAuthn PRF extension, which the authenticator only releases during a user-verified assertion. On authenticators without PRF support, no persistent session is stored — the device falls back to in-memory sessions only (there is no weaker at-rest fallback). The session is re-keyed at every real (prompted) login; the configured `duration` is therefore also the renewal interval. With `requireReauth: true` the decryption key is never written to disk; with `requireReauth: false` it is stored
|
|
61
|
+
**PRF requirement:** persistent sessions are encrypted under a key derived from the WebAuthn PRF extension, which the authenticator only releases during a user-verified assertion. On authenticators without PRF support, no persistent session is stored — the device falls back to in-memory sessions only (there is no weaker at-rest fallback). The session is re-keyed at every real (prompted) login; the configured `duration` is therefore also the renewal interval. With `requireReauth: true` the decryption key is never written to disk; with `requireReauth: false` it is stored next to the blob in localStorage to allow silent restore. See [SECURITY.md](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt) for the full model.
|
|
61
62
|
|
|
62
63
|
**Example:**
|
|
63
64
|
|
|
@@ -114,6 +115,8 @@ Register a new user with WebAuthn passkey authentication.
|
|
|
114
115
|
interface RegisterResult {
|
|
115
116
|
address: string; // Ethereum address (account #0 derived from BIP44)
|
|
116
117
|
username: string; // Registered username
|
|
118
|
+
hasBackup: boolean; // false for a new wallet: prompt for a backup now
|
|
119
|
+
evictionRisk: boolean; // true where the browser may wipe the wallet (WebKit tab, storage not persisted)
|
|
117
120
|
}
|
|
118
121
|
```
|
|
119
122
|
|
|
@@ -122,17 +125,21 @@ interface RegisterResult {
|
|
|
122
125
|
2. Creates WebAuthn credential (triggers biometric prompt)
|
|
123
126
|
3. Derives Ethereum wallet from mnemonic (path: m/44'/60'/0'/0/0)
|
|
124
127
|
4. Encrypts mnemonic using WebAuthn credential (AES-GCM-256)
|
|
125
|
-
5. Stores encrypted wallet in
|
|
128
|
+
5. Stores encrypted wallet in localStorage, next to the credential metadata
|
|
126
129
|
6. Starts 1-hour session with decrypted mnemonic in memory
|
|
127
|
-
7. Returns Ethereum address and
|
|
130
|
+
7. Returns Ethereum address, username, whether a backup exists and whether the browser may wipe the wallet
|
|
128
131
|
|
|
129
132
|
**Example:**
|
|
130
133
|
|
|
131
134
|
```typescript
|
|
132
|
-
const { address, username } = await w3pk.register({
|
|
135
|
+
const { address, username, hasBackup, evictionRisk } = await w3pk.register({
|
|
133
136
|
username: 'alice'
|
|
134
137
|
})
|
|
135
138
|
console.log('Registered:', username, 'with address:', address)
|
|
139
|
+
|
|
140
|
+
if (!hasBackup) {
|
|
141
|
+
showBackupPrompt({ blocking: evictionRisk }) // see getBackupStatus()
|
|
142
|
+
}
|
|
136
143
|
```
|
|
137
144
|
|
|
138
145
|
---
|
|
@@ -153,14 +160,20 @@ interface UserInfo {
|
|
|
153
160
|
```
|
|
154
161
|
|
|
155
162
|
**What happens:**
|
|
156
|
-
1. Retrieves credential ID from
|
|
163
|
+
1. Retrieves credential ID from localStorage
|
|
157
164
|
2. Triggers WebAuthn authentication (biometric prompt)
|
|
158
|
-
3. Retrieves encrypted wallet from
|
|
165
|
+
3. Retrieves encrypted wallet from localStorage
|
|
159
166
|
4. Decrypts mnemonic using WebAuthn signature
|
|
160
167
|
5. Starts new session (1 hour by default)
|
|
161
168
|
6. Returns user info
|
|
162
169
|
7. Subsequent operations use cached session - no repeated biometric prompts
|
|
163
170
|
|
|
171
|
+
**Throws:**
|
|
172
|
+
- `SyncRequiredError` if the passkey authenticated but this device has no credential metadata for it (carries `credentialId`); start a sync with `syncWalletWithPasskey()` and a password backup
|
|
173
|
+
- `WalletMissingError` if the credential metadata is here but the wallet record is gone (carries `credentialId` and `ethereumAddress`); restore with `syncWalletWithPasskey()` or `importFromSync()`
|
|
174
|
+
|
|
175
|
+
In both cases the user is not logged in and `onAuthStateChanged(true, …)` is not called.
|
|
176
|
+
|
|
164
177
|
**Example:**
|
|
165
178
|
|
|
166
179
|
```typescript
|
|
@@ -218,6 +231,23 @@ if (currentUser) {
|
|
|
218
231
|
|
|
219
232
|
---
|
|
220
233
|
|
|
234
|
+
### `storagePersisted: boolean | null` (property)
|
|
235
|
+
|
|
236
|
+
Whether the browser granted persistent storage. w3pk calls `navigator.storage.persist()` after each register, login and restore without waiting for it. `true` means the origin's storage is exempt from storage-pressure eviction. `null` until the browser answers (Firefox asks the user), or where the API is missing.
|
|
237
|
+
|
|
238
|
+
Persisted storage still doesn't survive Safari's 7-day cap, clearing site data or private browsing: see [SECURITY.md → Storage durability](./SECURITY.md#storage-durability-what-can-still-wipe-the-wallet).
|
|
239
|
+
|
|
240
|
+
**Example:**
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
await w3pk.login()
|
|
244
|
+
if (w3pk.storagePersisted === false) {
|
|
245
|
+
console.warn('Storage may be evicted: make sure the user has a backup')
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
221
251
|
### `hasExistingCredential(): Promise<boolean>`
|
|
222
252
|
|
|
223
253
|
Check if there are existing credentials (wallets) on this device.
|
|
@@ -772,6 +802,7 @@ interface SignatureResult {
|
|
|
772
802
|
- Uses EIP-191 prefix (for EOA accounts)
|
|
773
803
|
- See https://docs.login.xyz for message format
|
|
774
804
|
- Use `createSiweMessage()` helper for proper formatting
|
|
805
|
+
- Not supported in PRIMARY mode (throws): use STANDARD, STRICT or YOLO
|
|
775
806
|
|
|
776
807
|
**EIP-712 Signing:**
|
|
777
808
|
- Signs structured typed data according to EIP-712 standard
|
|
@@ -1336,6 +1367,61 @@ await accountAbstractionContract.verifyWebAuthnSignature({
|
|
|
1336
1367
|
|
|
1337
1368
|
---
|
|
1338
1369
|
|
|
1370
|
+
### `derivePrfSecret(tag: string): Promise<PrfSecret>`
|
|
1371
|
+
|
|
1372
|
+
Derive a 32-byte secret bound to the passkey, separate per origin and tag, for an identity in an anonymous protocol or a per-feature encryption key.
|
|
1373
|
+
|
|
1374
|
+
It uses the [WebAuthn PRF extension](https://w3c.github.io/webauthn/#prf-extension): the authenticator holds a per-credential secret and releases `PRF(input)` only on a live, user-verified assertion. The secret is never derived from or stored with the mnemonic.
|
|
1375
|
+
|
|
1376
|
+
**Parameters:**
|
|
1377
|
+
- `tag: string` - Non-empty label without `:`, such as `"zk-identity"`. Case-sensitive.
|
|
1378
|
+
|
|
1379
|
+
**Returns:** `PrfSecret`
|
|
1380
|
+
|
|
1381
|
+
```typescript
|
|
1382
|
+
interface PrfSecret {
|
|
1383
|
+
secret: Uint8Array; // 32 bytes
|
|
1384
|
+
credentialId: string; // passkey the secret is bound to
|
|
1385
|
+
}
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
**What happens:**
|
|
1389
|
+
1. Prompts for the passkey with `userVerification: "required"`; when logged in, only the current user's passkey is allowed, otherwise the user picks one
|
|
1390
|
+
2. Evaluates the PRF at `SHA-256("w3pk-prf-v1:" + origin + ":" + tag)`, with `origin` the normalized current origin (the browser further hashes it with the `"WebAuthn PRF"` context)
|
|
1391
|
+
3. Returns `HKDF-SHA256(PRF output, salt "w3pk-prf-secret-v1", info "w3pk-prf-v1:" + origin + ":" + tag)`, 32 bytes
|
|
1392
|
+
|
|
1393
|
+
The same passkey, origin and tag always give the same secret. A different tag, origin or passkey gives an unrelated one. Store `credentialId` next to whatever the secret protects: when it changes (a new passkey), so does the secret.
|
|
1394
|
+
|
|
1395
|
+
**Example:**
|
|
1396
|
+
|
|
1397
|
+
```typescript
|
|
1398
|
+
import { PrfUnsupportedError } from 'w3pk'
|
|
1399
|
+
|
|
1400
|
+
try {
|
|
1401
|
+
const { secret, credentialId } = await w3pk.derivePrfSecret('zk-identity')
|
|
1402
|
+
// Use the secret, keep it in memory only
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
if (error instanceof PrfUnsupportedError) {
|
|
1405
|
+
// This authenticator can't provide a passkey-bound secret
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
**Throws:**
|
|
1411
|
+
- `PrfUnsupportedError` (`code: 'PRF_UNSUPPORTED'`, with `credentialId`) if the authenticator returns no PRF output. There is no fallback.
|
|
1412
|
+
- `AuthenticationError` if the prompt is cancelled or the passkey isn't available
|
|
1413
|
+
- `CryptoError` if `tag` is empty or contains `:`
|
|
1414
|
+
|
|
1415
|
+
**Threat model:**
|
|
1416
|
+
- **Not in backups.** The secret depends only on the authenticator's PRF, so backup files, recovery phrases, social recovery shares and sync exports don't contain it, and their leak doesn't leak it.
|
|
1417
|
+
- **Not stored.** w3pk never writes the secret or the PRF output to localStorage or sessions, and zeroes its copy of the PRF output after the derivation. Each call prompts the user. If the app stores the secret, its storage is the weakest link.
|
|
1418
|
+
- **Bound to the passkey, not the wallet.** Restoring the wallet from a backup with a different passkey gives a different secret. The secret survives only as long as the passkey: a synced passkey (iCloud Keychain, Google Password Manager) carries it to other devices, a deleted or device-bound passkey loses it. Anything encrypted under it must be recoverable another way.
|
|
1419
|
+
- **Origin separation is w3pk's, not the browser's.** Passkeys are scoped to the RP ID (the hostname), so another origin on the same hostname (another port or scheme) can use the same passkey. The origin in the PRF input keeps `derivePrfSecret()` from giving one origin another's secret, but a script running on such an origin can evaluate the PRF itself with any input. Serve one app per hostname when that matters.
|
|
1420
|
+
- **Script on the origin.** Code running on the page (XSS, a compromised dependency) can call `derivePrfSecret()` or the PRF directly and get the secret after the user approves a prompt. User verification bounds it to a user action, not to the app's intent.
|
|
1421
|
+
- **Separate from the persistent session key.** That key uses a different PRF input, so no tag gives it.
|
|
1422
|
+
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1339
1425
|
### `sendTransaction(tx, options?): Promise<TransactionResult>`
|
|
1340
1426
|
|
|
1341
1427
|
Send an on-chain transaction using the wallet derived for the active security mode.
|
|
@@ -2243,7 +2329,13 @@ Get comprehensive backup status showing what protects the wallet.
|
|
|
2243
2329
|
**Returns:**
|
|
2244
2330
|
|
|
2245
2331
|
```typescript
|
|
2332
|
+
type BackupMethod = 'file' | 'qr' | 'phrase';
|
|
2333
|
+
|
|
2246
2334
|
interface BackupStatus {
|
|
2335
|
+
hasBackup: boolean; // a backup file, QR code or confirmed phrase exists
|
|
2336
|
+
evictionRisk: boolean; // the browser may wipe the wallet: WebKit outside a home-screen app, storage not persisted
|
|
2337
|
+
lastBackupAt?: string; // ISO 8601 time of the latest backup
|
|
2338
|
+
methods: BackupMethod[];
|
|
2247
2339
|
passkeySync: PasskeySyncStatus;
|
|
2248
2340
|
recoveryPhrase: RecoveryPhraseStatus;
|
|
2249
2341
|
socialRecovery?: SocialRecoveryStatus;
|
|
@@ -2251,18 +2343,42 @@ interface BackupStatus {
|
|
|
2251
2343
|
}
|
|
2252
2344
|
```
|
|
2253
2345
|
|
|
2346
|
+
Browser storage can be wiped (eviction, Safari's 7-day cap, clearing site data), so a wallet without a backup can be lost for good. While `hasBackup` is `false`, the security score is capped at 20 and its level is `vulnerable`.
|
|
2347
|
+
|
|
2348
|
+
`evictionRisk` is computed on each call from `navigator.storage.persisted()` and the display mode: `true` on WebKit (every iOS browser, Safari and other WebKit browsers on desktop) in a tab whose storage is not persisted, `false` in a home-screen app, once storage is persisted, and on Chromium and Firefox.
|
|
2349
|
+
|
|
2254
2350
|
**Example:**
|
|
2255
2351
|
|
|
2256
2352
|
```typescript
|
|
2257
2353
|
const status = await w3pk.getBackupStatus()
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2354
|
+
if (!status.hasBackup && status.evictionRisk) {
|
|
2355
|
+
showBackupGate() // blocking; on iOS, suggest "Add to Home Screen"
|
|
2356
|
+
} else if (!status.hasBackup) {
|
|
2357
|
+
showBackupWarning()
|
|
2358
|
+
}
|
|
2359
|
+
console.log('Security Score:', status.securityScore.total) // 0-100
|
|
2360
|
+
console.log('Level:', status.securityScore.level)
|
|
2361
|
+
console.log('Backup methods:', status.methods)
|
|
2261
2362
|
console.log('Social recovery:', status.socialRecovery?.enabled)
|
|
2262
2363
|
```
|
|
2263
2364
|
|
|
2264
2365
|
---
|
|
2265
2366
|
|
|
2367
|
+
### `confirmRecoveryPhraseBackup(): Promise<void>`
|
|
2368
|
+
|
|
2369
|
+
Record that the user wrote down their recovery phrase. Call it once the user confirms, so `getBackupStatus()` reports `hasBackup: true` with the `phrase` method.
|
|
2370
|
+
|
|
2371
|
+
**Example:**
|
|
2372
|
+
|
|
2373
|
+
```typescript
|
|
2374
|
+
await showRecoveryPhrase()
|
|
2375
|
+
if (await userConfirmsPhraseWrittenDown()) {
|
|
2376
|
+
await w3pk.confirmRecoveryPhraseBackup()
|
|
2377
|
+
}
|
|
2378
|
+
```
|
|
2379
|
+
|
|
2380
|
+
---
|
|
2381
|
+
|
|
2266
2382
|
### `createBackupFile(encryptionType?: 'password' | 'passkey' | 'hybrid', password?: string): Promise<{ blob: Blob; filename: string }>`
|
|
2267
2383
|
|
|
2268
2384
|
Create simplified backup file for wallet recovery.
|
|
@@ -2621,14 +2737,17 @@ console.log('Estimated devices:', capabilities.estimatedDevices)
|
|
|
2621
2737
|
|
|
2622
2738
|
---
|
|
2623
2739
|
|
|
2624
|
-
### `exportForSync(): Promise<{ blob: Blob; filename: string; qrCode?: string }>`
|
|
2740
|
+
### `exportForSync(password?: string): Promise<{ blob: Blob; filename: string; qrCode?: string }>`
|
|
2625
2741
|
|
|
2626
2742
|
Export wallet for syncing to another device.
|
|
2627
2743
|
|
|
2628
|
-
|
|
2744
|
+
Runs a fresh assertion restricted to the current passkey and encrypts the mnemonic with a key derived from its [WebAuthn PRF](https://w3c.github.io/webauthn/#prf-extension) output (`encryptionMethod: 'prf'`). Any device where the passkey synced restores the file with one prompt and no password, including a fresh device.
|
|
2629
2745
|
|
|
2630
2746
|
**Use case:** User wants to sync wallet to another device that has the same passkey.
|
|
2631
2747
|
|
|
2748
|
+
**Parameters:**
|
|
2749
|
+
- `password?: string` - Fallback when the authenticator has no PRF support: the file is then a password backup that names its passkey, to restore with `syncWalletWithPasskey(syncData, password)`
|
|
2750
|
+
|
|
2632
2751
|
**Returns:**
|
|
2633
2752
|
|
|
2634
2753
|
```typescript
|
|
@@ -2639,7 +2758,11 @@ Uses passkey encryption so it works on devices where the passkey is synced.
|
|
|
2639
2758
|
}
|
|
2640
2759
|
```
|
|
2641
2760
|
|
|
2642
|
-
**
|
|
2761
|
+
**File format (`prf`):** `encryptedMnemonic`, `prfSalt` (32 random bytes per file, base64url), `credential: { id, publicKey }`, `username`, `addressChecksum`. Key: HKDF-SHA256 over the PRF output evaluated at `SHA-256("w3pk-prf-sync-v1" || prfSalt)`, then AES-256-GCM. The public key doesn't decrypt the file; a fresh device needs it for `login()` and checks it against the assertion's signature.
|
|
2762
|
+
|
|
2763
|
+
**Security:** The PRF assertion is the fresh user verification; with an active session, export takes one prompt.
|
|
2764
|
+
|
|
2765
|
+
**Browser support:** PRF works in Chrome 108+ and Safari 17+ / iOS 17+, not in Firefox. Passkeys sync within one ecosystem only (Apple or Google); across ecosystems, use a password backup.
|
|
2643
2766
|
|
|
2644
2767
|
**Example:**
|
|
2645
2768
|
|
|
@@ -2661,13 +2784,18 @@ if (qrCode) {
|
|
|
2661
2784
|
}
|
|
2662
2785
|
```
|
|
2663
2786
|
|
|
2787
|
+
**Throws:**
|
|
2788
|
+
- `WalletError` if not logged in
|
|
2789
|
+
- `PrfUnsupportedError` if the authenticator returns no PRF output and no `password` was given
|
|
2790
|
+
- `AuthenticationError` if the assertion fails or is cancelled
|
|
2791
|
+
|
|
2664
2792
|
---
|
|
2665
2793
|
|
|
2666
2794
|
### `importFromSync(syncData: string | Blob): Promise<{ ethereumAddress: string; success: boolean }>`
|
|
2667
2795
|
|
|
2668
|
-
Import
|
|
2796
|
+
Import a PRF sync file from `exportForSync()`. No prior `login()` and no password required: one assertion restricted to the file's passkey decrypts it.
|
|
2669
2797
|
|
|
2670
|
-
**Use case:** User has passkey on both devices
|
|
2798
|
+
**Use case:** User has the passkey on both devices (synced through iCloud Keychain or Google Password Manager) and the wallet on one. Works on a fresh device with no data for the passkey: the SDK checks the file's public key against the assertion's signature, saves the passkey's metadata, stores the wallet, starts a session and fires `onAuthStateChanged(true, user)`.
|
|
2671
2799
|
|
|
2672
2800
|
**Parameters:**
|
|
2673
2801
|
- `syncData: string | Blob` - Sync file data
|
|
@@ -2684,23 +2812,29 @@ Import wallet from another device (sync wallet to this device).
|
|
|
2684
2812
|
**Example:**
|
|
2685
2813
|
|
|
2686
2814
|
```typescript
|
|
2687
|
-
//
|
|
2688
|
-
await w3pk.login()
|
|
2689
|
-
|
|
2690
|
-
// Import from sync file
|
|
2691
|
-
const syncData = '...' // From file upload or QR scan
|
|
2815
|
+
const syncData = '...' // From exportForSync(), by file upload or QR scan
|
|
2692
2816
|
|
|
2693
2817
|
const result = await w3pk.importFromSync(syncData)
|
|
2694
2818
|
console.log('Wallet synced:', result.ethereumAddress)
|
|
2695
2819
|
```
|
|
2696
2820
|
|
|
2821
|
+
**Throws:**
|
|
2822
|
+
- `WalletError` if the file isn't a PRF sync file (use `syncWalletWithPasskey()` for other backups, including `exportForSync(password)`'s fallback)
|
|
2823
|
+
- `SyncBootstrapError` with `reason: 'CREDENTIAL_MISMATCH'` if logged in with another passkey than the file's
|
|
2824
|
+
- `SyncBootstrapError` with `reason: 'PUBLIC_KEY_MISMATCH'` if the file's public key doesn't match the passkey's signature
|
|
2825
|
+
- `AuthenticationError` if the passkey isn't on this device or the prompt is cancelled
|
|
2826
|
+
- `PrfUnsupportedError` if the authenticator returns no PRF output
|
|
2827
|
+
- `CryptoError` or `Error` if the file was altered (ciphertext, address or address checksum)
|
|
2828
|
+
|
|
2697
2829
|
---
|
|
2698
2830
|
|
|
2699
2831
|
### `syncWalletWithPasskey(backupData: string | Blob, password?: string): Promise<{ mnemonic: string; ethereumAddress: string }>`
|
|
2700
2832
|
|
|
2701
2833
|
Sync wallet to this device using an existing cloud-synced passkey and a backup file. No prior session required.
|
|
2702
2834
|
|
|
2703
|
-
**Use case:** New device that already has a passkey synced via iCloud/Google Password Manager. The user provides their backup file; the SDK prompts them to select the passkey, decrypts the backup, stores wallet data locally,
|
|
2835
|
+
**Use case:** New device that already has a passkey synced via iCloud/Google Password Manager. The user provides their backup file; the SDK prompts them to select the passkey, decrypts the backup, saves the passkey's metadata if this device has none, stores wallet data locally, starts a session and fires `onAuthStateChanged(true, user)`.
|
|
2836
|
+
|
|
2837
|
+
A PRF sync file from `exportForSync()` takes no password: it's restored as `importFromSync()` does, with one assertion restricted to its passkey. On a device with no data for the passkey, the other backups that work are password backups made by a logged-in user: they name their passkey (`credential: { id, publicKey }`), and the username comes from the passkey's user handle. In both cases the backup's public key is checked against the assertion's signature.
|
|
2704
2838
|
|
|
2705
2839
|
**Parameters:**
|
|
2706
2840
|
- `backupData: string | Blob` - Backup file content (JSON string or Blob)
|
|
@@ -2708,26 +2842,25 @@ Sync wallet to this device using an existing cloud-synced passkey and a backup f
|
|
|
2708
2842
|
|
|
2709
2843
|
**Returns:** `{ mnemonic: string; ethereumAddress: string }`
|
|
2710
2844
|
|
|
2711
|
-
**Supported encryption methods:** `password
|
|
2845
|
+
**Supported encryption methods:** `prf` and `password` everywhere; `passkey` and `hybrid` only where this device already has the passkey's metadata
|
|
2712
2846
|
|
|
2713
2847
|
**Example:**
|
|
2714
2848
|
|
|
2715
2849
|
```typescript
|
|
2716
|
-
|
|
2717
|
-
const result = await w3pk.syncWalletWithPasskey(backupData)
|
|
2850
|
+
const result = await w3pk.syncWalletWithPasskey(backupData, 'MyPassword123!')
|
|
2718
2851
|
console.log('Synced wallet address:', result.ethereumAddress)
|
|
2719
|
-
|
|
2720
|
-
// Password or hybrid backup
|
|
2721
|
-
const result2 = await w3pk.syncWalletWithPasskey(backupData, 'MyPassword123!')
|
|
2722
|
-
console.log('Synced wallet address:', result2.ethereumAddress)
|
|
2723
2852
|
```
|
|
2724
2853
|
|
|
2725
2854
|
**Throws:**
|
|
2726
2855
|
- `WalletError` if password is required but not provided
|
|
2727
|
-
- `WalletError` if passkey public key is not available on this device
|
|
2728
2856
|
- `WalletError` if backup uses an unknown encryption method
|
|
2857
|
+
- `SyncBootstrapError` on a device with no data for the passkey (carries `reason` and `credentialId`):
|
|
2858
|
+
- `'PASSKEY_BACKUP'`: passkey or hybrid backup
|
|
2859
|
+
- `'NO_CREDENTIAL'`: password backup that doesn't name its passkey (made before 0.10.4)
|
|
2860
|
+
- `'CREDENTIAL_MISMATCH'`: the backup names another passkey than the one selected
|
|
2861
|
+
- `'PUBLIC_KEY_MISMATCH'`: the backup's public key doesn't match the passkey's signature
|
|
2729
2862
|
|
|
2730
|
-
**Note:** Differs from `importFromSync()` in that it
|
|
2863
|
+
**Note:** Differs from `importFromSync()` in that it also accepts password, passkey and hybrid backups.
|
|
2731
2864
|
|
|
2732
2865
|
---
|
|
2733
2866
|
|
|
@@ -2736,7 +2869,7 @@ console.log('Synced wallet address:', result2.ethereumAddress)
|
|
|
2736
2869
|
w3pk provides two types of sessions:
|
|
2737
2870
|
|
|
2738
2871
|
1. **In-Memory Sessions** (default): Cached in RAM, cleared on page refresh
|
|
2739
|
-
2. **Persistent Sessions** (opt-in): Encrypted in
|
|
2872
|
+
2. **Persistent Sessions** (opt-in): Encrypted in localStorage, survives page refresh
|
|
2740
2873
|
|
|
2741
2874
|
**Security Modes & Persistence:**
|
|
2742
2875
|
- **STANDARD mode**: Persistent sessions allowed ✅
|
|
@@ -2844,14 +2977,14 @@ Enable persistent sessions to maintain user login across page refreshes.
|
|
|
2844
2977
|
|
|
2845
2978
|
**How it works:**
|
|
2846
2979
|
1. On login, mnemonic is encrypted with WebAuthn-derived key
|
|
2847
|
-
2. Encrypted session stored in
|
|
2980
|
+
2. Encrypted session stored in localStorage
|
|
2848
2981
|
3. On page refresh, session is automatically restored (if not expired)
|
|
2849
2982
|
4. `requireReauth: true` prompts for biometric on refresh (more secure)
|
|
2850
2983
|
5. `requireReauth: false` silently restores session (more convenient)
|
|
2851
2984
|
|
|
2852
2985
|
**Security (read the trade-off):**
|
|
2853
2986
|
- Sessions only persist for STANDARD and YOLO modes; STRICT mode is NEVER persisted
|
|
2854
|
-
- Time-limited expiration; origin-isolated via
|
|
2987
|
+
- Time-limited expiration; origin-isolated via localStorage
|
|
2855
2988
|
- ⚠️ The "WebAuthn-derived key" is derived from **public** credential metadata stored in the same browser profile, **not** from an authenticator-held secret. A persistent session is therefore **decryptable by anyone who can read this origin's storage** (malicious extension, XSS exfil, disk image), and it keeps the seed in that state for the whole duration it's enabled. This is a UX-for-security trade-off, not hardware-backed protection.
|
|
2856
2989
|
- Enabling "Remember Me" does not add a new trust dependency (you already trust your origin's code), it **widens the window** in which a compromise of your origin can reach the seed. Keep `duration` short for higher-value wallets; don't enable on shared/untrusted devices.
|
|
2857
2990
|
- Full threat model: **[SECURITY.md → At-Rest Encryption](./SECURITY.md#at-rest-encryption-what-it-protects-and-what-it-doesnt)**.
|
|
@@ -3389,9 +3522,10 @@ Encrypt data using ML-KEM-1024 (post-quantum KEM) + AES-256-GCM for one or more
|
|
|
3389
3522
|
**Returns:**
|
|
3390
3523
|
```typescript
|
|
3391
3524
|
{
|
|
3525
|
+
version: 2; // Payload format (absent on legacy v1 payloads)
|
|
3392
3526
|
recipients: Array<{
|
|
3393
3527
|
publicKey: string; // Base64 recipient public key (1568 bytes)
|
|
3394
|
-
ciphertext: string; // Base64 ML-KEM ciphertext
|
|
3528
|
+
ciphertext: string; // Base64 ML-KEM ciphertext + AES-KW wrapped key (1608 bytes)
|
|
3395
3529
|
}>;
|
|
3396
3530
|
encryptedData: string; // Base64 AES-encrypted data (shared across all recipients)
|
|
3397
3531
|
iv: string; // Base64 initialization vector (12 bytes)
|
|
@@ -3887,7 +4021,11 @@ import {
|
|
|
3887
4021
|
WalletError,
|
|
3888
4022
|
CryptoError,
|
|
3889
4023
|
StorageError,
|
|
3890
|
-
ApiError
|
|
4024
|
+
ApiError,
|
|
4025
|
+
SyncRequiredError,
|
|
4026
|
+
WalletMissingError,
|
|
4027
|
+
SyncBootstrapError,
|
|
4028
|
+
PrfUnsupportedError
|
|
3891
4029
|
} from 'w3pk'
|
|
3892
4030
|
```
|
|
3893
4031
|
|
|
@@ -3898,18 +4036,33 @@ import {
|
|
|
3898
4036
|
- `RegistrationError` - Registration failures
|
|
3899
4037
|
- `WalletError` - Wallet operations (derivation, signing)
|
|
3900
4038
|
- `CryptoError` - Cryptographic operations (encryption, decryption)
|
|
3901
|
-
- `StorageError` - Storage operations (
|
|
4039
|
+
- `StorageError` - Storage operations (localStorage)
|
|
3902
4040
|
- `ApiError` - API/network errors (RPC, chainlist)
|
|
4041
|
+
- `SyncRequiredError` - `login()`: passkey authenticated, but this device has no credential metadata (`credentialId`)
|
|
4042
|
+
- `WalletMissingError` - `login()`: credential metadata found, but the wallet record is gone (`credentialId`, `ethereumAddress`)
|
|
4043
|
+
- `SyncBootstrapError` - `syncWalletWithPasskey()`, `importFromSync()`: the backup can't set this device up for the passkey (`reason`, `credentialId`)
|
|
4044
|
+
- `PrfUnsupportedError` - `derivePrfSecret()`, `exportForSync()`, `importFromSync()`: the authenticator returned no WebAuthn PRF output (`credentialId`)
|
|
3903
4045
|
|
|
3904
4046
|
### Error Handling
|
|
3905
4047
|
|
|
3906
4048
|
```typescript
|
|
3907
|
-
import {
|
|
4049
|
+
import {
|
|
4050
|
+
AuthenticationError,
|
|
4051
|
+
WalletError,
|
|
4052
|
+
SyncRequiredError,
|
|
4053
|
+
WalletMissingError
|
|
4054
|
+
} from 'w3pk'
|
|
3908
4055
|
|
|
3909
4056
|
try {
|
|
3910
4057
|
await w3pk.login()
|
|
3911
4058
|
} catch (error) {
|
|
3912
|
-
if (error instanceof
|
|
4059
|
+
if (error instanceof SyncRequiredError) {
|
|
4060
|
+
// Passkey synced to this device, wallet data isn't: ask for the password backup
|
|
4061
|
+
await w3pk.syncWalletWithPasskey(backupFile, password)
|
|
4062
|
+
} else if (error instanceof WalletMissingError) {
|
|
4063
|
+
// Metadata here, wallet record gone: any backup of this passkey restores it
|
|
4064
|
+
await w3pk.syncWalletWithPasskey(backupFile, password)
|
|
4065
|
+
} else if (error instanceof AuthenticationError) {
|
|
3913
4066
|
console.error('Authentication failed:', error.message)
|
|
3914
4067
|
} else if (error instanceof WalletError) {
|
|
3915
4068
|
console.error('Wallet error:', error.message)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -105,7 +105,7 @@ interface WebAuthnCredential {
|
|
|
105
105
|
- Standards: BIP32, BIP39, BIP44, EIP-155
|
|
106
106
|
- Curve: secp256k1 (Bitcoin/Ethereum standard)
|
|
107
107
|
- Algorithm: ECDSA + Keccak-256
|
|
108
|
-
- Storage:
|
|
108
|
+
- Storage: localStorage (AES-256-GCM encrypted)
|
|
109
109
|
- Managed by: Application (w3pk)
|
|
110
110
|
|
|
111
111
|
**Capabilities**:
|
|
@@ -231,10 +231,10 @@ STEP 3: Derive Encryption Key
|
|
|
231
231
|
|
|
232
232
|
STEP 4: Encrypt & Store
|
|
233
233
|
┌────────────────────────────┬────────────────────────────┐
|
|
234
|
-
│ localStorage │
|
|
234
|
+
│ localStorage │ localStorage │
|
|
235
235
|
│ ───────────── │ ───────────── │
|
|
236
236
|
│ │ │
|
|
237
|
-
│ w3pk_credential_*: │
|
|
237
|
+
│ w3pk_credential_*: │ w3pk_wallet_<address>: │
|
|
238
238
|
│ { │ { │
|
|
239
239
|
│ id: "hash(abc123)", │ ethereumAddress, │
|
|
240
240
|
│ publicKey (plaintext), │ encryptedMnemonic, │
|
|
@@ -268,9 +268,9 @@ STEP 1: WebAuthn Authentication
|
|
|
268
268
|
|
|
269
269
|
STEP 2: Fetch Stored Data
|
|
270
270
|
┌────────────────────────────┬────────────────────────────┐
|
|
271
|
-
│ localStorage │
|
|
271
|
+
│ localStorage │ localStorage │
|
|
272
272
|
│ │ │
|
|
273
|
-
│ Get by credentialId: │ Get by
|
|
273
|
+
│ Get by credentialId: │ Get by address: │
|
|
274
274
|
│ → publicKey │ → encryptedMnemonic │
|
|
275
275
|
└─────────────┬──────────────┴──────────────┬─────────────┘
|
|
276
276
|
│ │
|
|
@@ -393,15 +393,16 @@ User wants to send 1 ETH
|
|
|
393
393
|
|
|
394
394
|
**Why publicKey is plaintext**: It's needed to derive the encryption key. It's the **public** part of the WebAuthn key pair—not sensitive.
|
|
395
395
|
|
|
396
|
-
###
|
|
396
|
+
### Wallet Storage
|
|
397
397
|
|
|
398
|
-
**
|
|
399
|
-
**
|
|
400
|
-
**Key Path**: `ethereumAddress`
|
|
398
|
+
**Storage**: localStorage, next to the credential metadata, so browser eviction removes both or neither
|
|
399
|
+
**Key**: `w3pk_wallet_<ethereumAddress>`
|
|
401
400
|
**Purpose**: Encrypted wallet storage
|
|
402
401
|
|
|
402
|
+
Persistent sessions use `w3pk_session_<ethereumAddress>` and backup metadata `w3pk_backups`, in the same localStorage.
|
|
403
|
+
|
|
403
404
|
```typescript
|
|
404
|
-
// Example record in
|
|
405
|
+
// Example record in localStorage
|
|
405
406
|
{
|
|
406
407
|
ethereumAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7",
|
|
407
408
|
encryptedMnemonic: "U2FsdGVkX1+vupppZksvRf...", // AES-256-GCM blob
|
|
@@ -563,7 +564,7 @@ Layer 2: Encryption Key Derivation
|
|
|
563
564
|
Layer 3: Encrypted Mnemonic Storage
|
|
564
565
|
┌─────────────────────────────────────┐
|
|
565
566
|
│ ✅ AES-256-GCM encryption │
|
|
566
|
-
│ ✅
|
|
567
|
+
│ ✅ localStorage storage │
|
|
567
568
|
│ ✅ Cannot decrypt without Layers 1&2│
|
|
568
569
|
└─────────────────────────────────────┘
|
|
569
570
|
↓ REVEALS
|
|
@@ -749,7 +750,7 @@ Passkey (Face ID) → Decrypt mnemonic → Use wallet
|
|
|
749
750
|
|---------|----------|---------------|------------|
|
|
750
751
|
| **Curve** | P-256 | secp256k1 | N/A (uses AES) |
|
|
751
752
|
| **Purpose** | Authentication | Transactions | Encryption |
|
|
752
|
-
| **Storage** | Secure Enclave |
|
|
753
|
+
| **Storage** | Secure Enclave | localStorage | None (derived) |
|
|
753
754
|
| **Backup** | iCloud/Google | 12-word phrase | N/A |
|
|
754
755
|
| **Standards** | FIDO2, W3C | BIP32/39/44 | PBKDF2, AES-GCM |
|
|
755
756
|
| **Managed By** | OS | App | App |
|
|
@@ -808,7 +809,7 @@ Scenarios:
|
|
|
808
809
|
}
|
|
809
810
|
```
|
|
810
811
|
|
|
811
|
-
### What's in
|
|
812
|
+
### What's in `w3pk_wallet_<address>`?
|
|
812
813
|
|
|
813
814
|
```json
|
|
814
815
|
{
|
|
@@ -97,6 +97,30 @@ This document details WebAuthn support across browsers, operating systems, and d
|
|
|
97
97
|
| **VirtualBox/VMs** | No biometric passthrough | Use password fallback |
|
|
98
98
|
| **Remote Desktop** | Biometrics not forwarded | Use local device |
|
|
99
99
|
|
|
100
|
+
#### 🗑️ **Storage That Can Be Wiped**
|
|
101
|
+
|
|
102
|
+
The encrypted wallet, persistent sessions and backup metadata live in localStorage, next to the credential metadata, so they are either all there or all gone. The passkey itself lives in the authenticator and survives all of the cases below, but the wallet does not: the mnemonic is random and can't be rebuilt from the passkey. Users need a backup file or the recovery phrase.
|
|
103
|
+
|
|
104
|
+
| Environment | What happens | Mitigation |
|
|
105
|
+
|-------------|--------------|------------|
|
|
106
|
+
| **Safari (macOS, iOS)** | localStorage deleted after 7 days of browser use without the user interacting with the site | Add the web app to the Home Screen (exempt), keep a backup |
|
|
107
|
+
| **Any browser: clearing site data** | localStorage deleted with cookies and site data | Keep a backup |
|
|
108
|
+
| **Private/Incognito windows** | localStorage discarded when the window closes | Don't use for a long-lived wallet |
|
|
109
|
+
| **Chrome/Firefox under storage pressure** | Origins without persistent storage can be evicted | w3pk calls `navigator.storage.persist()`; check `w3pk.storagePersisted` |
|
|
110
|
+
|
|
111
|
+
Chrome grants persistent storage from engagement heuristics (bookmarked, installed, frequently visited) without a prompt; Firefox asks the user; Safari doesn't exempt sites from its 7-day cap. See [SECURITY.md → Storage durability](./SECURITY.md#storage-durability-what-can-still-wipe-the-wallet).
|
|
112
|
+
|
|
113
|
+
`register()` and `getBackupStatus()` return `evictionRisk`, computed on each call:
|
|
114
|
+
|
|
115
|
+
| Environment | `evictionRisk` |
|
|
116
|
+
|-------------|----------------|
|
|
117
|
+
| **WebKit tab, storage not persisted** (every iOS browser, Safari and other WebKit browsers on desktop) | `true` |
|
|
118
|
+
| **WebKit home-screen app** (`display-mode: standalone` or `navigator.standalone`) | `false` |
|
|
119
|
+
| **WebKit, `navigator.storage.persisted()` is `true`** | `false` |
|
|
120
|
+
| **Chromium, Firefox** | `false` |
|
|
121
|
+
|
|
122
|
+
Block on a backup while `!hasBackup && evictionRisk`, and show a dismissible banner on `!hasBackup` otherwise. See [Integration Guidelines → Backup & Recovery](./INTEGRATION_GUIDELINES.md#backup--recovery).
|
|
123
|
+
|
|
100
124
|
### Hardware Security Keys
|
|
101
125
|
|
|
102
126
|
| Key Type | CTAP Version | w3pk Support | Release Date |
|