epistery 1.4.5 → 1.5.1

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/MobileIdentity.md CHANGED
@@ -6,32 +6,6 @@ Safari's Intelligent Tracking Prevention (ITP) deletes localStorage and IndexedD
6
6
 
7
7
  Epistery uses non-extractable CryptoKeys stored in IndexedDB to create device-locked wallets ("rivets"). These keys never leave the device and cannot be extracted even via XSS attacks. On iOS Safari, Apple purges these keys after 7 days of inactivity.
8
8
 
9
- ## The Security Tradeoff
10
-
11
- Because Apple will not allow persistent secure storage, iOS users cannot benefit from the same security model as Android and desktop users.
12
-
13
- | Platform | Key Storage | Security Property |
14
- |----------|-------------|-------------------|
15
- | Android/Desktop | Non-extractable CryptoKey in IndexedDB | Private key **never** leaves device, immune to XSS extraction |
16
- | iOS Safari | Extractable key, server-escrowed | Key exists on server (encrypted), theoretically extractable |
17
-
18
- This is not a design choice. It is a forced degradation. To maintain continuity for iOS users, Epistery must backup keys to the server, fundamentally weakening the security model.
19
-
20
- **Implementation consequence:**
21
-
22
- ```javascript
23
- const isIOSSafari = /iPad|iPhone|iPod/.test(navigator.userAgent) &&
24
- !window.MSStream &&
25
- /Safari/.test(navigator.userAgent);
26
-
27
- if (isIOSSafari) {
28
- // BrowserWallet with server escrow - extractable, less secure
29
- // Because Apple won't let us keep non-extractable keys
30
- } else {
31
- // RivetWallet - non-extractable, device-locked, actually private
32
- }
33
- ```
34
-
35
9
  ## The FIDO/Passkey Double Standard
36
10
 
37
11
  Apple, Google, and Microsoft control the FIDO Alliance, which develops the WebAuthn/Passkey standards. These passkeys receive special treatment that third-party cryptographic keys do not.
@@ -74,140 +48,125 @@ These companies "led development of this expanded set of capabilities and are no
74
48
 
75
49
  The technical difference? Passkeys live in Apple-controlled infrastructure. Developer-created keys do not.
76
50
 
