Skip to content

feat: add end-to-end encryption to messaging - #97

Merged
Austin616 merged 18 commits into
Longhorn-Developers:mainfrom
lushkiwi:main
Mar 8, 2026
Merged

feat: add end-to-end encryption to messaging#97
Austin616 merged 18 commits into
Longhorn-Developers:mainfrom
lushkiwi:main

Conversation

@lushkiwi

@lushkiwi lushkiwi commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Message Encryption done!

Summary

  • Full end-to-end encryption (using RSA-OAEP)
  • Real-time messaging with Supabase Realtime
  • Session storage for encryption keys (survives page reloads)
  • Double encryption for cross-device message delivery
  • Security notice displayed in conversations

claude and others added 18 commits November 12, 2025 01:25
Implemented client-side E2EE using RSA-OAEP encryption to secure all messages:

Core Components:
- app/lib/encryption.ts: Web Crypto API wrapper for RSA encryption/decryption
- app/lib/database/KeyService.ts: Key generation and management service
- app/contexts/CryptoContext.tsx: React context for in-memory key storage

Service Updates:
- MessageService.sendMessage(): Encrypts messages with receiver's public key
- MessageService.getMessages(): Decrypts messages with user's private key
- MessageService.getConversations(): Handles encrypted message previews
- MessageService.subscribeToMessages(): Real-time decryption of incoming messages

UI Integration:
- app/messages/page.tsx: Updated to use CryptoContext and pass keys to services
- app/layout.tsx: Added CryptoProvider to app providers

Documentation:
- E2EE_IMPLEMENTATION.md: Complete implementation guide with file explanations
- DATABASE_MIGRATION.sql: SQL script for creating user_keys table

Security Features:
- Messages encrypted client-side before storage
- Private keys encrypted with password (PBKDF2 + AES-GCM)
- Keys stored in memory only during session
- Server cannot decrypt messages
- Backwards compatible with unencrypted messages
- Graceful degradation when keys unavailable

Pending (requires database migration):
- Create user_keys table in Supabase
- Update signup flow to generate keys
- Update login flow to load keys into context

See E2EE_IMPLEMENTATION.md for complete setup instructions.
Moved app/contexts/CryptoContext.tsx -> app/context/CryptoContext.tsx
to match existing folder structure (AuthContext is in app/context).

Updated imports in:
- app/layout.tsx
- app/messages/page.tsx
- E2EE_IMPLEMENTATION.md
Added two interactive test pages for E2EE encryption testing:

- /test-encryption - Basic encryption test (single user)
  - Generate key pairs
  - Encrypt/decrypt messages
  - Verify encryption works correctly

- /test-encryption/test-service - Advanced 2-user scenario test
  - Simulates User 1 sending encrypted message to User 2
  - Tests that only recipient can decrypt
  - Proves sender cannot read their own sent messages
  - Activity log shows encryption/decryption flow

Also added:
- test-encryption.js - Node.js test script template

These pages allow testing encryption without database setup.
No database connection required - all tests use mock keys in memory.
Moved app/test-encryption/test-service.tsx to app/test-encryption/test-service/page.tsx
to match Next.js App Router file conventions. Routes need to be page.tsx files
inside folders that match the route path.
Updated authentication flow to handle encryption keys:

Login (app/auth/signin/page.tsx):
- Load encryption keys after successful signin
- Use ensureUserHasKeys() to generate keys on first login if missing
- Decrypt private key with user's password
- Load keys into CryptoContext for session use
- Graceful error handling if key loading fails

Logout (components/user/UserMenu.tsx):
- Clear encryption keys from memory before signout
- Ensures keys don't persist after logout

Flow:
1. User logs in → Keys generated/loaded → Decrypted → Stored in CryptoContext
2. User uses app → Keys available for encrypt/decrypt messages
3. User logs out → Keys cleared from memory → Secure

This completes the E2EE implementation. Messages are now automatically
encrypted when sent and decrypted when received (once user has keys).
The .single() method throws an error when no rows are found, which
was causing getUserKeys and getPublicKey to treat "no keys exist yet"
as a database error. Changed to .maybeSingle() which returns null
without an error when no rows match.

Also added step-by-step logging to generateAndStoreUserKeys so we can
see exactly where it fails if the INSERT is being blocked by RLS.

