8.1 KiB
E2EE Implementation Complete Guide
Overview
This document describes the complete end-to-end encryption (E2EE) implementation for Sojorn, including all issues encountered and fixes applied.
Architecture
- Flutter Client: Uses X25519 for key exchange, Ed25519 for signatures, AES-GCM for encryption
- Go Backend: Stores key bundles in PostgreSQL, serves encryption keys
- Protocol: X3DH (Extended Triple Diffie-Hellman) for key agreement
Key Components
1. Key Storage
- FlutterSecureStorage: Local key persistence with
e2ee_keys_v3key - PostgreSQL Tables:
profiles,signed_prekeys,one_time_prekeys - Key Format: Identity keys stored as
Ed25519:X25519(base64 concatenated with colon)
2. Key Generation Flow
- Generate Ed25519 signing key pair (for signatures)
- Generate X25519 identity key pair (for DH)
- Generate X25519 signed prekey with Ed25519 signature
- Generate 20 X25519 one-time prekeys (OTKs)
- Upload key bundle to backend
3. Message Encryption Flow
- Fetch recipient's key bundle from backend
- Verify signed prekey signature with Ed25519
- Perform X3DH key agreement
- Derive shared secret using KDF (SHA-256)
- Encrypt message with AES-GCM
- Delete used OTK from server
Issues Encountered & Fixes
Issue #1: 208-bit Key Bug ❌→✅
Problem: Keys were 26 characters (208 bits) instead of 32 bytes (256 bits)
Root Cause: Using string-based KDF instead of proper byte-based KDF
Fix: Updated _kdf method to use SHA-256 on byte arrays
Files Modified: simple_e2ee_service.dart
Issue #2: Database Constraint Error ❌→✅
Problem: SQLSTATE 42P10 - ON CONFLICT constraint mismatch
Root Cause: Go code used ON CONFLICT (user_id) but DB had PRIMARY KEY (user_id, key_id)
Fix: Updated Go code to use correct constraint ON CONFLICT (user_id, key_id)
Files Modified: user_repository.go
Issue #3: Fake Zero Signatures ❌→✅
Problem: SPK signatures were all zeros (AAAAAAAA...)
Root Cause: Manual upload used fake signature for testing
Fix: Updated manual upload to generate real Ed25519 signatures
Files Modified: simple_e2ee_service.dart
Issue #4: Asymmetric Security ❌→✅
Problem: One user skipped signature verification (legacy), other enforced it
Root Cause: Legacy user detection created security asymmetry
Fix: Removed legacy logic, enforced signature verification for all users
Files Modified: simple_e2ee_service.dart
Issue #5: Key Upload Not Automatic ❌→✅
Problem: Keys loaded locally but never uploaded to backend
Root Cause: _doInitialize returned early after loading keys
Fix: Added backend existence check and automatic upload
Files Modified: simple_e2ee_service.dart
Issue #6: NULL Database Values ❌→✅
Problem: registration_id was NULL causing scan errors
Root Cause: Database column allowed NULL values
Fix: Updated Go code to handle sql.NullInt64 with default values
Files Modified: user_repository.go
Issue #7: Noisy WebSocket Logs ❌→✅
Problem: Ping/pong messages cluttered console
Root Cause: WebSocket heartbeat logging
Fix: Filtered out ping/pong messages completely
Files Modified: secure_chat_service.dart
Issue #8: Modal Header Override ❌→✅
Problem: AppBar changes in chat screen were hidden by modal wrapper
Root Cause: SecureChatModal had custom header overriding SecureChatScreen AppBar
Fix: Added upload button to modal header instead
Files Modified: secure_chat_modal_sheet.dart
Current Status ✅
Working Components
- ✅ 32-byte key generation
- ✅ Valid Ed25519 signatures
- ✅ Signature verification
- ✅ Key bundle upload/download
- ✅ X3DH key agreement
- ✅ AES-GCM encryption/decryption
- ✅ OTK management (generation, usage, deletion)
- ✅ Backend key storage/retrieval
- ✅ Cross-platform encryption (Android↔Web)
Key Files Modified
Flutter:
- lib/services/simple_e2ee_service.dart (core E2EE logic)
- lib/services/secure_chat_service.dart (WebSocket + key management)
- lib/screens/secure_chat/secure_chat_modal_sheet.dart (UI upload button)
Go Backend:
- internal/handlers/key_handler.go (API endpoints + validation)
- internal/repository/user_repository.go (database operations)
Database Schema
-- Key storage tables
profiles (identity_key, registration_id)
signed_prekeys (user_id, key_id, public_key, signature)
one_time_prekeys (user_id, key_id, public_key)
Testing Checklist
Before Testing
- Ensure both users have valid keys (check
[E2EE] Keys exist on backend - ready) - Verify signatures are non-zero (check backend logs)
- Confirm OTKs are available (should have 20 OTKs each)
Test Flow
- Key Upload: Tap "🔑" button → should see
[E2EE] Key bundle uploaded successfully - Message Send: Type message → should see
[E2EE] SPK signature verified successfully - Message Receive: Should see
[DECRYPT] SUCCESS: Decrypted message: "..." - OTK Deletion: Should see
[E2EE] Deleted used OTK #[id] from server
Expected Logs
Sender:
[ENCRYPT] Fetching key bundle for recipient: [...]
[E2EE] SPK signature verified successfully.
[E2EE] Deleted used OTK #[id] from server
Receiver:
[DECRYPT] Used OTK with key_id: [id]
[DECRYPT] SUCCESS: Decrypted message: "[message_text]"
Next Steps: Message Recovery
Problem
When users uninstall the app or lose local keys, they cannot decrypt historical messages.
Solution Requirements
- Key Backup Strategy: Securely backup encryption keys
- Message Recovery: Allow decryption of historical messages after key recovery
- Security: Maintain E2EE guarantees while enabling recovery
Proposed Solutions
Option 1: Cloud Key Backup
- Encrypt identity keys with user password
- Store encrypted backup in cloud storage
- Recover keys with password authentication
Option 2: Social Recovery
- Allow trusted contacts to help recover keys
- Use Shamir's Secret Sharing for security
- Requires multiple trusted contacts
Option 3: Server-Side Recovery (Limited)
- Store encrypted key backups on server
- Server cannot decrypt without user password
- Similar to Signal's approach
Option 4: Message Re-encryption
- Store messages encrypted with server keys
- Re-encrypt with new keys after recovery
- Breaks perfect forward secrecy
Recommended Approach
Start with Option 1 (Cloud Key Backup) as it's:
- Most user-friendly
- Maintains security (password-protected)
- Technically straightforward
- Reversible if needed
Implementation Plan for Key Recovery
Phase 1: Key Backup
- Add password-based key encryption
- Implement cloud backup storage
- Add backup/restore UI
- Test backup/restore flow
Phase 2: Message Recovery
- Store message headers for re-decryption
- Implement batch message re-decryption
- Add recovery progress indicators
- Test with historical messages
Phase 3: Security Enhancements
- Add backup encryption verification
- Implement backup rotation
- Add recovery security checks
- Monitor recovery success rates
Security Considerations
Current Security Model
- ✅ Perfect Forward Secrecy (PFS) via OTKs
- ✅ Post-Compromise Security via key rotation
- ✅ Authentication via Ed25519 signatures
- ✅ Confidentiality via AES-GCM
Recovery Security Impact
- ⚠️ Breaks PFS for recovered messages
- ✅ Maintains confidentiality with password protection
- ✅ Preserves authentication via signature verification
- ⚠️ Requires trust in backup storage
Mitigation Strategies
- Use strong password requirements
- Implement backup encryption verification
- Add backup expiration policies
- Monitor backup access patterns
Conclusion
The E2EE implementation is now fully functional with all major issues resolved. The system provides:
- Strong cryptographic guarantees
- Cross-platform compatibility
- Automatic key management
- Secure message transmission
The next phase focuses on key recovery to handle user device changes while maintaining security principles.
Last Updated: January 29, 2026 Status: ✅ Production Ready (except key recovery)