epistery 1.4.4 → 1.5.0
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 +78 -118
- package/artifacts/build-info/38fa5ceb2313552f45736ffa98317780.json +1 -0
- package/artifacts/contracts/agent.sol/Agent.dbg.json +1 -1
- package/artifacts/contracts/agent.sol/Agent.json +69 -2
- package/client/wallet.js +315 -0
- package/contracts/agent.sol +40 -1
- package/dist/chains/EthereumChain.js +2 -2
- package/dist/chains/EthereumChain.js.map +1 -1
- package/dist/chains/PolygonChain.js +1 -1
- package/dist/chains/PolygonChain.js.map +1 -1
- package/dist/utils/Utils.d.ts +15 -0
- package/dist/utils/Utils.d.ts.map +1 -1
- package/dist/utils/Utils.js +47 -0
- package/dist/utils/Utils.js.map +1 -1
- package/dist/utils/types.d.ts +2 -1
- package/dist/utils/types.d.ts.map +1 -1
- package/dist/utils/types.js.map +1 -1
- package/docs/IdentityNaming.md +142 -0
- package/img.png +0 -0
- package/img_1.png +0 -0
- package/img_2.png +0 -0
- package/img_3.png +0 -0
- package/index.mjs +96 -1
- package/package.json +1 -1
- package/routes/connect.mjs +8 -1
- package/routes/fido.mjs +118 -0
- package/routes/index.mjs +4 -0
- package/routes/whitelist/index.mjs +82 -9
- package/src/chains/EthereumChain.ts +2 -2
- package/src/chains/PolygonChain.ts +1 -1
- package/src/utils/Utils.ts +61 -0
- package/src/utils/types.ts +6 -2
- package/artifacts/build-info/60715e1c329fcc4dc4bf715978075cf3.json +0 -1
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
|
|
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
|
-
##
|
|
53
|
+
## Epistery's Response: PRF-Wraps-Rivet
|
|
80
54
|
|
|
81
|
-
|
|
55
|
+
Rather than fight the platform, use its tools — but do not surrender the threat model.
|
|
82
56
|
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
### Components
|
|
92
62
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
###
|
|
71
|
+
### Registration Ceremony
|
|
99
72
|
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
### Unlock Ceremony (Including After iOS Purge)
|
|
108
84
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
94
|
+
### Threat Model
|
|
114
95
|
|
|
115
|
-
|
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
136
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
## UX Comparison
|
|
156
121
|
|
|
157
|
-
|
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
Using FIDO means accepting Apple/Google's infrastructure for credential persistence. The architecture limits the dependency:
|
|
170
135
|
|
|
171
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
147
|
+
This is factual, documentable, and inverts Apple's privacy narrative — while making the right technical choice.
|
|
189
148
|
|
|
190
|
-
|
|
149
|
+
## The Multi-Domain Advantage
|
|
191
150
|
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
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. **
|
|
206
|
-
2. **
|
|
207
|
-
3. **
|
|
208
|
-
4. **Build the coalition
|
|
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,
|
|
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`
|