w3pk 0.10.2 → 0.10.3
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/dist/index.d.mts +124 -85
- package/dist/index.d.ts +124 -85
- 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 +1 -1
- package/dist/zk/index.js.map +1 -1
- 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 +87 -18
- package/docs/ARCHITECTURE.md +14 -13
- package/docs/BROWSER_COMPATIBILITY.md +13 -0
- package/docs/INTEGRATION_GUIDELINES.md +36 -20
- package/docs/POST_QUANTUM.md +236 -1468
- package/docs/QUICK_START.md +4 -4
- package/docs/RECOVERY.md +35 -1
- 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 +3 -2
- package/dist/inspect/node.d.mts +0 -112
- package/dist/inspect/node.d.ts +0 -112
package/docs/QUICK_START.md
CHANGED
|
@@ -26,7 +26,7 @@ Create a new w3pk instance:
|
|
|
26
26
|
const w3pk = createWeb3Passkey()
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
When you instantiate w3pk, the SDK
|
|
29
|
+
When you instantiate w3pk, the SDK uses your browser's localStorage for secure local storage and sets up the WebAuthn configuration tailored to your device.
|
|
30
30
|
|
|
31
31
|
It also prepares the session management system with a default duration of 1 hour, meaning you won't be repeatedly prompted for biometric authentication during that time. Everything happens client-side—no server connection is needed, keeping your wallet truly under your control.
|
|
32
32
|
|
|
@@ -45,7 +45,7 @@ console.log('Address:', address)
|
|
|
45
45
|
|
|
46
46
|
Registration is where the magic happens. First, w3pk generates a fresh BIP39 mnemonic—a 12-word recovery phrase that serves as the master key to your wallet. Then your device's biometric prompt appears (Face ID, Touch ID, Windows Hello, or similar), creating a WebAuthn credential tied to your hardware. The SDK derives your Ethereum wallet from the mnemonic using the standard BIP44 path (m/44'/60'/0'/0/0), giving you a proper Ethereum address.
|
|
47
47
|
|
|
48
|
-
Here's the security layer: the mnemonic gets encrypted using AES-GCM-256 with the WebAuthn credential as the encryption key, meaning only your biometric authentication can decrypt it. The encrypted wallet is stored
|
|
48
|
+
Here's the security layer: the mnemonic gets encrypted using AES-GCM-256 with the WebAuthn credential as the encryption key, meaning only your biometric authentication can decrypt it. The encrypted wallet is stored in your browser's localStorage, next to the credential metadata. To make your experience smooth, w3pk immediately starts a session by caching the decrypted mnemonic in memory for 1 hour, so you won't need to authenticate again right away. Finally, it returns your Ethereum address and username: you're ready to go!
|
|
49
49
|
|
|
50
50
|
### 4. Sign a Message
|
|
51
51
|
|
|
@@ -78,9 +78,9 @@ await w3pk.login()
|
|
|
78
78
|
console.log('Logged in')
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Login is a beautifully streamlined process. The SDK retrieves your credential ID from
|
|
81
|
+
Login is a beautifully streamlined process. The SDK retrieves your credential ID from localStorage and triggers WebAuthn authentication—this is when your biometric prompt appears. Notice there's no username or password to remember; your device recognizes you through your biometric data.
|
|
82
82
|
|
|
83
|
-
Once authenticated, w3pk retrieves your encrypted wallet from
|
|
83
|
+
Once authenticated, w3pk retrieves your encrypted wallet from localStorage and uses the WebAuthn signature as the decryption key to unlock your mnemonic. A fresh session starts immediately, caching the decrypted mnemonic in memory for 1 hour (this is configurable and balances security with user experience—long enough to avoid annoying prompts, short enough to stay secure). The method returns your user info (address and username), and you're good to go. For the next hour, all wallet operations will use this cached session without bothering you with repeated biometric prompts—smooth, secure, and user-friendly.
|
|
84
84
|
|
|
85
85
|
## Complete Example
|
|
86
86
|
|
package/docs/RECOVERY.md
CHANGED
|
@@ -58,6 +58,30 @@ Before diving into the recovery system, it's crucial to understand the differenc
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
+
## 🚦 Backup Status
|
|
62
|
+
|
|
63
|
+
Browser storage can be wiped (eviction, Safari's 7-day cap, clearing site data). Without a backup, the wallet is then lost for good, so ask for one right after registration and warn while none exists.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const { hasBackup } = await sdk.register({ username: 'alice' });
|
|
67
|
+
|
|
68
|
+
if (!hasBackup) {
|
|
69
|
+
// "Back up now": download a backup file...
|
|
70
|
+
const { blob, filename } = await sdk.createBackupFile('password', password);
|
|
71
|
+
// ...or show the recovery phrase, then record the user's confirmation
|
|
72
|
+
await sdk.confirmRecoveryPhraseBackup();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const status = await sdk.getBackupStatus();
|
|
76
|
+
status.hasBackup; // true once a backup file, QR code or confirmed phrase exists
|
|
77
|
+
status.lastBackupAt; // ISO 8601 time of the latest backup
|
|
78
|
+
status.methods; // e.g. ['file', 'phrase']
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
While `hasBackup` is `false`, the security score is capped at 20 and its level is `vulnerable`. Backups are recorded per wallet, in localStorage on this device.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
61
85
|
## 🎯 Backup File Format
|
|
62
86
|
|
|
63
87
|
All workflows use the same universal format:
|
|
@@ -478,6 +502,16 @@ const { blob, filename } = await w3pk.createBackupFile('passkey');
|
|
|
478
502
|
const { blob, filename } = await w3pk.createBackupFile('hybrid', password);
|
|
479
503
|
```
|
|
480
504
|
|
|
505
|
+
### Backup Status
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// Record that the user wrote down the recovery phrase
|
|
509
|
+
await w3pk.confirmRecoveryPhraseBackup();
|
|
510
|
+
|
|
511
|
+
// { hasBackup, lastBackupAt, methods, ..., securityScore }
|
|
512
|
+
const status = await w3pk.getBackupStatus();
|
|
513
|
+
```
|
|
514
|
+
|
|
481
515
|
### Restore from Backup
|
|
482
516
|
|
|
483
517
|
```typescript
|
|
@@ -621,7 +655,7 @@ src/backup/
|
|
|
621
655
|
├── backup-file.ts # Main backup orchestration
|
|
622
656
|
├── types.ts # TypeScript interfaces
|
|
623
657
|
├── encryption.ts # PBKDF2 + AES-256-GCM utilities
|
|
624
|
-
└── storage.ts #
|
|
658
|
+
└── storage.ts # localStorage for metadata
|
|
625
659
|
|
|
626
660
|
src/sync/
|
|
627
661
|
├── backup-sync.ts # DeviceSyncManager class
|
package/docs/SECURITY.md
CHANGED
|
@@ -31,22 +31,22 @@ key = PBKDF2( SHA-256("w3pk-v4:" + credentialId + ":" + publicKey),
|
|
|
31
31
|
|
|
32
32
|
Every input to that key lives in the browser profile:
|
|
33
33
|
|
|
34
|
-
- `credentialId` — stored next to the ciphertext in **
|
|
34
|
+
- `credentialId` — stored next to the ciphertext in **localStorage**
|
|
35
35
|
- `publicKey` — stored in **plaintext in localStorage** (it's needed for key derivation)
|
|
36
36
|
- `salt` — a **constant string** in the open-source code
|
|
37
37
|
|
|
38
|
-
So an attacker who can read this origin's localStorage
|
|
38
|
+
So an attacker who can read this origin's localStorage — a malicious browser extension, a one-shot XSS that exfiltrates storage, a disk image of an unlocked device, or a Time Machine / cloud backup of the browser profile — can recompute the key and decrypt the seed **offline, with no biometric/PIN prompt and without ever calling the SDK.** The prompt gates SDK control flow (a UX and anti-casual-misuse boundary), not the cryptography at rest.
|
|
39
39
|
|
|
40
|
-
**What at-rest encryption does buy you:** protection against a wallet
|
|
40
|
+
**What at-rest encryption does buy you:** protection against a wallet record that leaks *in isolation* (e.g. only the `w3pk_wallet_<address>` entry, without the credential record's public key) and against casual inspection. It is **not** a defense against read access to the full browser profile. The [Threat Model](#threat-model) section has the per-scenario matrix.
|
|
41
41
|
|
|
42
42
|
### "Remember Me" (persistent sessions): PRF-keyed, renewed at every real login
|
|
43
43
|
|
|
44
|
-
`persistentSession.enabled: true` stores the encrypted mnemonic in
|
|
44
|
+
`persistentSession.enabled: true` stores the encrypted mnemonic in localStorage so the user isn't prompted on every page refresh. Unlike wallet storage, the persistent-session blob is **not** encrypted under a public-data-derived key. It is encrypted under a key derived (HKDF-SHA256) from the **WebAuthn PRF extension output** — a per-credential secret the authenticator releases only during a user-verified (biometric/PIN) assertion. Nothing stored on disk can recompute it. Every real login re-evaluates the PRF (with a fixed input, so the output is deterministic per credential) and re-keys the blob; the session expiry *is* the renewal boundary.
|
|
45
45
|
|
|
46
46
|
The at-rest guarantee then depends on `requireReauth`:
|
|
47
47
|
|
|
48
48
|
- **`requireReauth: true` (default):** the key is **never stored**. Every restore re-derives it from a live assertion's PRF output. A storage-read attacker (extension, storage-exfiltrating XSS, disk image, cloud backup of the profile) gets ciphertext that cannot be decrypted without the physical authenticator — the blob is hardware-bound at rest.
|
|
49
|
-
- **`requireReauth: false` (silent "Remember Me"):** the PRF-derived key is stored
|
|
49
|
+
- **`requireReauth: false` (silent "Remember Me"):** the PRF-derived key is stored as base64url next to the blob, so restore works without a prompt; it is re-imported as a non-extractable `CryptoKey` before use. Anyone who can read this origin's localStorage (XSS, an extension, a disk image or profile backup) can decrypt the blob. This is less of a change than it sounds: when the key was a non-extractable `CryptoKey` in IndexedDB, script on the origin could already *use* it to decrypt, and the browser wrote its raw bytes to the profile on disk. Silent restore fundamentally requires a usable key at rest. The [cross-origin wallet](https://github.com/w3hc/w3pk/issues/156) moves this storage out of the host app's origin, so script on the host page can no longer read it.
|
|
50
50
|
- **No PRF support** (older authenticators, some browser/OS combos): persistent sessions are **not stored at all** — the device gets in-memory sessions only. There is deliberately no fallback to weaker encryption.
|
|
51
51
|
|
|
52
52
|
What "Remember Me" still changes is the **exposure window**, not the trust model: a compromise of the already-trusted origin (XSS, a malicious npm dependency, a compromised script/CDN) can use the live session *any time within the configured duration*, including while the tab is idle and across browser restarts — instead of only while the wallet is actively in use.
|
|
@@ -58,6 +58,21 @@ What "Remember Me" still changes is the **exposure window**, not the trust model
|
|
|
58
58
|
- `requireReauth: true` (default) still prompts on page refresh and keeps the blob hardware-bound; `requireReauth: false` restores silently for the whole duration.
|
|
59
59
|
- Do not enable it on shared or untrusted devices.
|
|
60
60
|
|
|
61
|
+
### Storage durability: what can still wipe the wallet
|
|
62
|
+
|
|
63
|
+
The encrypted wallet, persistent sessions and backup metadata live in localStorage, next to the credential metadata. Browsers evict or clear an origin's storage as a whole or by storage type, so they are either all there or all gone: a passkey is never left logging in to a missing wallet. Before v0.10.3 the wallet lived in IndexedDB, and Chrome's storage-pressure eviction could delete it while keeping the credential in localStorage, leaving a wallet that could not be rebuilt from the passkey.
|
|
64
|
+
|
|
65
|
+
After register, login and restore, w3pk calls `navigator.storage.persist()`. `w3pk.storagePersisted` reports the answer (`null` until the browser answers, or where the API is missing). Persisted storage is exempt from storage-pressure eviction.
|
|
66
|
+
|
|
67
|
+
What still wipes localStorage, and with it the wallet:
|
|
68
|
+
|
|
69
|
+
- **Safari's 7-day cap** on script-writable storage: Safari (macOS and iOS) deletes localStorage for a site the user hasn't interacted with for 7 days of browser use. Web apps added to the Home Screen are exempt.
|
|
70
|
+
- **Clearing site data**: "Clear browsing data", site settings, or developer tools.
|
|
71
|
+
- **Private browsing**: storage is discarded when the private window closes.
|
|
72
|
+
- **Storage-pressure eviction** in browsers that don't grant persistence: Chrome grants it from engagement heuristics and Firefox asks the user, so `storagePersisted` may be `false`.
|
|
73
|
+
|
|
74
|
+
The mnemonic is random, so it cannot be rebuilt from the passkey. Users need a backup file or the recovery phrase to survive any of the above (see [RECOVERY.md](./RECOVERY.md)).
|
|
75
|
+
|
|
61
76
|
### Origin / hostname / URL scoping
|
|
62
77
|
|
|
63
78
|
What the credential and stored data are bound to — three *different* scopes, and none of them is the full URL:
|
|
@@ -383,7 +398,7 @@ console.assert(
|
|
|
383
398
|
|
|
384
399
|
### ⚠️ NOT Protected Against
|
|
385
400
|
|
|
386
|
-
1. **Full browser-profile read access** - An attacker who can read this origin's localStorage
|
|
401
|
+
1. **Full browser-profile read access** - An attacker who can read this origin's localStorage derives the key and decrypts the seed offline, with no prompt (see [At-Rest Encryption](#at-rest-encryption-what-it-protects-and-what-it-doesnt))
|
|
387
402
|
2. **Malicious extension / XSS exfil / disk image / profile backup** - All are forms of the above storage-read attack
|
|
388
403
|
3. **Compromised origin code** - XSS, a malicious dependency, or a compromised CDN runs *as* your origin and can use an active or persistent session
|
|
389
404
|
4. **Physical coercion** - Forcing the user to authenticate
|
|
@@ -425,11 +440,11 @@ Understanding w3pk's security boundaries is critical for proper deployment. The
|
|
|
425
440
|
**How it works:**
|
|
426
441
|
- Wallet encrypted with AES-256-GCM
|
|
427
442
|
- Key derived from credential ID + public key (PBKDF2, 210k iterations)
|
|
428
|
-
- Requires both
|
|
443
|
+
- Requires both the credential record and the wallet record from localStorage
|
|
429
444
|
|
|
430
445
|
**Limitations:**
|
|
431
446
|
- **Key is deterministic** - same metadata = same key
|
|
432
|
-
- An attacker with
|
|
447
|
+
- An attacker with read access to localStorage can decrypt offline
|
|
433
448
|
- Does NOT require fresh authentication to derive the key
|
|
434
449
|
- Protection is authentication-gating by SDK, not cryptographic impossibility
|
|
435
450
|
|
|
@@ -1312,17 +1327,25 @@ If you discover a security vulnerability in w3pk, please report it responsibly:
|
|
|
1312
1327
|
- Attempt to exploit in production systems
|
|
1313
1328
|
|
|
1314
1329
|
**Do:**
|
|
1315
|
-
-
|
|
1330
|
+
- Report privately via [GitHub Security Advisories](https://github.com/w3hc/w3pk/security/advisories/new)
|
|
1316
1331
|
- Provide detailed reproduction steps
|
|
1317
1332
|
- Wait for confirmation before public disclosure
|
|
1318
1333
|
- Follow responsible disclosure timeline (typically 90 days)
|
|
1319
1334
|
|
|
1320
1335
|
**We will:**
|
|
1321
|
-
- Acknowledge receipt within 48 hours
|
|
1336
|
+
- Acknowledge receipt within 48 hours (within 24 hours for a vulnerability reported as actively exploited)
|
|
1322
1337
|
- Investigate and provide updates
|
|
1323
1338
|
- Work on a fix and coordinate disclosure
|
|
1324
1339
|
- Credit you in security advisories (if desired)
|
|
1325
1340
|
|
|
1341
|
+
### Coordinated Disclosure and Downstream Compliance (EU Cyber Resilience Act)
|
|
1342
|
+
|
|
1343
|
+
w3pk is FOSS (GPL-3.0) published outside the course of a commercial activity, which keeps it outside direct CRA "manufacturer" obligations. Applications that embed w3pk and are placed on the EU market commercially are generally the "manufacturer" under the CRA, and carry its vulnerability-handling duties — including the 24-hour early warning and 72-hour report to ENISA/CSIRTs for an actively exploited vulnerability, once the relevant CRA provisions apply.
|
|
1344
|
+
|
|
1345
|
+
The acknowledgment timeline above exists to support that: a fast, accurate advisory from us is what lets a downstream integrator meet their own reporting clock. If you are shipping a commercial product built on w3pk, treat our GitHub Security Advisories as your upstream source and don't wait on us past those windows — file your own report on the timeline the CRA requires and update it as we provide details.
|
|
1346
|
+
|
|
1347
|
+
**Outside the EU:** no direct equivalent imposes the same fixed vulnerability-reporting clock on a wallet SDK. The US has no mandatory product-security law comparable to the CRA — the closest analogue, the FCC's Cyber Trust Mark, is a voluntary IoT label; sector-specific rules (the SEC's 4-business-day incident disclosure, CISA's CIRCIA reporting) apply to public companies and critical-infrastructure operators, not FOSS maintainers or typical wallet integrators. China's amended Cybersecurity Law (effective January 1, 2026) tightens incident-reporting timelines and adds personal liability for responsible officers, but it is a data/incident-security regime, not a CRA-style product certification, and applies to entities operating in China rather than to this project. Integrators distributing into those markets should check their own obligations under those regimes separately; this section covers the EU CRA specifically because it is the one with reporting deadlines that reach this project's disclosure process.
|
|
1348
|
+
|
|
1326
1349
|
## How It Works
|
|
1327
1350
|
|
|
1328
1351
|
### 1. Encryption Key Derivation
|
|
@@ -1378,17 +1401,17 @@ const encryptionKey = await crypto.subtle.deriveKey(
|
|
|
1378
1401
|
**Important security properties (read honestly):**
|
|
1379
1402
|
|
|
1380
1403
|
- The encryption key is **deterministic** - the same credential metadata always produces the same key.
|
|
1381
|
-
- Both key inputs live in the browser profile: `credentialId`
|
|
1404
|
+
- Both key inputs live in the browser profile: `credentialId` next to the ciphertext and `publicKey` in plaintext, both in localStorage, and the salt is a constant in the source. **An attacker who can read this origin's storage can derive the key and decrypt the seed offline.**
|
|
1382
1405
|
- The biometric/PIN prompt is an **SDK control-flow gate**, not a cryptographic barrier. It stops someone from driving *your SDK* without authenticating; it does **not** stop someone who bypasses the SDK and decrypts the files directly.
|
|
1383
1406
|
- This is **not** signature-based or PRF-based encryption — the key is not bound to any authenticator-held secret.
|
|
1384
1407
|
|
|
1385
1408
|
**What an attacker with storage read access can actually do:**
|
|
1386
1409
|
|
|
1387
1410
|
```typescript
|
|
1388
|
-
// Attacker has copied this origin's localStorage
|
|
1389
|
-
const stolenCredentialId = "..." // from
|
|
1390
|
-
const stolenPublicKey = "..." // from
|
|
1391
|
-
const stolenEncryptedSeed = "..." // from
|
|
1411
|
+
// Attacker has copied this origin's localStorage
|
|
1412
|
+
const stolenCredentialId = "..." // from w3pk_wallet_<address> (next to the ciphertext)
|
|
1413
|
+
const stolenPublicKey = "..." // from w3pk_credential_<id> (plaintext)
|
|
1414
|
+
const stolenEncryptedSeed = "..." // from w3pk_wallet_<address>
|
|
1392
1415
|
|
|
1393
1416
|
// Recompute the key from public metadata — no authenticator, no prompt
|
|
1394
1417
|
const key = deriveEncryptionKeyFromWebAuthn(stolenCredentialId, stolenPublicKey)
|
|
@@ -1417,7 +1440,7 @@ The only thing PBKDF2's 210k iterations add here is a mild slowdown; since the i
|
|
|
1417
1440
|
}
|
|
1418
1441
|
```
|
|
1419
1442
|
|
|
1420
|
-
####
|
|
1443
|
+
#### LocalStorage (Encrypted Wallet, `w3pk_wallet_<address>`)
|
|
1421
1444
|
```json
|
|
1422
1445
|
{
|
|
1423
1446
|
"ethereumAddress": "0x1234...5678",
|
|
@@ -1579,9 +1602,8 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1579
1602
|
│ └────────────────────────────────────────────────────────┘ │
|
|
1580
1603
|
│ │
|
|
1581
1604
|
│ ┌────────────────────────────────────────────────────────┐ │
|
|
1582
|
-
│ │
|
|
1583
|
-
│ │
|
|
1584
|
-
│ │ Store: wallets │ │
|
|
1605
|
+
│ │ localStorage (Origin-Scoped) │ │
|
|
1606
|
+
│ │ Key: w3pk_wallet_<address> │ │
|
|
1585
1607
|
│ │ │ │
|
|
1586
1608
|
│ │ { │ │
|
|
1587
1609
|
│ │ "ethereumAddress": "0x1234...", // PUBLIC │ │
|
|
@@ -1592,7 +1614,7 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1592
1614
|
│ │ │ │
|
|
1593
1615
|
│ │ ⚠️ Encrypted mnemonic requires decryption key │ │
|
|
1594
1616
|
│ │ ⚠️ Key can be derived from localStorage metadata │ │
|
|
1595
|
-
│ │ ⚠️ Both needed for offline decryption
|
|
1617
|
+
│ │ ⚠️ Both records needed for offline decryption │ │
|
|
1596
1618
|
│ └────────────────────────────────────────────────────────┘ │
|
|
1597
1619
|
│ │
|
|
1598
1620
|
│ ┌────────────────────────────────────────────────────────┐ │
|
|
@@ -1635,17 +1657,13 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1635
1657
|
│ │ File System (Browser Profile Directory) │ │
|
|
1636
1658
|
│ │ │ │
|
|
1637
1659
|
│ │ ~/Library/Application Support/Google/Chrome/Default/ │ │
|
|
1638
|
-
│ │
|
|
1639
|
-
│ │
|
|
1640
|
-
│ │
|
|
1641
|
-
│ │ │ │ │
|
|
1642
|
-
│ │ └─ IndexedDB/ │ │
|
|
1643
|
-
│ │ └─ https_example.com_0/w3pk/ │ │
|
|
1644
|
-
│ │ → Contains encrypted wallet │ │
|
|
1660
|
+
│ │ └─ Local Storage/ │ │
|
|
1661
|
+
│ │ └─ https_example.com_0.localstorage │ │
|
|
1662
|
+
│ │ → Credential metadata and encrypted wallet │ │
|
|
1645
1663
|
│ │ │ │
|
|
1646
1664
|
│ │ ⚠️ Files stored on disk (not encrypted by browser) │ │
|
|
1647
1665
|
│ │ ⚠️ OS-level encryption (FileVault/BitLocker) needed │ │
|
|
1648
|
-
│ │ ⚠️ Attacker with file access can copy
|
|
1666
|
+
│ │ ⚠️ Attacker with file access can copy all of it │ │
|
|
1649
1667
|
│ └────────────────────────────────────────────────────────┘ │
|
|
1650
1668
|
│ │
|
|
1651
1669
|
└─────────────────────────────────────────────────────────────┘
|
|
@@ -1654,7 +1672,7 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1654
1672
|
#### Browser Security Mechanisms
|
|
1655
1673
|
|
|
1656
1674
|
**Origin Isolation:**
|
|
1657
|
-
- localStorage
|
|
1675
|
+
- localStorage is **automatically scoped** to origin
|
|
1658
1676
|
- `https://example.com` cannot access `https://attacker.com` storage
|
|
1659
1677
|
- Even subdomains are isolated: `app.example.com` ≠ `example.com`
|
|
1660
1678
|
- Protocol matters: `http://example.com` ≠ `https://example.com`
|
|
@@ -1688,7 +1706,7 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1688
1706
|
| Storage Layer | What's Stored | Protection | Attack Surface |
|
|
1689
1707
|
|---------------|---------------|------------|----------------|
|
|
1690
1708
|
| **localStorage** | Credential metadata (public) | Origin isolation | ⚠️ XSS, file access |
|
|
1691
|
-
| **
|
|
1709
|
+
| **localStorage** | Encrypted wallet, persistent sessions | Origin isolation + AES-256-GCM | ⚠️ XSS, file access + decryption |
|
|
1692
1710
|
| **Memory** | Plaintext mnemonic (during session) | Process isolation (weak) | ❌ XSS, memory dumps |
|
|
1693
1711
|
| **Secure Enclave** | WebAuthn private key | Hardware protection | ✅ Very strong (HW attacks only) |
|
|
1694
1712
|
| **File System** | Browser profile directory | OS permissions | ⚠️ Malware, physical access |
|
|
@@ -1707,7 +1725,7 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1707
1725
|
8. SDK encrypts mnemonic with AES-256-GCM
|
|
1708
1726
|
9. SDK stores:
|
|
1709
1727
|
- localStorage: credentialId, publicKey, address (plaintext)
|
|
1710
|
-
-
|
|
1728
|
+
- localStorage: encryptedMnemonic (ciphertext)
|
|
1711
1729
|
10. Secure Enclave stores: privateKey (hardware-protected)
|
|
1712
1730
|
```
|
|
1713
1731
|
|
|
@@ -1720,7 +1738,7 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1720
1738
|
5. Browser returns: signature
|
|
1721
1739
|
6. SDK verifies signature matches publicKey
|
|
1722
1740
|
7. SDK derives encryption key from credentialId + publicKey
|
|
1723
|
-
8. SDK decrypts wallet from
|
|
1741
|
+
8. SDK decrypts wallet from localStorage
|
|
1724
1742
|
9. SDK stores plaintext mnemonic in memory (session)
|
|
1725
1743
|
10. Session expires after configured duration
|
|
1726
1744
|
```
|
|
@@ -1751,24 +1769,20 @@ Understanding where w3pk stores data and how browser security mechanisms protect
|
|
|
1751
1769
|
**Chrome/Chromium (macOS):**
|
|
1752
1770
|
```
|
|
1753
1771
|
~/Library/Application Support/Google/Chrome/Default/
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
└─ IndexedDB/
|
|
1757
|
-
└─ https_example.com_0/
|
|
1772
|
+
└─ Local Storage/leveldb/
|
|
1773
|
+
└─ https_example.com_0.localstorage
|
|
1758
1774
|
```
|
|
1759
1775
|
|
|
1760
1776
|
**Chrome/Chromium (Windows):**
|
|
1761
1777
|
```
|
|
1762
1778
|
%LOCALAPPDATA%\Google\Chrome\User Data\Default\
|
|
1763
|
-
|
|
1764
|
-
└─ IndexedDB\
|
|
1779
|
+
└─ Local Storage\leveldb\
|
|
1765
1780
|
```
|
|
1766
1781
|
|
|
1767
1782
|
**Firefox (macOS):**
|
|
1768
1783
|
```
|
|
1769
1784
|
~/Library/Application Support/Firefox/Profiles/<profile>/
|
|
1770
|
-
|
|
1771
|
-
└─ storage/default/https+++example.com/ (IndexedDB)
|
|
1785
|
+
└─ storage/default/https+++example.com/ls/ (localStorage)
|
|
1772
1786
|
```
|
|
1773
1787
|
|
|
1774
1788
|
**Safari (macOS):**
|
|
@@ -2020,7 +2034,7 @@ await w3pk.deriveWallet(1) // ✅ Prompts for biometric (session expired)
|
|
|
2020
2034
|
w3pk supports **two types of sessions** (v0.8.2+):
|
|
2021
2035
|
|
|
2022
2036
|
1. **In-Memory Sessions** (default): RAM-only, cleared on page refresh
|
|
2023
|
-
2. **Persistent Sessions** (opt-in): Encrypted in
|
|
2037
|
+
2. **Persistent Sessions** (opt-in): Encrypted in localStorage, survives page refresh
|
|
2024
2038
|
|
|
2025
2039
|
**In-Memory Session (default):**
|
|
2026
2040
|
- ✅ Decrypted mnemonic cached in RAM only
|
|
@@ -2031,10 +2045,10 @@ w3pk supports **two types of sessions** (v0.8.2+):
|
|
|
2031
2045
|
- ✅ Cleared when browser tab closes
|
|
2032
2046
|
|
|
2033
2047
|
**Persistent Session (opt-in — "Remember Me"):**
|
|
2034
|
-
- Mnemonic encrypted in
|
|
2048
|
+
- Mnemonic encrypted in localStorage under a **WebAuthn-PRF-derived key** (HKDF-SHA256 of the authenticator's per-credential PRF secret); survives page refresh for the configured duration
|
|
2035
2049
|
- Re-keyed at every real (prompted) login — the session expiry is the renewal boundary
|
|
2036
2050
|
- `requireReauth: true`: the key is never stored; every restore re-derives it from a live user-verified assertion (blob is hardware-bound at rest)
|
|
2037
|
-
- `requireReauth: false`: the key is stored as
|
|
2051
|
+
- `requireReauth: false`: the key is stored as base64url next to the blob for silent restore — readable by anything that can read this origin's localStorage, until the [cross-origin wallet](https://github.com/w3hc/w3pk/issues/156) moves it out of the host app's origin
|
|
2038
2052
|
- Requires a PRF-capable authenticator; without PRF, no persistent session is stored (in-memory only, no weaker fallback)
|
|
2039
2053
|
- Only for STANDARD and YOLO modes; **NEVER** persisted for STRICT mode
|
|
2040
2054
|
- Time-limited expiration; cleared on logout / `clearSession()`
|
|
@@ -2045,7 +2059,7 @@ w3pk supports **two types of sessions** (v0.8.2+):
|
|
|
2045
2059
|
|
|
2046
2060
|
**Security properties:**
|
|
2047
2061
|
- Default (in-memory) sessions exist **only in RAM** - never written to disk
|
|
2048
|
-
- Persistent sessions are keyed to the authenticator hardware via the PRF extension — a storage-read attacker cannot recompute the key from anything on disk (with `requireReauth: false`, the
|
|
2062
|
+
- Persistent sessions are keyed to the authenticator hardware via the PRF extension — a storage-read attacker cannot recompute the key from anything on disk (except with `requireReauth: false`, where the key is stored next to the blob; see [At-Rest Encryption → "Remember Me"](#at-rest-encryption-what-it-protects-and-what-it-doesnt))
|
|
2049
2063
|
- Automatically cleared after expiration; cleared on logout (both RAM and persistent)
|
|
2050
2064
|
- STRICT mode **always** requires fresh authentication (no persistence)
|
|
2051
2065
|
|
|
@@ -2843,7 +2857,6 @@ await w3pk.login()
|
|
|
2843
2857
|
|
|
2844
2858
|
The attacker **cannot** use your `example.com` credential even if they:
|
|
2845
2859
|
- Copy your localStorage data
|
|
2846
|
-
- Copy your IndexedDB data
|
|
2847
2860
|
- Trick you into visiting their site
|
|
2848
2861
|
- Use an identical UI
|
|
2849
2862
|
|
|
@@ -2854,7 +2867,6 @@ The browser enforces that credentials for `example.com` can only be used on `exa
|
|
|
2854
2867
|
```typescript
|
|
2855
2868
|
// Browser storage is automatically scoped by origin
|
|
2856
2869
|
localStorage // Scoped to "https://example.com"
|
|
2857
|
-
IndexedDB // Scoped to "https://example.com"
|
|
2858
2870
|
|
|
2859
2871
|
// A different origin cannot access this storage
|
|
2860
2872
|
// - https://attacker.com → different origin
|
|
@@ -2955,8 +2967,8 @@ const w3pk = createWeb3Passkey({
|
|
|
2955
2967
|
"createdAt": 1234567890
|
|
2956
2968
|
}
|
|
2957
2969
|
|
|
2958
|
-
//
|
|
2959
|
-
//
|
|
2970
|
+
// localStorage (origin-scoped by browser)
|
|
2971
|
+
// Key: w3pk_wallet_<address>
|
|
2960
2972
|
{
|
|
2961
2973
|
"ethereumAddress": "0x1234...",
|
|
2962
2974
|
"encryptedMnemonic": "v1kT...", // AES-GCM encrypted
|
|
@@ -2973,9 +2985,8 @@ const w3pk = createWeb3Passkey({
|
|
|
2973
2985
|
|
|
2974
2986
|
**Security properties:**
|
|
2975
2987
|
|
|
2976
|
-
1. **localStorage**: Origin-scoped by browser (cannot access from different origin)
|
|
2977
|
-
2. **
|
|
2978
|
-
3. **Authenticator**: RP ID-bound + hardware-protected
|
|
2988
|
+
1. **localStorage**: Origin-scoped by browser (cannot access from different origin); the wallet is also encrypted under a key derived from the credential metadata
|
|
2989
|
+
2. **Authenticator**: RP ID-bound + hardware-protected
|
|
2979
2990
|
|
|
2980
2991
|
### Common Questions
|
|
2981
2992
|
|
|
@@ -3079,10 +3090,6 @@ Modern authenticators (TouchID, Windows Hello, YubiKey) have **built-in secure s
|
|
|
3079
3090
|
│ │ • Username │ │
|
|
3080
3091
|
│ │ • Public key (safe) │ │
|
|
3081
3092
|
│ │ • Ethereum address │ │
|
|
3082
|
-
│ └─────────────────────────────────┘ │
|
|
3083
|
-
│ │
|
|
3084
|
-
│ IndexedDB │
|
|
3085
|
-
│ ┌─────────────────────────────────┐ │
|
|
3086
3093
|
│ │ • Encrypted wallet │ │
|
|
3087
3094
|
│ │ • Credential ID │ │
|
|
3088
3095
|
│ └─────────────────────────────────┘ │
|
|
@@ -3138,7 +3145,7 @@ Modern authenticators (TouchID, Windows Hello, YubiKey) have **built-in secure s
|
|
|
3138
3145
|
- ❌ **NOT** the wallet mnemonic
|
|
3139
3146
|
|
|
3140
3147
|
**The wallet mnemonic is stored:**
|
|
3141
|
-
- In browser
|
|
3148
|
+
- In browser localStorage (encrypted)
|
|
3142
3149
|
- Can only be decrypted with WebAuthn signature
|
|
3143
3150
|
- Requires fresh biometric authentication
|
|
3144
3151
|
|
|
@@ -3598,7 +3605,7 @@ console.log('Write this down:', mnemonic)
|
|
|
3598
3605
|
| Best for | Convenience + Security | General use | High-value holdings |
|
|
3599
3606
|
|
|
3600
3607
|
**\*File access = theft?**
|
|
3601
|
-
- **w3pk:** Can decrypt mnemonic with localStorage
|
|
3608
|
+
- **w3pk:** Can decrypt mnemonic with localStorage. Requires device encryption for full protection.
|
|
3602
3609
|
- **MetaMask:** Can decrypt vault with password. Weak passwords are vulnerable to brute force.
|
|
3603
3610
|
- **Hardware Wallet:** Private keys never leave device. File access doesn't help attacker.
|
|
3604
3611
|
|
|
@@ -3792,7 +3799,7 @@ w3pk's security model combines **WebAuthn authentication** with **deterministic
|
|
|
3792
3799
|
An attacker would need different things depending on their goal:
|
|
3793
3800
|
|
|
3794
3801
|
**To decrypt the wallet offline (outside w3pk):**
|
|
3795
|
-
- Access to browser storage files (localStorage
|
|
3802
|
+
- Access to browser storage files (localStorage)
|
|
3796
3803
|
- Can then import mnemonic into any wallet
|
|
3797
3804
|
|
|
3798
3805
|
**To use the wallet via w3pk SDK:**
|