77
- This is not a privacy measure. It is a competitive moat. The W3C WebAuthn working group has acknowledged this tension - see [Issue #1569: Prevent browsers from deleting credentials that the RP wanted to be server-side](https://github.com/w3c/webauthn/issues/1569).
51
+ This is not a privacy measure. It is a competitive moat. The W3C WebAuthn working group has acknowledged this tension see [Issue #1569: Prevent browsers from deleting credentials that the RP wanted to be server-side](https://github.com/w3c/webauthn/issues/1569).
78
52
 
79
- ## Strategic Implications for Epistery
53
+ ## Epistery's Response: PRF-Wraps-Rivet
80
54
 
81
- ### The Messaging Opportunity
55
+ Rather than fight the platform, use its tools — but do not surrender the threat model.
82
56
 
83
- Apple's "privacy" policies force less private implementations:
84
-
85
- > "On Android and desktop, your Epistery wallet keys never leave your device. On iOS, Apple's storage policies require server backup, making your keys objectively less secure. Apple claims this is for privacy while their own passkey credentials - also device-bound keys - are exempt and sync through iCloud. The difference is control: they purge your secure keys while preserving theirs."
86
-
87
- This is factual, documentable, and inverts Apple's privacy narrative.
57
+ The WebAuthn **PRF extension** lets a relying party ask a passkey to evaluate a pseudo-random function over a fixed input, producing deterministic output bytes that never leave the device. We use those bytes to derive an AES key that wraps the user's secp256k1 rivet private key. The encrypted blob is stored on the epistery server, indexed by the FIDO credential ID and domain. On unlock, the device performs the FIDO ceremony, the PRF output decrypts the blob in memory, and the rivet signs as usual.
88
58
 
89
- ### The Multi-Domain Advantage
59
+ This pattern is already in production in the ReefRootz reference implementation (`skswave/reefrootz`), where it solves the iOS purge problem for an existing user base.
90
60
 
91
- Epistery's architecture provides some resilience:
61
+ ### Components
92
62
 
93
- - Each publisher domain runs its own Epistery instance (first-party context)
94
- - Chain verification is the shared layer, not browser storage
95
- - There is no single `epistery.com` domain to classify as a tracker
96
- - ITP's cross-site tracking heuristics don't easily apply
63
+ | Component | Role | Where it lives |
64
+ |-----------|------|---------------|
65
+ | FIDO credential | Persistent unlock factor | Secure Enclave + iCloud Keychain (OS-managed, ITP-exempt) |
66
+ | PRF output | AES-256 key material | Computed on device per ceremony; never persisted, never transmitted |
67
+ | secp256k1 rivet keypair | Ethereum-compatible signing key | Generated at registration; private key encrypted before any storage |
68
+ | Encrypted blob | AES-GCM(rivet private key, PRF-derived key) | Epistery server, keyed by `(domain, credentialId)` |
69
+ | Whitelist entry | Public identity | `WhitelistEntry { addr, name, role, meta }` in the domain's agent contract |
97
70
 
98
- ### Recovery Mechanisms
71
+ ### Registration Ceremony
99
72
 
100
- The Identity Contract provides a path forward:
73
+ 1. User triggers "Secure this device" on a domain.
74
+ 2. Browser invokes `navigator.credentials.create()` with the PRF extension; user does Face ID / Touch ID.
75
+ 3. Client derives the AES-256 key from a domain-constant PRF eval input.
76
+ 4. Client generates a fresh secp256k1 keypair (the rivet).
77
+ 5. Client AES-GCM-encrypts the rivet private key with the PRF-derived key.
78
+ 6. Client posts `{ credentialId, publicKey, rivetAddress, encryptedBlob }` to the epistery server; server stores the blob.
79
+ 7. The new rivet address is added to a whitelist in the domain's agent contract under the user's existing name (admin-mediated or via auto-approval, depending on domain policy).
101
80
 
102
- 1. **First device/site**: Creates invisible rivet, registers on-chain
103
- 2. **Additional devices**: Authorized by existing device via multi-sig
104
- 3. **Recovery after iOS purge (with other devices live)**: Invisible re-authorization
105
- 4. **Recovery with no devices live**: One-time re-affirmation required
81
+ The rivet private key only exists in JS memory during the ceremony and is discarded after the blob is uploaded.
106
82
 
107
- Server-set httpOnly cookies (not subject to the same ITP rules as script-writable storage) can bridge the gap by maintaining an encrypted pointer to the user's chain identity.
83
+ ### Unlock Ceremony (Including After iOS Purge)
108
84
 
109
- ## FidoWallet Integration
85
+ 1. User visits a domain. IndexedDB may have been purged; the session cookie or a credential lookup identifies which FIDO credential to use.
86
+ 2. Browser invokes `navigator.credentials.get()` with the same PRF eval input.
87
+ 3. Client recovers the AES-256 key on device.
88
+ 4. Client fetches the encrypted blob from the epistery server.
89
+ 5. Client AES-GCM-decrypts the blob in memory to recover the secp256k1 private key.
90
+ 6. Rivet signs the session challenge; key exchange completes as for any other wallet.
110
91
 
111
- Rather than fight the platform, use its tools. FIDO/WebAuthn credentials persist because Apple blesses them. By integrating FidoWallet as a third wallet type alongside RivetWallet and Web3Wallet, Epistery can leverage platform-blessed persistence while maintaining the on-chain identity as the sovereign anchor.
92
+ After the request completes, the private key is dropped. The next session repeats the ceremony, transparent to the user behind a single biometric touch.
112
93
 
113
- ### Wallet Type Comparison
94
+ ### Threat Model
114
95
 
115
- | Wallet Type | Key Location | Persistence | User Experience | iOS Safari |
116
- |-------------|--------------|-------------|-----------------|------------|
117
- | **RivetWallet** | IndexedDB (non-extractable) | Fragile (ITP purge) | Invisible | Unreliable |
118
- | **Web3Wallet** | MetaMask / external | Persistent | Popup on connect + each sign | Works |
119
- | **FidoWallet** | Secure Enclave | Persistent | One biometric touch | **Reliable** |
96
+ | Threat | Mitigation |
97
+ |--------|------------|
98
+ | iOS ITP purges IndexedDB | The encrypted blob lives on the epistery server; the decryption key derives from a credential outside ITP scope |
99
+ | Server compromise | The blob is AES-GCM-encrypted with a key the server never sees and cannot derive; without the user's biometric, the blob is inert ciphertext |
100
+ | XSS exfiltrates the rivet private key | The key exists in JS memory only during the unlock window. Larger surface than a non-extractable IndexedDB key, but only at the moment of use |
101
+ | Lost device | Same as Tier 1 today — another device whitelisted under the same name retains access; admin can re-issue or whitelist a fresh credential |
120
102
 
121
- ### User Experience Flow
103
+ This sits between the two extremes the previous design framed: stronger than full server-escrow (the server has the blob but not the key), weaker than non-extractable IndexedDB on a device ITP doesn't touch. It is the right tradeoff for iOS specifically, and acceptable as a uniform pattern across platforms for implementation simplicity.
122
104
 
123
- **First visit (registration):**
124
- 1. User triggers "Secure this device" (prompted or via status page)
125
- 2. Single biometric touch (Face ID / Touch ID / fingerprint)
126
- 3. WebAuthn credential created, stored in Secure Enclave
127
- 4. Epistery stores credential ID + public key server-side, tied to session or chain identity
105
+ ## Tier 1 Identity Integration
128
106
 
129
- **Return visit (authentication):**
130
- 1. Server sees session cookie, knows user has FIDO credential
131
- 2. Requests WebAuthn assertion
132
- 3. Single biometric touch
133
- 4. Session re-established
107
+ Epistery has two identity tiers, and FIDO fits the casual (default) tier:
134
108
 
135
- **Recovery after iOS purge:**
136
- - Cookie survives (server-set httpOnly, not subject to ITP)
137
- - One touch to re-authenticate
138
- - New rivet created and linked to existing Identity Contract
139
- - Chain state intact, local key reconstructed
109
+ - **Tier 1 Domain-scoped named whitelist.** The user's name is recorded in `WhitelistEntry.name` on the domain's agent contract (`contracts/agent.sol`). Multiple device addresses can share the same name; each is a "device of the same person" to the domain. A FIDO-bound rivet on the user's phone is just another whitelisted device under the same name.
110
+ - **Tier 2 — Identity Contract.** Per-user contract with `authorizedRivets[]` and multi-sig recovery. Gas-costly. Use when sovereign portability across domains matters.
140
111
 
141
- ### UX Comparison by Scenario
112
+ A FIDO rivet **does not require an Identity Contract**. The casual tier handles the common case: the user has a phone (FIDO-protected rivet) and a desktop (RivetWallet rivet), both whitelisted under the same name on the domains they use. The address-to-name resolver in the auth middleware surfaces the same name for both addresses, so the domain treats them as one person.
142
113
 
143
- | Scenario | RivetWallet | Web3Wallet | FidoWallet |
144
- |----------|-------------|------------|------------|
145
- | First visit | Invisible | Popup + approve | One touch |
146
- | Return (storage intact) | Invisible | Reconnect popup | Invisible (cookie) |
147
- | Return (storage purged) | **Broken** | Reconnect popup | One touch |
148
- | Sign operation | Invisible | Popup each time | Invisible (rivet signs) |
149
- | iOS Safari | Unreliable | Works (external app) | **Reliable** |
114
+ ## The Curve Problem (Resolved)
150
115
 
151
- FidoWallet is less intrusive than Web3Wallet while providing the persistence that RivetWallet lacks on iOS.
116
+ FIDO uses P-256 (secp256r1). Ethereum uses secp256k1. Earlier iterations of this design proposed using the FIDO credential directly for chain signing, which would have required ERC-4337 account abstraction or P-256 verifier contracts.
152
117
 
153
- ### The Curve Problem
118
+ PRF-wraps-rivet sidesteps the problem: PRF derives an AES-256 key (curve-agnostic), and the rivet that signs on-chain is a freshly generated secp256k1 keypair. FIDO never signs chain transactions; it only protects the AES wrap. The curve mismatch dissolves because the FIDO credential and the rivet are decoupled.
154
119
 
155
- FIDO uses P-256 (secp256r1). Ethereum uses secp256k1. Direct on-chain signature verification requires reconciliation:
120
+ ## UX Comparison
156
121
 
157
- | Approach | Tradeoff |
158
- |----------|----------|
159
- | **Account abstraction (ERC-4337)** | Smart contract wallets can verify P-256, adds complexity |
160
- | **FIDO as auth, rivet for signing** | FIDO proves identity, authorizes secp256k1 rivet for chain ops |
161
- | **PRF-derived key** | Derive secp256k1 from passkey, but extractable |
122
+ | Scenario | RivetWallet (IndexedDB) | Web3Wallet (MetaMask) | FidoWallet (PRF-wraps-rivet) |
123
+ |----------|------|------|------|
124
+ | First visit | Invisible | Popup + approve | One biometric touch |
125
+ | Return visit (storage intact) | Invisible | Reconnect popup | One touch per session |
126
+ | Return visit (iOS purged storage) | **Broken** | Reconnect popup | One touch (fetches blob) |
127
+ | Sign operation | Invisible | Popup each time | Invisible after unlock (rivet signs) |
128
+ | iOS Safari reliability | Unreliable | Works (external app) | **Reliable** |
162
129
 
163
- **Recommended architecture:** FIDO as the persistence/recovery anchor, rivet as the signing key.
130
+ FidoWallet adds a single biometric gesture per session in exchange for surviving ITP purges and avoiding MetaMask's per-signature popups.
164
131
 
165
- This mirrors the Web3Wallet model:
166
- - MetaMask holds keys externally, Epistery requests signatures
167
- - FIDO holds credential externally, Epistery uses it to authorize rivets
132
+ ## Strategic Position
168
133
 
169
- The FIDO credential proves "I am this user" to the server. The server trusts the FIDO-authenticated session to authorize rivet keys. Those rivets do the actual chain signing.
134
+ Using FIDO means accepting Apple/Google's infrastructure for credential persistence. The architecture limits the dependency:
170
135
 
171
- ### Server Configuration
136
+ 1. **The FIDO server is the epistery host** — no platform servers in the auth path; the relying party is the domain itself.
137
+ 2. **The on-chain identity remains sovereign** — whitelist entries and (optionally) Identity Contracts are the anchor; FIDO is a local unlock factor.
138
+ 3. **The PRF output never leaves the device** — Apple's iCloud sync handles the credential, but the AES key the credential produces is computed locally each ceremony.
139
+ 4. **Users can extricate further** — Tier 2 graduation, hardware tokens, or pure RivetWallet on non-iOS devices remain available.
172
140
 
173
- The epistery host already decides which wallet types to accept. FidoWallet becomes a third option:
141
+ ## The Messaging Opportunity
174
142
 
175
- ```
176
- Wallet types accepted by publisher:
177
- ├── RivetWallet (device-locked, invisible, iOS-fragile)
178
- ├── Web3Wallet (external, persistent, popup-heavy)
179
- └── FidoWallet (platform-blessed, one-touch, persistent)
180
- ```
181
-
182
- Server config example: `acceptWallets: ['rivet', 'fido']` or `['fido', 'web3']`
183
-
184
- ### Status Page Integration
143
+ Apple's "privacy" policies force less private implementations:
185
144
 
186
- The status page at `/.well-known/epistery/status` already allows users to manage wallet selection. FidoWallet credentials would appear alongside existing options, letting power users manage their constellation of keys across the Identity Contract.
145
+ > "On Android and desktop, your Epistery wallet keys never leave your device. On iOS, Apple purges those keys after 7 days while their own passkey credentials also device-bound keys are preserved indefinitely. We bridge the gap by using their passkeys to protect ours: the AES key that unlocks your wallet is computed by your phone's biometric and never sent to any server, including ours. Apple's purge becomes an inconvenience instead of a kill switch."
187
146
 
188
- ### Strategic Position
147
+ This is factual, documentable, and inverts Apple's privacy narrative — while making the right technical choice.
189
148
 
190
- Using FIDO means accepting Apple/Google's infrastructure for credential persistence. However:
149
+ ## The Multi-Domain Advantage
191
150
 
192
- 1. **The FIDO server is independent** - Epistery runs its own relying party, no platform servers in the auth flow
193
- 2. **The Identity Contract remains sovereign** - On-chain identity is the anchor, FIDO is a convenience bridge
194
- 3. **Users can extricate further** - Add more devices, hardware keys, full self-custody at their own pace
195
- 4. **Playing by their rules** - FIDO is an open standard; using it is legitimate, not an exploit
151
+ Epistery's architecture provides natural resilience:
196
152
 
197
- The gesture is heavier than invisible rivet creation, but lighter than MetaMask, and it survives the iOS purge.
153
+ - Each publisher domain runs its own Epistery instance (first-party context).
154
+ - Chain verification is the shared layer, not browser storage.
155
+ - There is no single `epistery.com` domain to classify as a tracker.
156
+ - ITP's cross-site tracking heuristics don't easily apply.
198
157
 
199
158
  ## Conclusion
200
159
 
201
- The mobile identity landscape is shaped by platform vendors who have constructed privacy policies that handicap alternatives while exempting their own infrastructure. iOS Safari's localStorage purging is not a neutral privacy measure - it is a selective restriction that degrades third-party security while preserving first-party (Apple-controlled) credential persistence.
160
+ The mobile identity landscape is shaped by platform vendors who construct privacy policies that handicap alternatives while exempting their own infrastructure. iOS Safari's localStorage purging is not a neutral privacy measure it is a selective restriction that degrades third-party security while preserving first-party (Apple-controlled) credential persistence.
202
161
 
203
162
  Epistery's response:
204
163
 
205
- 1. **Document the tradeoff** - iOS users get weaker security, and they should know why
206
- 2. **Implement graceful degradation** - Server escrow for iOS, full device-lock for others
207
- 3. **Leverage on-chain identity** - The chain is the persistent layer; local storage is reconstructible
208
- 4. **Build the coalition** - Every publisher using Epistery has incentive to amplify this message
164
+ 1. **Use the blessed credential as an unlock factor, not the signing key.** PRF-wraps-rivet preserves Ethereum-compatibility and on-chain sovereignty.
165
+ 2. **Store the encrypted blob, not the key.** The server holds ciphertext; the decryption material lives in the user's Secure Enclave.
166
+ 3. **Lean on Tier 1 identity.** Most users don't need an Identity Contract; the domain whitelist with stable names already supports multi-device.
167
+ 4. **Build the coalition.** Every publisher using Epistery has incentive to amplify this messaging — the technical case and the political case align.
209
168
 
210
- The monopolies assume identity lives in their silos. Epistery puts identity on-chain, making their storage sabotage an inconvenience rather than a kill switch.
169
+ The monopolies assume identity lives in their silos. Epistery puts identity on-chain, makes the local unlock factor opaque to them, and turns their storage sabotage into a routine biometric touch.
211
170
 
212
171
  ---
213
172
 
@@ -220,3 +179,4 @@ The monopolies assume identity lives in their silos. Epistery puts identity on-c
220
179
  - [Corbado: Passkeys & WebAuthn PRF for E2E Encryption](https://www.corbado.com/blog/passkeys-prf-webauthn)
221
180
  - [Didomi: Apple 7-Day Cap on Script-Writable Storage](https://support.didomi.io/apple-adds-a-7-day-cap-on-all-script-writable-storage)
222
181
  - [Safari ITP Current Status - cookiestatus.com](https://www.cookiestatus.com/safari/)
182
+ - ReefRootz reference implementation: `skswave/reefrootz`