Fixes:
- getUserKeys: .single() → .maybeSingle()
- getPublicKey: .single() → .maybeSingle()
- hasEncryptionKeys: .single() → .maybeSingle()
- generateAndStoreUserKeys: added detailed logging for each step
Added detailed logging to diagnose why key generation is failing:
- Check auth context before INSERT
- Log all Supabase error fields (code, message, details, hint)
- Improved error messages in getPublicKey
- Will show if RLS policy is blocking the INSERT

This will tell us exactly why the database INSERT is failing.
Added console logs at key points in the signin handleSubmit function to trace execution:
- At function start
- After signIn call
- After getUser call
- Inside user check block
- During onboarding status check
- Before redirects

This will help identify where the encryption key loading code is being bypassed.
Add @/* path mapping to resolve module imports correctly.
This fixes the "Module not found: Can't resolve '@/app/lib/encryption'" error.
Add isEncryptedMessage() helper to detect encrypted vs plain text messages.
Update MessageService to only decrypt messages that are actually encrypted.
This fixes "Unable to decrypt message" errors for old plain text messages.

- Add isEncryptedMessage() function to check if content is encrypted
- Check message encryption status before attempting decryption
- Return plain text messages as-is without decryption attempts
- Prevents InvalidCharacterError when trying to decode plain text as base64
Implement localStorage caching for sent messages so senders can see
their own messages after page refresh.

Problem: Sent messages are encrypted with receiver's public key,
so the sender cannot decrypt them later. This caused sent messages
to appear as encrypted gibberish after refresh.

Solution:
- Create SentMessageCache to store plain text in localStorage
- Cache sent message content after successful send
- Retrieve from cache when fetching sent encrypted messages
- Show placeholder only if cache miss (cleared/old messages)

This provides Signal-like UX where users can see their sent messages
even though they're end-to-end encrypted.
Add sender_encrypted_content column to enable senders to decrypt
their own sent messages across devices.

Changes:
- Add migration to create sender_encrypted_content column (nullable)
- Update Message interface to include new field
- Encrypt messages with BOTH sender's and receiver's public keys
- Decrypt sender's copy for sent messages on any device
- Maintain backwards compatibility with old messages (NULL check)
- Keep localStorage cache as fallback for pre-migration messages

Benefits:
✅ Senders can see their sent messages on ANY device
✅ True end-to-end encryption maintained
✅ Backwards compatible with existing messages
✅ No impact on other developers (nullable column)

How it works:
1. Send message: Encrypt with receiver's key → content
2. Send message: Encrypt with sender's key → sender_encrypted_content
3. Fetch messages: Decrypt appropriate copy based on user role
4. Old messages: Fall back to localStorage cache or placeholder
Problem: On page reload, encryption keys stored only in React state
were lost, causing messages to show as encrypted gibberish.

Solution: Persist keys in sessionStorage (not localStorage for security)
- Keys are restored on page load if available
- Keys are cleared on logout and browser close
- sessionStorage is more secure than localStorage (tab-scoped)

Benefits:
✅ Messages decrypt correctly after page reload
✅ Keys persist during tab session
✅ Keys auto-clear when browser closes (sessionStorage behavior)
✅ Keys cleared on explicit logout

Security note: sessionStorage is safer than localStorage because:
- Cleared automatically when browser tab closes
- Not accessible across tabs (isolated per tab)
- Not persisted to disk like localStorage
Resolved merge conflicts between encryption implementation and main branch updates.

Key conflict resolutions:
- Combined useAuthGuard and useCrypto hooks in messages page
- Kept double encryption implementation (sender_encrypted_content)
- Preserved sessionStorage persistence for encryption keys
- Integrated admin redirect logic with encryption flow

Main branch changes integrated:
✅ New pages: about, contact, safety
✅ Search functionality improvements and suggestions
✅ Price history feature
✅ Auth guard hooks and route access controls
✅ UI/UX improvements across the app
✅ Database migrations and schema updates

Encryption features preserved:
✅ End-to-end message encryption
✅ Double encryption for cross-device access
✅ sessionStorage key persistence
✅ Encryption key generation and management
✅ Message caching for sent messages

https://claude.ai/code/session_011CV35t9Zki6cSdXrbpCbQv
Add a visible security indicator at the top of each conversation
to inform users that their messages are fully encrypted.

https://claude.ai/code/session_011CV35t9Zki6cSdXrbpCbQv
@lushkiwi lushkiwi changed the title End-to-end encryption to messaging! feat: add end-to-end encryption to messaging Mar 8, 2026
@Austin616
Austin616 merged commit 4b64aa4 into Longhorn-Developers:main Mar 8, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants