Skip to content

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 chunks

App Secret Derivation

// Pseudocode for key derivation
appSecret = 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.net embedded in app-a.com has different storage than the same iframe embedded in app-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

MessagePurpose
parentIdentifyIdentify parent to iframe
checkAuthCheck authentication status
requestAuthRequest authentication (open popup)
uploadDataUpload data to Swarm
downloadDataDownload data from Swarm

Iframe → Parent Messages

MessagePurpose
proxyReadyIframe is initialized
authStatusResponseAuthentication status
authSuccessAuthentication completed
uploadDataResponseUpload result
downloadDataResponseDownload result
errorError occurred
MessagePurpose
setSecretSend 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:

  1. Plaintext header — account metadata visible without decryption (account name, export timestamp, account type, and type-specific fields)
  2. Encrypted payload — AES-GCM-256 encrypted AccountStateSnapshot containing identities, connected apps, and postage stamps

Header fields vary by account type:

Account TypeExtra Header Fields
PasskeycredentialId
EthereumethereumAddress
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:

  1. Serialize account state via serializeAccountStateSnapshot() (strips appSecret)
  2. Derive swarmEncryptionKey from derivationKey via HMAC-SHA256 with context "swarm-encryption"
  3. Derive AES-GCM-256 key: swarmEncryptionKey → HMAC-SHA256 with context "swarm-id-backup-encryption-v1"
  4. Encrypt with random 12-byte IV
  5. Build header with account metadata + ciphertext

Import Flow

Import requires authentication to re-derive the encryption key:

  1. Parse the plaintext header to identify account type and metadata
  2. User authenticates (passkey or wallet) to obtain masterKey
  3. Derive derivationKeyswarmEncryptionKeybackupKey → AES-GCM-256 key
  4. Decrypt and validate the payload via deserializeAccountStateSnapshot()

Swarm Restore Flow

When a passkey auth succeeds on a new device with no local account:

  1. Derive derivationKey from masterKey
  2. Derive swarmEncryptionKey from derivationKey
  3. Derive backupKey → epoch feed owner private key
  4. Build feed topic from account ID
  5. Look up latest epoch feed entry in Swarm
  6. 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 iframeOrigin matches your trusted domain
  • Keep the trusted domain secure and audited
  • Use Content Security Policy to restrict iframe embedding