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.
@@ -0,0 +1,197 @@
1
+ # w3pk — Cross-Origin Iframe Wallet Implementation Plan
2
+
3
+ **Goal:** Remove all key material from the host app's JS realm. Host apps send payloads over postMessage; the wallet origin signs — or refuses, based on per-app policy. No onchain contracts, no gas, works on every chain. A companion browser extension (Phase 2) adds AI-powered explanation of what apps request.
4
+
5
+ **Threat model addressed:** Malicious or XSS-compromised host apps. Out of scope: compromised wallet origin, compromised device/OS, malicious browser extensions with full page access.
6
+
7
+ **Security claim (for SECURITY.md):** *Host compromise cannot extract keys; damage is bounded by the app's active grant and the balance of its app-specific address.* (Not: "prevents XSS.")
8
+
9
+ ---
10
+
11
+ # Phase 1 — Iframe Wallet with Iframe-Enforced Policy
12
+
13
+ ## 1. Architecture
14
+
15
+ ```
16
+ ┌─────────────────────────────┐ ┌──────────────────────────────┐
17
+ │ Host app (any origin) │ │ wallet.w3pk.w3hc.org (iframe) │
18
+ │ │ │ │
19
+ │ w3pk SDK (thin client) │◄──────►│ Key vault (mnemonic, HD │
20
+ │ - postMessage RPC │ post │ derivation, signing) │
21
+ │ - EIP-1193 provider shim │ Message│ Policy engine │
22
+ │ - no key material, ever │ │ Session manager │
23
+ └─────────────────────────────┘ │ WebAuthn (RP ID = wallet…w3hc.org) │
24
+ └──────────┬───────────────────┘
25
+ │ window.open (same origin)
26
+ ┌──────────▼───────────────────┐
27
+ │ Confirmation popup │
28
+ │ tx decoding + simulation │
29
+ │ grant display & approval │
30
+ │ biometric re-auth │
31
+ └──────────────────────────────┘
32
+ ```
33
+
34
+ Three deliverables:
35
+
36
+ 1. **`w3pk` SDK (host side)** — becomes a thin RPC client. Public API unchanged where possible (`register`, `login`, `signMessage`, `sendTransaction`, `getEIP1193Provider`, `deriveWallet` minus key export).
37
+ 2. **Wallet origin app (`wallet.w3pk.w3hc.org`)** — static, minimal-dependency page loaded in a hidden iframe. Owns WebAuthn credentials, mnemonic encryption/decryption, HD derivation, signing, sessions, and the policy engine.
38
+ 3. **Confirmation popup** — same wallet origin, opened via `window.open` for grant approval and per-signature confirmation. A separate window cannot be overlaid or clickjacked by the host.
39
+
40
+ Only requests and signatures cross the postMessage boundary. Private keys — master or derived — never do, in any mode.
41
+
42
+ ## 2. Security modes
43
+
44
+ | Mode | Address | User interaction | Enforcement |
45
+ |---|---|---|---|
46
+ | **PRIMARY** | P-256 passkey address (EIP-7951) | Biometric prompt on **every** signature | Hardware — every P-256 signature *is* a WebAuthn ceremony; the key physically cannot sign without the user. Strongest tier by construction. Limited to signatures / EIP-7951-capable flows (no plain EOA txs). |
47
+ | **STRICT** | STRICT-derived address | Confirmation popup + fresh biometric on **every** signature | Policy engine forces `confirm` classification for all requests. No persistent session. |
48
+ | **STANDARD** | STANDARD-derived address (per origin/tag) | User reviews and **approves the grant** (the JSON below) once at connect, in the popup. Within the grant: silent. Outside or above thresholds: escalates to popup. | Policy engine enforces the approved grant on every request. |
49
+ | **YOLO** | Tag-derived address | Grant approval once; then signs generously (high cap, broad silent list) | Same engine, permissive preset. **Key still never leaves the iframe** — YOLO is a policy, not key export. Backstopped by app-budget funding. |
50
+
51
+ Key mode principles:
52
+
53
+ - **STANDARD's consent moment is the grant, not each transaction.** The popup renders the grant human-readably (allowed contracts, spend cap, expiry, silent vs. confirm actions) and the user approves it with a biometric. Everything the grant covers is then silent; everything else escalates. Editing/renewing a grant re-runs the same approval.
54
+ - **PRIMARY and STRICT differ in *what* signs, not *how often* the user is asked:** PRIMARY's prompt is the P-256 hardware ceremony itself; STRICT's is the policy engine refusing to sign with the derived secp256k1 key without a fresh confirmation.
55
+ - **YOLO's worst case = its grant ∩ its address balance.** Surface app-specific addresses as "app budgets" in UX: fund the GAMING address with 0.1 ETH and that is the physical maximum loss, independent of the policy engine.
56
+
57
+ ## 3. Milestones
58
+
59
+ ### M1 — Wallet origin skeleton + transport
60
+
61
+ - **Naming decision (permanent):** wallet origin = `wallet.w3pk.w3hc.org`, RP ID = `wallet.w3pk.w3hc.org`, break-glass mirror = `recovery.wallet.w3pk.w3hc.org`. RP ID scoping consequences: the recovery mirror (child of the RP ID) can perform the passkey ceremony; the demo app at `w3pk.w3hc.org` (parent) cannot — it is a plain untrusted host app like any other. The RP ID and origin-scoped storage bind users to this hostname permanently; the `w3hc.org` zone must be governed accordingly (see M5 Availability). A later move to a standalone domain would require a per-user re-registration ceremony.
62
+ - Ship `wallet.w3pk.w3hc.org` as a static bundle: strict CSP, SRI, no third-party scripts, no analytics, near-zero dependencies.
63
+ - postMessage RPC layer:
64
+ - Handshake with capability exchange and protocol version.
65
+ - Every message validated against a JSON schema; strict `event.origin` checks on both sides; per-request nonces; responses via `MessageChannel` ports (not broadcast).
66
+ - **App identity = transport-level `event.origin`.** Never trust an origin claimed inside the payload.
67
+ - SDK refactor: replace in-realm crypto calls with RPC calls. Feature-flag so the legacy in-realm mode remains available during migration.
68
+
69
+ ### M2 — Key custody migration
70
+
71
+ - Move WebAuthn registration/login into the wallet origin. RP ID = `wallet.w3pk.w3hc.org` → host apps can no longer trigger the credential ceremony at all.
72
+ - Use the WebAuthn **PRF extension** (where available) to derive the mnemonic encryption key from the authenticator; fall back to the current encryption scheme otherwise. Decryption happens only in the wallet origin.
73
+ - HD derivation (origin/tag → app-specific keys) and EIP-7951 P-256 signing move into the wallet origin. Derived keys never cross postMessage.
74
+ - **Remove key export from the cross-origin API.** `exportMnemonic()` and YOLO key return are not representable in the protocol.
75
+ - Migration path for existing users: one-time flow that re-registers the credential under the wallet RP ID and re-encrypts the backup. `restoreFromBackupFile` keeps working, in the wallet origin.
76
+
77
+ ### M3 — Policy engine + grant approval flow
78
+
79
+ - Grant schema, stored per `(appOrigin, tag)` in the wallet origin (IndexedDB, encrypted at rest):
80
+
81
+ ```json
82
+ {
83
+ "version": 1,
84
+ "appOrigin": "https://game.example.com",
85
+ "tag": "GAMING",
86
+ "mode": "STANDARD",
87
+ "chains": [1, 10, 8453],
88
+ "expiry": "2026-07-19T00:00:00Z",
89
+ "spend": { "capWei": "100000000000000000", "windowHours": 24 },
90
+ "allowedContracts": [
91
+ { "address": "0xabc...", "selectors": ["0xa9059cbb"] }
92
+ ],
93
+ "silent": ["personal_sign", "eth_signTypedData_v4:whitelisted"],
94
+ "confirm": ["eth_sendTransaction:aboveCap", "approve", "permit"],
95
+ "forbid": ["eth_sign_rawHash:unlessTagged"]
96
+ }
97
+ ```
98
+
99
+ `allowedContracts` is a whitelist of existing third-party contracts the app may call — data the engine checks, nothing to deploy.
100
+
101
+ - **Grant approval flow (the STANDARD consent moment):** at first connect, the app proposes a grant; the popup renders it human-readably; the user approves with a biometric. Stored grants are renewable, editable, and revocable from a wallet-origin management screen. Expiry enforced by the engine.
102
+ - Enforcement pipeline on every signing request: `parse → match grant → decode calldata → classify (silent / confirm / deny) → sign or escalate`.
103
+ - Hard rules baked in regardless of grant or mode: `approve`/`permit`/`Permit2` and raw-hash signing always require confirmation; unlimited approvals rewritten or denied by default.
104
+ - Mode presets implemented as grant templates: STRICT = all-confirm + no persistence; YOLO = permissive; PRIMARY = routes to P-256 signing (inherently per-signature ceremony).
105
+
106
+ ### M4 — Confirmation popup (trusted UI)
107
+
108
+ - Same wallet origin, opened with `window.open`. Two jobs:
109
+ 1. **Grant approval** — render the proposed grant: which app, which contracts, spend cap, expiry, what will be silent vs. confirmed.
110
+ 2. **Per-signature confirmation** — decoded calldata (bundled/self-hosted ABI + 4byte registry), simulated balance diffs, target contract, requesting origin, and which grant rule triggered escalation.
111
+ - Fresh WebAuthn assertion for approvals and high-tier actions.
112
+ - Popup-blocked fallback: full-page redirect flow with return URL.
113
+ - **Must be safe standalone** — the Phase 2 extension enhances comprehension but is never required for safety.
114
+
115
+ ### M5 — Hardening + release
116
+
117
+ - Wallet origin: CSP report-only → enforce; `Cross-Origin-Opener-Policy`; deliberate frame-ancestors decision (allow embedding, bind identity to embedder origin).
118
+ - Extend the existing IPFS build-verification + onchain registry to the deployed wallet-origin bundle, not just the npm package.
119
+ - Test suite: hostile-host fixture attempting key extraction and policy bypass; policy engine property tests; replay/nonce tests on the RPC layer.
120
+ - Docs: update SECURITY.md with the bounded-damage claim above; explicitly state that workers/service workers are not a security *boundary* (they are used for availability, below).
121
+
122
+ #### Availability (the wallet origin as shared dependency)
123
+
124
+ The wallet is a static bundle on CDN-class hosting — no backend, no database, no server-side key handling. The encrypted mnemonic lives in the user's browser storage, so an outage never touches keys, funds, or sessions in progress. The remaining risk is sized and mitigated as follows:
125
+
126
+ - **Service worker in the wallet origin (availability, not security):** after first load, the wallet bundle serves from the user's own cache. Returning users keep authenticating and signing through an origin outage; only first-time users on a new device are affected.
127
+ - **Offline recovery is origin-independent:** the `standalone/` recovery tool + encrypted backup files + social recovery restore the mnemonic without any w3pk infrastructure. **Availability failure ≠ custody failure** — state this verbatim in SECURITY.md.
128
+ - **Break-glass mirror (later, optional):** RP ID = `wallet.w3pk.w3hc.org` means any subdomain of it can perform the passkey ceremony. A pre-published `recovery.wallet.w3pk.w3hc.org` on separate infrastructure lets users restore from a backup file even if the primary is dark (origin storage does not transfer, so this is a recovery path, not a hot mirror — which is all break-glass needs to be).
129
+ - **Domain governance is the real SPOF, not uptime:** the RP ID and origin-scoped storage bind users to the domain permanently. Registrar lock + 2FA, DNSSEC, long-term renewal, deploy credentials treated like a private key. Domain loss/hijack is the catastrophic scenario; CDN downtime is minutes per year.
130
+
131
+ Accurate availability claim: *during a wallet-origin outage, new users cannot onboard; existing users are unaffected; funds are never locked under any scenario.*
132
+
133
+ #### Verifiability (trusting the wallet origin without taking its word)
134
+
135
+ The wallet origin is the trusted computing base — it decrypts the mnemonic. The architecture does not remove that trust; it consolidates it (from "every host app, forever" to "one small static codebase") and then makes it verifiable in layers:
136
+
137
+ 1. **Auditable source:** the wallet-origin app is open source, like the SDK. Necessary, not sufficient.
138
+ 2. **Reproducible builds:** deterministic build pipeline — anyone can build a tagged release and get byte-identical output whose IPFS CID matches the onchain registry entry. The published artifact is provably derived from the audited source.
139
+ 3. **Served-bytes verification (the hard part):** a server can serve *different* code to a targeted user — the classic weakness of web-delivered crypto that no repo audit catches. Mitigations:
140
+ - **The Phase 2 extension verifies each load:** hash the served wallet bundle, compare against the registry CID, badge ✅/❌ in the popup. A targeted swap becomes detectable by any extension user, which deters it against all users. This is the extension's *primary* job (M6), ahead of AI explanations.
141
+ - **Service-worker pinning helps incidentally:** returning users run cached code, so a swap doesn't reach them until refresh — widening the detection window.
142
+ - **Standards track:** adopt browser-native web app integrity/transparency (manifest-pinned, publicly logged bundles) when it ships; the extension is the practical substitute until then.
143
+
144
+ Residual gap, stated honestly in SECURITY.md: a first-time user with no tools trusts DNS + TLS + the deploy pipeline for that first load — like every web wallet; unlike most, we say so.
145
+
146
+ ---
147
+
148
+ # Phase 2 — AI Inspection Extension
149
+
150
+ **Goal:** A standalone browser extension with three jobs, in priority order: (a) verify that the wallet origin serves the registry-listed build (served-bytes integrity, see M5 Verifiability), (b) analyze the dapp a user is visiting (the AI inspection feature moved out of the SDK), and (c) explain grants in plain language during STANDARD-mode approval.
151
+
152
+ **Why move it out of the SDK:**
153
+ - Inside the SDK it runs in the host realm — a malicious host can suppress or falsify its own inspection. An extension is an independent trust surface the host cannot touch.
154
+ - It removes the Rukh/AI dependency (and its bundle weight and sponsored-API coupling) from the core signing path, keeping the wallet origin minimal and auditable.
155
+
156
+ **Role boundary (invariant):** the extension is a **comprehension layer, never a security dependency**. The popup's own decoding + simulation must remain sufficient for a safe decision — most users won't have the extension, and non-Chromium browsers exist.
157
+
158
+ ## Milestones
159
+
160
+ ### M6 — Extension MVP: bundle verification + standalone dapp inspection
161
+
162
+ - **Wallet-bundle integrity check (primary):** on each wallet-origin load, hash the served bundle, compare against the onchain registry CID, badge ✅/❌ in the popup and extension icon. Requires M5's reproducible-build + registry pipeline to exist first.
163
+ - Port `inspect`/`gatherCode` browser logic to an MV3 extension: user visits a dapp, clicks the extension, gets the Rukh-generated security report (transaction methods, signing patterns, red flags) in the extension UI.
164
+ - Deprecate `inspect`/`inspectNow` in the SDK (keep the Node.js variant for CI/dev workflows).
165
+ - Privacy: page content is sent to the Rukh API — explicit consent on first use, per-site opt-in, document what is transmitted. (The integrity check is fully local — hashing only, nothing transmitted.)
166
+
167
+ ### M7 — Grant explanation in the approval flow
168
+
169
+ - When the wallet popup opens for grant approval, it signals the extension (wallet-origin ↔ extension messaging via `externally_connectable`, pinned to the extension ID; never through the host page).
170
+ - The extension fetches/produces a plain-language explanation: *"This game is asking to spend up to 0.1 ETH from your GAMING budget on two contracts (its game contract and USDC) until Friday, without asking each time. It also wants silent message signing — typical for game session logins."*
171
+ - Rendered in the popup as an annotation clearly attributed to the extension, alongside — never replacing — the popup's native decoded view. Absence of the extension degrades gracefully to the native view.
172
+ - Flag mismatches: grant requests exceeding what the inspected dapp code appears to need (e.g. asks for token-approval rights but its code never calls approve).
173
+
174
+ ### M8 — Trust & distribution
175
+
176
+ - The extension is now itself a trust surface: pin its ID in the wallet origin's allowlist, sign releases, extend IPFS build-verification to extension builds, publish to Chrome Web Store (Firefox port later).
177
+ - Fallback path: the popup can offer the same Rukh-generated explanation via a direct wallet-origin → Rukh call for users without the extension (marked as such), keeping the extension optional rather than privileged.
178
+
179
+ ---
180
+
181
+ # Security invariants (both phases)
182
+
183
+ 1. No private key or mnemonic ever crosses the postMessage boundary, in any mode — including YOLO.
184
+ 2. Raw-hash signing is never silent.
185
+ 3. App identity comes from transport-level `event.origin`, never from the payload.
186
+ 4. `approve`/`permit`-class signatures always escalate to trusted UI.
187
+ 5. The wallet-origin bundle (and later the extension) is registry-verified (IPFS CID) like the SDK itself; builds are reproducible.
188
+ 6. The extension informs and verifies; it never authorizes. The popup + biometric remain the only consent path.
189
+ 7. Trust is consolidated, not eliminated: the wallet origin is the trusted computing base, and the docs say so.
190
+
191
+ # Sequencing
192
+
193
+ Phase 1: M1–M2 first — the custody boundary is the entire point. M3–M4 in parallel once transport is stable. M5 gates GA. Phase 2 starts after M4 exists (the popup is the extension's integration point); the dapp-inspection half of M6 can begin earlier as a standalone port, but its integrity-check half depends on M5's reproducible-build pipeline.
194
+
195
+ # Future path (out of scope)
196
+
197
+ The grant JSON is deliberately enforcement-agnostic. If onchain enforcement (EIP-7702 session keys, gas sponsorship via 4337) becomes worth it, the same grants compile to an audited delegate's permission module — an upgrade per grant, not a redesign.
@@ -0,0 +1,29 @@
1
+ # July 13 Roadmap
2
+
3
+ Issue/PR titles for w3pk, derived from [SYNCING_ACROSS_DEVICES.md](SYNCING_ACROSS_DEVICES.md) and [IFRAME_IMPLEMENTATION_PLAN.md](IFRAME_IMPLEMENTATION_PLAN.md), following the project's PR naming convention (imperative, sentence case, backticks for code identifiers).
4
+
5
+ ---
6
+
7
+ ## 1. Remove AI inspection
8
+
9
+ 1. Remove AI inspection from the SDK
10
+
11
+ ## 2. Syncing across devices
12
+
13
+ 2. Add PRF-based passwordless device sync
14
+ - PRF key derivation in `crypto.ts`, `prf` encryption method for backup files, PRF payloads in `exportForSync()` and `syncWalletWithPasskey`
15
+ 3. Fix fresh-device sync flow
16
+ - Wallet persistence when no local credential exists, remove login requirement from `importFromSync`, replace the dead-end login error with a typed `SyncRequiredError`
17
+
18
+ ## 3. Cross-origin iframe wallet
19
+
20
+ 4. Add wallet origin with postMessage RPC transport
21
+ - Static bundle at `wallet.w3pk.w3hc.org`, handshake + origin checks + nonces, SDK becomes a thin RPC client
22
+ 5. Move key custody into the wallet origin
23
+ - WebAuthn under the wallet RP ID, HD derivation and EIP-7951 signing in-origin, remove key export from the cross-origin API, one-time credential re-registration flow
24
+ 6. Add policy engine and grant approval flow
25
+ - Grant schema and per-origin storage, enforcement pipeline, hard rules for `approve`/`permit` and raw-hash signing, mode presets as grant templates, grant management screen
26
+ 7. Add confirmation popup
27
+ - Calldata decoding and simulation, fresh WebAuthn assertion for approvals, popup-blocked redirect fallback
28
+ 8. Harden and release the wallet origin
29
+ - CSP/COOP/frame-ancestors, onchain build verification of the wallet bundle, hostile-host test suite, service worker for availability, break-glass recovery mirror, SECURITY.md update
@@ -0,0 +1,333 @@
1
+ # w3pk Security Audit Findings
2
+
3
+ **Target:** [w3hc/w3pk](https://github.com/w3hc/w3pk) — `114-audit` branch
4
+ **Commit:** `c82a69affb472598babd7570c3e57613a84e7162`
5
+ **Scope:** `src/` per README, excluding `src/inspect`, `src/zk`, `src/education`, plus the
6
+ `docs`, `examples`, `scripts`, `standalone`, and `test` folders. Solidity contract excluded.
7
+ **Codebase size:** ~11,904 lines of TS/JS in scope.
8
+
9
+ > This document is an independent code review, **not** a substitute for a full professional
10
+ > audit. It reflects manual reading of the security-critical modules (wallet/key derivation,
11
+ > backup encryption, Shamir recovery, social recovery, stealth addresses, origin derivation,
12
+ > SIWE, sync, persistent sessions). It has not been fuzzed, formally verified, or run against
13
+ > a test suite. Absence of a finding in a module is not proof that module is safe.
14
+
15
+ ---
16
+
17
+ ## Severity summary
18
+
19
+ | # | Severity | Finding | Location | Status |
20
+ |---|----------|---------|----------|--------|
21
+ | 1 | Critical | Wallet encryption key derived entirely from public WebAuthn data | `src/wallet/crypto.ts` | ✅ FIXED |
22
+ | 2 | Critical | Broken `recoverFromShares` threshold logic in backup-based recovery | `src/recovery/backup-based-recovery.ts` | ✅ FIXED |
23
+ | 3 | High | Hardcoded constant salt in WebAuthn KDF | `src/wallet/crypto.ts` | ✅ FIXED |
24
+ | 4 | High | Stealth scalars not reduced mod curve order (ERC-5564) | `src/stealth/crypto.ts` | ✅ FIXED |
25
+ | 5 | Medium | GF(256) table construction in Shamir needs verification | `src/recovery/shamir.ts` | |
26
+ | 6 | Medium | Shamir share combination has no integrity/authenticity check | `src/recovery/shamir.ts`, recovery managers | |
27
+ | 7 | Medium | P-256 → Ethereum address derivation skips EIP-55 checksum | `src/wallet/origin-derivation.ts` | |
28
+ | 8 | Medium | Origin-derived index uses only 32 bits of SHA-256 with modulo bias | `src/wallet/origin-derivation.ts` | |
29
+ | 9 | Medium | Persistent sessions: at-rest mnemonic protection inherits findings #1/#3 | `src/core/persistent-session.ts` | |
30
+ | 10 | Low | Weak password-strength acceptance threshold | `src/backup/encryption.ts` | |
31
+ | 11 | Low | Guardian shares transported/stored with weak handling guidance | `src/recovery/social.ts` | |
32
+ | 12 | Low | SIWE parser is hand-rolled and diverges from EIP-4361 ABNF | `src/siwe/index.ts` | |
33
+ | 13 | Low | Device fingerprint and sync-method detection are spoofable | `src/backup/encryption.ts`, `src/sync/*` | |
34
+ | 14 | Info | Deprecated `deriveEncryptionKey` (v2) still present | `src/wallet/crypto.ts` | |
35
+ | 15 | Info | `console.error`/`console.warn` may leak diagnostic detail | multiple | |
36
+
37
+ ---
38
+
39
+ ## 1. Critical — Wallet encryption key derived entirely from public WebAuthn data
40
+
41
+ **File:** `src/wallet/crypto.ts` — `deriveEncryptionKeyFromWebAuthn`
42
+
43
+ The AES-GCM key protecting the BIP39 mnemonic is derived solely from `credentialId` and,
44
+ optionally, the WebAuthn `publicKey`:
45
+
46
+ ```
47
+ keyMaterial = `w3pk-v4:${credentialId}:${publicKey}`
48
+ ```
49
+
50
+ Both inputs are **public**. The WebAuthn `credential.id` is transmitted in the clear and stored
51
+ next to the ciphertext (`src/auth/authenticate.ts` persists `credentialId: credential.id`); the
52
+ public key is public by definition. The biometric/PIN prompt gates only the *ceremony*, not the
53
+ key material — no authenticator secret (PRF / `hmac-secret` output, signature, UV-derived value)
54
+ enters the KDF.
55
+
56
+ **Impact:** Anyone holding the ciphertext plus the credential ID (which travels with it) can
57
+ recompute the key offline and decrypt the mnemonic. The 210,000 PBKDF2 iterations add nothing
58
+ because the "password" is a known constant. This is a full break of the WebAuthn-protected
59
+ encryption.
60
+
61
+ **Propagation:** the same primitive is reused in `src/sync/vault.ts` (`createSyncPackage` /
62
+ `restoreFromSync`), `src/sync/backup-sync.ts` (passkey export/import), and
63
+ `src/core/persistent-session.ts`. All inherit the weakness.
64
+
65
+ **Recommendation:** Use the WebAuthn **PRF extension** (or `hmac-secret`) to obtain a
66
+ per-credential secret that never leaves the authenticator and feed that into the KDF. The
67
+ encryption key must depend on a value an attacker holding the ciphertext cannot reconstruct.
68
+
69
+ ---
70
+
71
+ ## 2. Critical — Broken `recoverFromShares` threshold logic
72
+
73
+ **File:** `src/recovery/backup-based-recovery.ts` — `SocialRecovery.recoverFromShares`
74
+
75
+ This method calls `combineShares(shareBytes, shares.length)` — it passes the **number of shares
76
+ provided** as the threshold, rather than the **threshold the secret was actually split with**.
77
+
78
+ ```
79
+ const recoveredBytes = combineShares(shareBytes, shares.length);
80
+ ```
81
+
82
+ In `shamir.ts`, `combineShares(shares, threshold)` uses `shares.slice(0, threshold)` and
83
+ Lagrange-interpolates assuming the polynomial degree is `threshold - 1`. If the caller supplies
84
+ a number of shares different from the original split threshold, interpolation reconstructs the
85
+ **wrong** secret (silently, since GF(256) interpolation always returns *a* value). The only thing
86
+ catching it afterward is a JSON-parse + address-match check, which will usually throw "invalid
87
+ shares" — but can in principle yield a corrupted-but-parseable result.
88
+
89
+ The companion implementation in `src/recovery/social.ts` (`recoverFromGuardians`) does this
90
+ **correctly**, reading `threshold` from the embedded share metadata. The two recovery managers
91
+ are inconsistent, and the `backup-based-recovery.ts` one is wrong.
92
+
93
+ **Impact:** Recovery via this code path fails (or, worst case, mis-reconstructs) whenever the
94
+ number of presented shares ≠ the original threshold — which is the normal case when more than the
95
+ minimum number of guardians respond. Loss-of-access risk for funds protected by social recovery.
96
+
97
+ **Recommendation:** Embed the original `threshold` in each share's metadata and always pass that
98
+ value to `combineShares`. Add round-trip tests for `M < provided ≤ N`.
99
+
100
+ ---
101
+
102
+ ## 3. High — Hardcoded constant salt in WebAuthn KDF
103
+
104
+ **File:** `src/wallet/crypto.ts` — `deriveEncryptionKeyFromWebAuthn` (and deprecated
105
+ `deriveEncryptionKey`)
106
+
107
+ The PBKDF2 salt is a fixed value derived from the literal string `"w3pk-salt-v4"` (and
108
+ `"w3pk-salt-v2"`). A constant, globally known salt gives no per-user separation and permits
109
+ precomputation across all users.
110
+
111
+ **Note:** the password-based backup path in `src/backup/encryption.ts` does this correctly
112
+ (random per-encryption salt). The WebAuthn path should follow that pattern.
113
+
114
+ **Recommendation:** Generate a random unique salt per encryption and store it with the ciphertext.
115
+
116
+ ---
117
+
118
+ ## 4. High — Stealth scalars not reduced modulo the curve order
119
+
120
+ **File:** `src/stealth/crypto.ts` — `multiplyGeneratorByScalar`, `addPrivateKeys`
121
+
122
+ The ERC-5564 routines hash the ECDH shared secret with `keccak256` and use the raw 32-byte result
123
+ directly as a secp256k1 scalar:
124
+
125
+ - `multiplyGeneratorByScalar(hashedSharedSecret)` calls `new ethers.Wallet(scalar)` on the raw
126
+ hash. If the hash is `0` or `≥ n` (curve order), ethers throws — a small but nonzero failure
127
+ probability per announcement, producing intermittent, hard-to-reproduce errors.
128
+ - `addPrivateKeys` reduces `(k1 + k2) mod n` correctly, but the `s_h` term entering it is the
129
+ unreduced hash; the spec computes `s_h` as a scalar and consistency between the public-key side
130
+ (`s_h × G`) and the private-key side (`spending_privkey + s_h`) requires the **same reduced
131
+ scalar** be used on both sides.
132
+
133
+ Because the public-key path (`multiplyGeneratorByScalar`) and the private-key path
134
+ (`addPrivateKeys`) treat `s_h` inconsistently with respect to reduction, there is risk that a
135
+ derived stealth address cannot be spent by the derived private key in edge cases.
136
+
137
+ **Additional concern:** `addPublicKeys` implements raw secp256k1 point addition in `BigInt` with
138
+ no handling for the point-at-infinity case (`x1 === x2 && y1 !== y2`), and uses `computeSharedSecret`
139
+ returning a value treated as the ECDH x-coordinate hashed directly — verify it matches the ERC-5564
140
+ shared-secret definition (the standard hashes the compressed shared point, conventions vary).
141
+
142
+ **Impact:** Potentially unspendable stealth funds in edge cases; intermittent failures.
143
+
144
+ **Recommendation:** Reduce `s_h` mod `n` once, reject `0`, and use that single reduced scalar on
145
+ both the public-key and private-key sides. Use a vetted secp256k1 library (e.g. `@noble/secp256k1`)
146
+ for point/scalar arithmetic rather than hand-rolled BigInt math, and add cross-check tests asserting
147
+ `address(stealthPrivKey) === stealthAddress` over many randomized runs.
148
+
149
+ **Fix:** Implemented `reduceScalarModN()` function that:
150
+ - Reduces any scalar modulo the secp256k1 curve order `n`
151
+ - Validates the scalar is in the range `[1, n-1]`
152
+ - Rejects zero scalars with proper error handling
153
+
154
+ Updated all three stealth address functions (`generateStealthAddress`, `checkStealthAddress`,
155
+ `computeStealthPrivateKey`) to reduce `hashedSharedSecret` using `reduceScalarModN()` before
156
+ using it in either public-key or private-key operations. This ensures the same properly-reduced
157
+ scalar `s_h` is used consistently on both code paths, eliminating the risk of unspendable funds
158
+ and intermittent failures.
159
+
160
+ ---
161
+
162
+ ## 5. Medium — GF(256) table construction in Shamir needs verification
163
+
164
+ **File:** `src/recovery/shamir.ts` — `GF256`
165
+
166
+ Log/exp tables are built for indices 0–254 followed by `EXP_TABLE[255] = EXP_TABLE[0]`. `divide`
167
+ computes `(LOG[a] - LOG[b] + 255) % 255` (range 0–254) and `multiply` reduces by `% 255`, so the
168
+ `EXP_TABLE[255]` entry is dead but harmless. The real risk is that a subtle off-by-one in field
169
+ construction silently corrupts recovered secrets without throwing.
170
+
171
+ **Recommendation:** Add an exhaustive round-trip test
172
+ (`combineShares(splitSecret(x, t, n), t) === x`) over all 256 byte values and representative
173
+ `(t, n)` pairs, plus known-answer vectors against a reference implementation. Gate the recovery
174
+ feature on this.
175
+
176
+ ---
177
+
178
+ ## 6. Medium — Shamir share combination has no integrity/authenticity check
179
+
180
+ **Files:** `src/recovery/shamir.ts` (`combineShares`); `src/recovery/social.ts`,
181
+ `src/recovery/backup-based-recovery.ts`
182
+
183
+ `combineShares` blindly takes the first `threshold` shares and interpolates. A single corrupted or
184
+ maliciously crafted share silently yields a wrong secret. Downstream JSON-parse + address-match
185
+ checks catch *most* corruption but are not cryptographic integrity guarantees, and a malicious
186
+ guardian can grief recovery by submitting a bad share that's indistinguishable until reconstruction
187
+ fails.
188
+
189
+ **Recommendation:** Use an authenticated share format — e.g. include a hash commitment to the
190
+ secret and/or a per-share MAC so individual shares can be validated before combination, and so a
191
+ bad share can be attributed.
192
+
193
+ ---
194
+
195
+ ## 7. Medium — P-256 → Ethereum address derivation skips EIP-55 checksum
196
+
197
+ **File:** `src/wallet/origin-derivation.ts` — `deriveAddressFromP256PublicKey`
198
+
199
+ The function returns `'0x' + hash.slice(-40)` directly (lower-case, unchecksummed), unlike the rest
200
+ of the codebase which uses `ethers.getAddress(...)`. Downstream comparisons that assume EIP-55
201
+ checksumming (e.g. strict equality against a checksummed address) can produce false mismatches, and
202
+ unchecksummed addresses surfaced to users reduce typo protection.
203
+
204
+ **Recommendation:** Wrap the result in `ethers.getAddress(...)`.
205
+
206
+ ---
207
+
208
+ ## 8. Medium — Origin-derived index uses 32 bits of SHA-256 with modulo bias
209
+
210
+ **File:** `src/wallet/origin-derivation.ts` — `deriveIndexFromOriginModeAndTag`
211
+
212
+ The derivation reads only the first 4 bytes of the SHA-256 digest (`getUint32(0)`) and computes
213
+ `uint32 % MAX_INDEX` where `MAX_INDEX = 0x7fffffff`. Two issues: (a) only 32 bits of entropy feed
214
+ the index, and (b) `2^32 % (2^31 - 1)` introduces a small modulo bias toward low indices. While
215
+ BIP32 non-hardened indices are inherently public and this is not a secret, collisions between
216
+ distinct (origin, mode, tag) tuples would map two different sites to the **same** derived wallet,
217
+ which is a correctness/privacy concern.
218
+
219
+ **Recommendation:** Document the collision model explicitly, or widen the index space and use
220
+ rejection sampling instead of modulo to remove bias.
221
+
222
+ ---
223
+
224
+ ## 9. Medium — Persistent sessions inherit the WebAuthn at-rest weakness
225
+
226
+ **File:** `src/core/persistent-session.ts`
227
+
228
+ Persistent sessions store an `encryptedMnemonic` in IndexedDB, encrypted via
229
+ `deriveEncryptionKeyFromWebAuthn` (findings #1/#3). The module correctly refuses to persist
230
+ `STRICT` mode and enforces expiry, but "remember me" here means a mnemonic encrypted with public
231
+ inputs sits at rest in the browser. The `credentialId` is also stored in the same record, i.e. the
232
+ ciphertext and the (public) key-derivation input live together.
233
+
234
+ **Recommendation:** Fix #1/#3 first; persistent sessions are only as strong as the underlying KDF.
235
+ Consider also requiring a fresh WebAuthn assertion (UV) to unwrap the session even when "remember
236
+ me" is on.
237
+
238
+ ---
239
+
240
+ ## 10. Low — Weak password-strength acceptance threshold
241
+
242
+ **File:** `src/backup/encryption.ts` — `validatePasswordStrength`
243
+
244
+ Accepts `valid: true` at `score >= 50` with no remaining feedback — satisfiable with length plus
245
+ two character classes, weak for a wallet backup password.
246
+
247
+ **Recommendation:** Raise the bar and/or adopt an entropy estimator (e.g. zxcvbn) instead of a
248
+ heuristic score.
249
+
250
+ ---
251
+
252
+ ## 11. Low — Guardian-share handling guidance encourages risky storage
253
+
254
+ **File:** `src/recovery/social.ts` (and `backup-based-recovery.ts`)
255
+
256
+ The generated guardian explainer text states shares are "Safe to store digitally" and suggests
257
+ screenshots. While a single share below threshold is individually useless, the human-facing copy
258
+ underplays that an attacker collecting `threshold` shares (e.g. via compromised cloud backups of
259
+ several guardians) fully reconstructs the **password-encrypted** backup — whose password may itself
260
+ be weak (finding #10). Guidance should emphasize that shares are sensitive and that the backup
261
+ password is the last line of defense.
262
+
263
+ **Recommendation:** Tighten the guardian instructions; recommend offline/encrypted storage and warn
264
+ against screenshots/unencrypted cloud.
265
+
266
+ ---
267
+
268
+ ## 12. Low — Hand-rolled SIWE parser diverges from EIP-4361 ABNF
269
+
270
+ **File:** `src/siwe/index.ts` — `parseSiweMessage` / `validateSiweMessage`
271
+
272
+ The parser splits on lines and pattern-matches prefixes rather than following the EIP-4361 ABNF.
273
+ Edge cases (statements containing lines that begin with field prefixes, missing/optional-field
274
+ ordering, CRLF vs LF, resources block termination) can be mis-parsed. SIWE messages are a security
275
+ boundary for authentication; parser ambiguity can enable confusion between what a user signed and
276
+ what a server validates. `validateSiweMessage` also does not verify the signer address against the
277
+ recovered signature (out of this function's scope, but worth confirming the caller does).
278
+
279
+ **Recommendation:** Use a maintained SIWE library (e.g. the `siwe` package or viem's helpers), or
280
+ add a conformance test corpus. Ensure callers always verify the signature recovers to `address`.
281
+
282
+ ---
283
+
284
+ ## 13. Low — Device fingerprint and platform detection are spoofable
285
+
286
+ **Files:** `src/backup/encryption.ts` (`getDeviceFingerprint`), `src/sync/*` (UA sniffing)
287
+
288
+ `getDeviceFingerprint` is already documented as non-cryptographic; confirmed it must never enter key
289
+ material (it currently does not). Sync-method/platform detection relies on `navigator.userAgent`
290
+ substring checks, which are trivially spoofable and brittle. These are used only for UX routing, so
291
+ impact is low, but they should not gate any security decision.
292
+
293
+ **Recommendation:** Keep fingerprint strictly out of key derivation; treat platform detection as a
294
+ non-authoritative hint.
295
+
296
+ ---
297
+
298
+ ## 14. Info — Deprecated v2 KDF still present
299
+
300
+ **File:** `src/wallet/crypto.ts` — `deriveEncryptionKey`
301
+
302
+ The `@deprecated` v2 derivation remains exported and shares the same structural flaws as #1/#3
303
+ (public inputs + constant salt). Dead crypto paths are a foot-gun.
304
+
305
+ **Recommendation:** Remove once migration is complete, or clearly fence it behind a one-way
306
+ migration routine.
307
+
308
+ ---
309
+
310
+ ## 15. Info — Diagnostic logging
311
+
312
+ Several modules use `console.error` / `console.warn` (e.g. `device-manager.ts`, `social.ts`,
313
+ sync paths). Confirm no secret material (mnemonic, private key, share data, credential secrets) is
314
+ ever logged, including in error objects passed to the custom `*Error` wrappers.
315
+
316
+ ---
317
+
318
+ ## Overall assessment
319
+
320
+ The architecture (WebAuthn-gated wallet, password-encrypted backup files, Shamir social recovery,
321
+ ERC-5564 stealth, origin-scoped derivation) is reasonable, and the **password-based** backup
322
+ encryption is implemented to a good standard (random salt, AES-256-GCM, 310k PBKDF2 iterations).
323
+
324
+ However, the **WebAuthn-protected** confidentiality story is fundamentally broken (#1, #3, #9): the
325
+ key is derived from public values, so any party holding the stored ciphertext can decrypt it. There
326
+ is also at least one **correctness bug in social recovery** (#2) that can prevent legitimate
327
+ recovery, and the **hand-rolled cryptography** in stealth addresses (#4) and Shamir (#5/#6) needs
328
+ either replacement with vetted libraries or exhaustive test coverage before it can be trusted with
329
+ funds.
330
+
331
+ **Recommendation:** Treat findings #1, #2, #3, and #4 as release blockers. After remediation, this
332
+ code should still undergo a full third-party audit (with fuzzing and known-answer vectors for all
333
+ crypto primitives) before being used to store meaningful value.