Architecture
Swarm ID uses an OAuth-style popup authentication flow. Chrome and Firefox work out of the box; Safari works in download-only mode (auth works, uploads disabled due to ITP storage partitioning). See #167 for details.
System Overview
The architecture consists of three main components:
┌─────────────────────────────────────────────────────────────┐│ Your dApp (e.g., https://my-dapp.com) ││ ┌────────────────────────────────────────────────────┐ ││ │ SwarmIdClient │ ││ │ │ ││ │ ┌─────────────────────────────────────────────┐ │ ││ │ │ Hidden iframe (https://swarm-id.snaha.net) │ │ ││ │ │ │ │ ││ │ │ SwarmIdProxy │ │ ││ │ │ - Stores app-specific secret │ │ ││ │ │ - Proxies Bee API calls │ │ ││ │ └─────────────────────────────────────────────┘ │ ││ └────────────────────────────────────────────────────┘ ││ │ ││ │ Opens popup ││ ↓ ││ ┌────────────────────────────────────────────────────┐ ││ │ Auth Popup (https://swarm-id.snaha.net) │ ││ │ │ ││ │ Auth Popup (SvelteKit /connect route) │ ││ │ - Stores master key (first-party context) │ ││ │ - Derives app-specific secrets │ ││ │ - Sends secret to iframe via postMessage │ ││ └────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────┘Components
SwarmIdClient
The main library used by parent dApp windows. It:
- Creates and embeds a hidden iframe from the trusted domain
- Provides authentication UI (login button, status)
- Exposes methods for Bee operations (upload, download, etc.)
- Communicates with the iframe via postMessage
SwarmIdProxy
Runs inside the hidden iframe on the trusted domain. It:
- Receives app-specific secrets from the auth popup
- Stores secrets in partitioned localStorage
- Proxies Bee API calls with authentication
- Validates all incoming messages from the parent
Auth Popup
The authentication popup is implemented as a SvelteKit route (/connect) in the identity management UI. It:
- Manages the master key in first-party localStorage
- Derives app-specific secrets using HMAC-SHA256
- Sends secrets to the iframe via postMessage
- Provides the authentication UI (Passkey/WebAuthn and SIWE flows)
Key Hierarchy
All cryptographic keys are derived from a single master key produced during Passkey or SIWE authentication:
Master Key (from Passkey/SIWE challenge)├─> App-Specific Secret (HMAC-SHA256 with app origin)│ ├─> Low-stakes keys (feed, session) → shared with apps│ └─> High-stakes keys (stamps, ACT) → never shared├─> derivationKey (HMAC-SHA256 with "derivation-key", persisted in account)│ ├─> swarmEncryptionKey (HMAC-SHA256 with "swarm-encryption")│ │ ├─> backup AES key (HMAC-SHA256 with "swarm-id-backup-encryption-v1")│ │ │ └─> AES-GCM-256 CryptoKey (for .swarmid encryption)│ │ └─> backup feed key (HMAC-SHA256 with "backup-key")│ │ └─> backup private key (for Swarm epoch feed owner)│ └─> postageSignerKey (HMAC-SHA256 with "postage-signer" or "postage-signer:{identityId}")│ └─> PrivateKey for signing postage batch chunksApp Secret Derivation
// Pseudocode for key derivationappSecret = HMAC - SHA256(masterKey, appOrigin)This ensures:
- Deterministic - Same master key + app origin always produces the same secret
- Unique per app - Each app gets a different secret
- No cross-app leakage - Knowing one app’s secret doesn’t reveal others
- Master key protection - The master key never leaves the trusted domain
Storage Partitioning
Modern browsers partition storage by (iframe-origin, parent-origin). This means:
- An iframe from
swarm-id.snaha.netembedded inapp-a.comhas different storage than the same iframe embedded inapp-b.com - This provides browser-enforced isolation between apps
- Even if an app is compromised, it can’t access other apps’ secrets
Message Protocol
All cross-origin communication uses postMessage with Zod validation:
Parent → Iframe Messages
| Message | Purpose |
|---|---|
parentIdentify | Identify parent to iframe |
checkAuth | Check authentication status |
requestAuth | Request authentication (open popup) |
uploadData | Upload data to Swarm |
downloadData | Download data from Swarm |
Iframe → Parent Messages
| Message | Purpose |
|---|---|
proxyReady | Iframe is initialized |
authStatusResponse | Authentication status |
authSuccess | Authentication completed |
uploadDataResponse | Upload result |
downloadDataResponse | Download result |
error | Error occurred |
Popup → Iframe Messages
| Message | Purpose |
|---|---|
setSecret | Send derived app-specific secret |
Backup & Recovery
Swarm ID supports account backup via encrypted .swarmid files and automatic Swarm-based sync.
.swarmid File Format
Each .swarmid file is a JSON object with two parts:
- Plaintext header — account metadata visible without decryption (account name, export timestamp, account type, and type-specific fields)
- Encrypted payload — AES-GCM-256 encrypted
AccountStateSnapshotcontaining identities, connected apps, and postage stamps
Header fields vary by account type:
| Account Type | Extra Header Fields |
|---|---|
| Passkey | credentialId |
| Ethereum | ethereumAddress |
| Agent | (none) |
During Ethereum account import, the user must re-enter their secret seed. The master key is re-derived from secretSeed + publicKey — key material is never stored in the backup header.
Export Flow
Export requires no re-authentication — the derivationKey is already persisted in the account. The flow:
- Serialize account state via
serializeAccountStateSnapshot()(stripsappSecret) - Derive
swarmEncryptionKeyfromderivationKeyvia HMAC-SHA256 with context"swarm-encryption" - Derive AES-GCM-256 key:
swarmEncryptionKey→ HMAC-SHA256 with context"swarm-id-backup-encryption-v1" - Encrypt with random 12-byte IV
- Build header with account metadata + ciphertext
Import Flow
Import requires authentication to re-derive the encryption key:
- Parse the plaintext header to identify account type and metadata
- User authenticates (passkey or wallet) to obtain
masterKey - Derive
derivationKey→swarmEncryptionKey→backupKey→ AES-GCM-256 key - Decrypt and validate the payload via
deserializeAccountStateSnapshot()
Swarm Restore Flow
When a passkey auth succeeds on a new device with no local account:
- Derive
derivationKeyfrommasterKey - Derive
swarmEncryptionKeyfromderivationKey - Derive
backupKey→ epoch feed owner private key - Build feed topic from account ID
- Look up latest epoch feed entry in Swarm
- Download and decrypt the account snapshot
Security: appSecret Exclusion
The appSecret field is structurally excluded from ExportedConnectedAppSchemaV1. Even if raw data contains an appSecret, Zod strips it during parsing. App secrets are re-derived on-demand from the master key — they are never stored in backups.
Security Considerations
What’s Protected
- Master key - Only stored in first-party context on trusted domain
- App secrets - Derived per-app, stored in partitioned storage
- Cross-app isolation - Browser-enforced through storage partitioning
Attack Vectors Mitigated
- XSS in dApp - Can’t access master key (different origin)
- Malicious dApp - Only gets its own derived secret
- Man-in-the-middle - HTTPS required for all communication
- Message spoofing - All messages validated with Zod schemas
Recommendations
- Always use HTTPS in production
- Validate the
iframeOriginmatches your trusted domain - Keep the trusted domain secure and audited
- Use Content Security Policy to restrict iframe embedding