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.
@@ -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 initializes a connection to your browser's IndexedDB for secure local storage and sets up the WebAuthn configuration tailored to your device.
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 safely in your browser's IndexedDB. 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!
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 IndexedDB 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.
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 IndexedDB 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.
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 # IndexedDB for metadata
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 **IndexedDB**
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 **and** IndexedDB — 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.
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 blob that leaks *in isolation* (e.g. only the IndexedDB object, without localStorage'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.
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 IndexedDB 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.
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 alongside the blob as a **non-extractable `CryptoKey`**, so restore works without a prompt. Scripts (including XSS) can *use* it while it lives but can never read its bytes, and it cannot be recomputed from any stored strings. The honest limit: an attacker who copies the **full browser profile** at the storage-engine level carries the key material with it. Silent restore fundamentally requires a usable key on disk; this is the strongest form it can take.
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 **and** IndexedDB 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))
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 localStorage (metadata) AND IndexedDB (encrypted wallet)
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 BOTH localStorage AND IndexedDB can decrypt offline
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
- - Email security details to the maintainers (see README.md)
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` in IndexedDB, `publicKey` in plaintext 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.**
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 + IndexedDB
1389
- const stolenCredentialId = "..." // from IndexedDB (next to the ciphertext)
1390
- const stolenPublicKey = "..." // from localStorage (plaintext)
1391
- const stolenEncryptedSeed = "..." // from IndexedDB
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
- #### IndexedDB (Encrypted Wallet)
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
- │ │ IndexedDB (Origin-Scoped) │ │
1583
- │ │ Database: w3pk │ │
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
- │ │ ├─ Local Storage/ │ │
1639
- │ │ │ └─ https_example.com_0.localstorage │ │
1640
- │ │ │ → Contains credential metadata (plaintext) │ │
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 both │ │
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 and IndexedDB are **automatically scoped** to origin
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
- | **IndexedDB** | Encrypted wallet | Origin isolation + AES-256-GCM | ⚠️ XSS, file access + decryption |
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
- - IndexedDB: encryptedMnemonic (ciphertext)
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 IndexedDB
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
- ├─ Local Storage/leveldb/
1755
- │ └─ https_example.com_0.localstorage
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
- ├─ Local Storage\leveldb\
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
- ├─ webappsstore.sqlite (localStorage)
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 IndexedDB, survives page refresh
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 IndexedDB under a **WebAuthn-PRF-derived key** (HKDF-SHA256 of the authenticator's per-credential PRF secret); survives page refresh for the configured duration
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 a **non-extractable `CryptoKey`** for silent restore — usable by scripts, never readable, not recomputable from stored data
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 residual risk is a full browser-profile copy carrying the non-extractable key material at the storage-engine level; see [At-Rest Encryption → "Remember Me"](#at-rest-encryption-what-it-protects-and-what-it-doesnt))
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
- // IndexedDB (origin-scoped by browser)
2959
- // Store: wallets
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. **IndexedDB**: Origin-scoped by browser + encrypted with WebAuthn signature
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 IndexedDB (encrypted)
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 + IndexedDB. Requires device encryption for full protection.
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 + IndexedDB)
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:**