Flamingo Sync implements a zero-knowledge, end-to-end encrypted synchronization system. The server is a trusted data store for encrypted blobs but is NEVER trusted with plaintext data or encryption keys.
┌─────────────────────────────────────────────────────────┐
│ CLIENT (Electron) │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ Master Key │ │ Key Encryption Key (KEK) │ │
│ │ (256-bit) │◄───│ Derived from password via │ │
│ │ AES-256-GCM │ │ PBKDF2 (600k iterations) │ │
│ └──────┬───────┘ └──────────────────────────────┘ │
│ │ │
│ │ Encrypts/Decrypts │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Sync Data (plaintext) │ │
│ │ • History • Environments • Secrets │ │
│ │ • Collections • Settings │ │
│ └────────────────────────────────────────────┘ │
│ │
│ Master Key is wrapped (encrypted) with KEK │
│ Only wrapped key is sent to server │
└──────────────────────┬──────────────────────────────────┘
│ Encrypted blobs only
▼
┌─────────────────────────────────────────────────────────┐
│ SERVER (Supabase) │
│ │
│ ┌────────────────┐ ┌──────────────────────────────┐ │
│ │ Encrypted │ │ Wrapped Master Key │ │
│ │ Data Blobs │ │ (encrypted with KEK) │ │
│ │ (AES-256-GCM) │ │ + salt + nonce │ │
│ └────────────────┘ └──────────────────────────────┘ │
│ │
│ Server only sees: encrypted bytes, nonces, metadata │
│ Server NEVER sees: plaintext, master key, password │
└─────────────────────────────────────────────────────────┘
- Length: 256 bits (32 bytes)
- Algorithm: AES-256-GCM
- Storage:
- Local: IndexedDB / electron-store (encrypted at rest by OS)
- Server: NEVER in plaintext
- Purpose: Encrypts/decrypts all sync data
- Generation:
crypto.subtle.generateKey()on first sync setup
- Derivation: PBKDF2-SHA256
- Input: User password + random salt (32 bytes)
- Iterations: 600,000
- Storage: NOT stored. Derived fresh on each login.
- Purpose: Wraps/unwraps the Master Key for server storage
- Format:
AES-256-GCM(master_key, KEK, nonce) - Stored with: salt (for KEK derivation) + nonce (for AES-GCM)
- Safe to store on server since KEK requires the password
- All sync data is encrypted with AES-256-GCM before upload
- Master key is wrapped with a password-derived KEK
- Server never receives plaintext data
- Even with full database access, attackers cannot decrypt data without the password
- AES-256-GCM provides authenticated encryption (detects tampering)
- Optional SHA-256 checksums for blob integrity verification
- Version tracking prevents rollback attacks
- Supabase Auth for user authentication
- PBKDF2-derived KEK ensures only the password holder can unwrap the master key
- Session tokens stored as SHA-256 hashes (server never stores plaintext tokens)
- PBKDF2 with 600,000 iterations (OWASP 2023 recommended minimum: 600k)
- Each user gets a unique random salt
- Combined with Supabase's bcrypt password hashing (defense in depth)
- Attacker gets: encrypted blobs, wrapped master key, salts, nonces
- Attacker cannot decrypt without the password
- No password-equivalent data stored on server
- PBKDF2 iterations make brute-force attacks slow
- All API calls should use HTTPS (TLS 1.3)
- Supabase Auth uses PKCE flow
- Session tokens transmitted only over TLS
- Each encryption operation uses a unique nonce
- Duplicate nonce+encrypted_blob combinations are rejected
- Version counter prevents rollback of synced data
- Session tokens stored as SHA-256 hashes (server)
- Token rotation invalidates old tokens
- Token revocation kills all sessions for a device
- Device approval adds extra verification step
- PBKDF2 with 600k iterations (~600ms per attempt on modern hardware)
- Supabase rate-limiting on auth endpoints
- No plaintext-key-equivalent data to leak
- Log in to any device
- Download encrypted master key from server
- Derive KEK from password
- Unwrap master key
- Download and decrypt all data
- Outcome: Full recovery possible
- App can still decrypt local data (master key is stored locally)
- Sync to new devices is impossible (cannot derive KEK to unwrap server key)
- Outcome: Existing data accessible, sync broken
- Mitigation: Use a password manager; enable biometric unlock
- No way to decrypt server data
- Server's encrypted master key cannot be unwrapped without password
- Outcome: Permanent data loss. This is by design.
- Mitigation: Regular local backups; write down recovery codes
- Old master key becomes inaccessible (wrapped with old password's KEK)
- Must set up sync again (generates new master key)
- Previous encrypted data becomes orphaned on server
- Outcome: Data accessible on devices that still have the old master key locally
- Mitigation: Re-wrap master key with new KEK before password change
- Compromised client device: If an attacker has access to the unlocked device, they can read decrypted data and access the master key in local storage
- Keylogger/malware: Password or master key could be captured at input time
- Supabase Auth compromise: If Supabase Auth is compromised, an attacker could authenticate as a user but still cannot decrypt data without the password
- Future quantum computing: AES-256 is quantum-resistant for now, but PBKDF2 may need upgrading to Argon2
- PBKDF2 with 600k iterations adds ~500-800ms to login on modern hardware
- AES-256-GCM encryption/decryption is fast (< 50ms for typical sync payloads)
- Full sync of large histories may take several seconds (network + decryption)
- When key is rotated, all existing sessions get the new wrapped key
- Old encrypted data remains encrypted with the old key
- Full re-encryption of all data requires downloading, decrypting with old key, re-encrypting with new key, and re-uploading
- Until data is re-encrypted, old sessions with the old local key can still read it
- Argon2id: Replace PBKDF2 with Argon2id for better GPU/ASIC resistance
- Biometric Protection: Use OS biometric APIs to protect local master key
- Recovery Codes: Generate one-time recovery codes that can decrypt the master key
- Social Recovery: Split master key using Shamir's Secret Sharing
- Hardware-Backed Keys: Use TPM/secure enclave for master key storage
- End-to-End Verified: Add key fingerprint verification between devices
All encryption/decryption functions in src/lib/crypto.ts are for CLIENT-SIDE use.
They are included in the backend-server codebase for reference/documentation only.
The server imports none of these functions. The server only handles encrypted blobs.
The KEK wrapping scheme described above (PBKDF2-derived KEK wrapping the master key) is the target architecture.
The current implementation stores the master key as raw base64 in sync_sessions.encrypted_master_key, transmitted over TLS to the server. This means:
- A server compromise still cannot decrypt data blobs (AES-256-GCM key is required)
- The master key is encrypted in transit via TLS
- However, the server does hold the raw key bytes at rest
The KEK wrapping enhancement will add an additional layer of protection so the server never holds the raw key.
The server logs 8 event types to the audit_logs table:
| Event | When |
|---|---|
token_claimed |
Temp token claimed |
device_connected |
New device session created |
config_updated |
Sync preferences changed |
key_stored |
Master key stored or rotated |
token_rotated |
Session token rotated |
token_revoked |
Session revoked |
device_revoked |
Device removed |
data_cleared |
Sync data deleted |
Row-Level Security policies ensure users can only access their own data.
All queries include user_id = auth.uid() filters.
Service role key is used only for admin operations (never exposed to client).