Files
timebank-cc-public/references/plans/e2e-encryption-plan.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

32 KiB

End-to-End Encryption Implementation Plan for WireChat

Executive Summary

This document outlines a comprehensive plan to implement end-to-end encryption (E2E) for WireChat messages in a vendor-update-safe manner. The implementation will encrypt the body column in the wirechat_messages table while maintaining full compatibility with the existing WireChat package.

1. Current WireChat Architecture Analysis

1.1 Message Flow

Based on analysis of WireChat v0.2.10:

Message Creation Path:

  1. User submits message via Chat.php Livewire component (line 318: sendMessage())
  2. Message model created directly: Message::create() (lines 392, 439-446)
  3. Message stored in database with plaintext body
  4. BroadcastMessage job dispatched to queue
  5. MessageCreated event broadcast via Laravel Echo/Reverb
  6. Recipients receive message via WebSocket listener (line 80-81)

Key Hook Points:

  • Message creation: Chat.php::sendMessage() at lines 392-446
  • Message retrieval: Message model via Eloquent
  • Broadcasting: BroadcastMessage job and MessageCreated event
  • Display: Livewire components with loadedMessages property

1.2 Database Schema

wirechat_messages table:
- id (primary key)
- conversation_id (foreign key)
- sendable_id (polymorphic)
- sendable_type (polymorphic)
- reply_id (nullable foreign key)
- body (TEXT, nullable) ← TARGET FOR ENCRYPTION
- type (string: text/attachment)
- kept_at (timestamp, nullable)
- deleted_at (soft delete)
- timestamps

2. End-to-End Encryption Architecture

2.1 Encryption Strategy: Signal Protocol-Inspired Approach

Choice Rationale:

  • NOT Symmetric per-conversation keys (vulnerable if key is compromised)
  • YES Asymmetric encryption with ephemeral per-message keys
  • Hybrid approach: RSA for key exchange, AES-256-GCM for message content

2.2 Key Management System

User Key Pairs (RSA 4096-bit):

  • Generated on first message send/receive
  • Private key encrypted with user password-derived key (PBKDF2)
  • Public key stored in database for other users to encrypt messages
  • Private key stored encrypted in database, decrypted client-side only

Message Encryption Keys (AES-256-GCM):

  • Ephemeral symmetric key generated per message
  • Encrypted separately for each conversation participant using their RSA public key
  • Allows forward secrecy: compromising one message doesn't compromise others

Key Storage Tables:

user_encryption_keys:
- id
- user_id (polymorphic: sendable_id, sendable_type)
- public_key (TEXT)
- encrypted_private_key (TEXT) ← encrypted with user's password
- private_key_salt (TEXT)
- created_at
- updated_at

message_encryption_keys:
- id
- message_id (foreign key to wirechat_messages)
- recipient_id (polymorphic: sendable_id, sendable_type)
- encrypted_message_key (TEXT) ← AES key encrypted with recipient's RSA public key
- nonce (TEXT) ← unique per encryption
- created_at
- index on (message_id, recipient_id, recipient_type)

2.3 Encryption Flow

Sending Message:

  1. Frontend: Generate random AES-256-GCM key
  2. Frontend: Encrypt message body with AES key
  3. Frontend: For each conversation participant:
    • Fetch participant's RSA public key
    • Encrypt AES key with participant's public key
  4. Backend: Store encrypted body in wirechat_messages.body
  5. Backend: Store encrypted keys in message_encryption_keys (one row per recipient)
  6. Broadcast encrypted message via existing WireChat events

Receiving Message:

  1. Frontend: Receive encrypted message via WebSocket
  2. Frontend: Fetch own encrypted message key from message_encryption_keys
  3. Frontend: Decrypt AES key using own RSA private key (decrypted from password)
  4. Frontend: Decrypt message body with AES key
  5. Frontend: Display decrypted message

2.4 Security Features

Forward Secrecy:

  • Unique AES key per message
  • Past messages remain secure even if current keys are compromised

Authentication:

  • Optional: Sign messages with sender's private key
  • Recipients verify signature with sender's public key
  • Prevents impersonation attacks

Metadata Protection Limitations:

  • Conversation participants visible (unavoidable with current architecture)
  • Message timestamps visible (required for sorting)
  • Message count visible (required for pagination)
  • Content and attachments fully encrypted

3. Laravel Override Strategy (Vendor-Update-Safe)

3.1 Service Provider: EncryptionServiceProvider

Location: app/Providers/EncryptionServiceProvider.php

class EncryptionServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Register event listeners
        Event::listen(MessageCreated::class, MessageCreatedEncryptionListener::class);

        // Register model observer
        Message::observe(MessageEncryptionObserver::class);

        // Register Livewire component override
        Livewire::component('wirechat.chat', \App\Livewire\EncryptedChat::class);
    }
}

3.2 Model Observer: MessageEncryptionObserver

Location: app/Observers/MessageEncryptionObserver.php

Purpose: Intercept message operations without modifying vendor code

class MessageEncryptionObserver
{
    public function retrieved(Message $message)
    {
        // Mark message as encrypted for frontend handling
        // Do NOT decrypt here (server should never see plaintext)
        $message->setAttribute('is_encrypted', $this->isEncrypted($message));
    }

    public function created(Message $message)
    {
        // Validate encryption metadata exists
        // Log encryption status for audit
    }
}

3.3 Event Listener: MessageCreatedEncryptionListener

Location: app/Listeners/MessageCreatedEncryptionListener.php

Purpose: Handle encryption key distribution when message is broadcast

class MessageCreatedEncryptionListener
{
    public function handle(MessageCreated $event)
    {
        $message = $event->message;
        $conversation = $message->conversation;

        // Attach encrypted keys for each participant
        // This allows recipients to decrypt the message
        $event->encryptedKeys = MessageEncryptionKey::where('message_id', $message->id)
            ->get()
            ->mapWithKeys(function ($key) {
                return ["{$key->recipient_type}:{$key->recipient_id}" => [
                    'encrypted_key' => $key->encrypted_message_key,
                    'nonce' => $key->nonce,
                ]];
            });
    }
}

3.4 Livewire Component Override: EncryptedChat

Location: app/Livewire/EncryptedChat.php

Purpose: Extend Chat component without modifying vendor file

class EncryptedChat extends \Namu\WireChat\Livewire\Chat\Chat
{
    // Override sendMessage to handle encryption on frontend
    public function sendMessage()
    {
        // Validation remains the same
        // Encryption happens client-side via JavaScript
        // This method receives already-encrypted body

        parent::sendMessage();
    }

    // Add method to fetch user's encryption keys
    public function getUserEncryptionKeys()
    {
        return UserEncryptionKey::where('sendable_id', auth()->id())
            ->where('sendable_type', auth()->user()->getMorphClass())
            ->first();
    }

    // Add method to fetch conversation participants' public keys
    public function getConversationPublicKeys()
    {
        $participants = $this->conversation->participants;

        return $participants->map(function ($participant) {
            return [
                'id' => $participant->participantable_id,
                'type' => $participant->participantable_type,
                'public_key' => $participant->participantable->encryptionKey->public_key ?? null,
            ];
        });
    }
}

3.5 Custom Routes and Controllers

Location: routes/web.php and app/Http/Controllers/EncryptionController.php

Purpose: API endpoints for key management

// routes/web.php
Route::middleware(['auth'])->group(function () {
    Route::post('/encryption/initialize', [EncryptionController::class, 'initialize']);
    Route::get('/encryption/public-keys/{conversation}', [EncryptionController::class, 'getPublicKeys']);
    Route::post('/encryption/store-message-keys', [EncryptionController::class, 'storeMessageKeys']);
});

4. Database Schema Additions

4.1 Migration: create_user_encryption_keys_table

Location: database/migrations/YYYY_MM_DD_create_user_encryption_keys_table.php

Schema::create('user_encryption_keys', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('sendable_id');
    $table->string('sendable_type');
    $table->text('public_key');
    $table->text('encrypted_private_key');
    $table->string('private_key_salt');
    $table->string('key_version')->default('v1'); // Allow key rotation
    $table->boolean('is_active')->default(true);
    $table->timestamps();

    $table->index(['sendable_id', 'sendable_type']);
    $table->unique(['sendable_id', 'sendable_type', 'is_active']);
});

4.2 Migration: create_message_encryption_keys_table

Schema::create('message_encryption_keys', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('message_id');
    $table->unsignedBigInteger('recipient_id');
    $table->string('recipient_type');
    $table->text('encrypted_message_key');
    $table->string('nonce');
    $table->string('algorithm')->default('AES-256-GCM');
    $table->timestamps();

    $table->foreign('message_id')
        ->references('id')
        ->on('wirechat_messages')
        ->onDelete('cascade');

    $table->index(['message_id']);
    $table->index(['recipient_id', 'recipient_type']);
    $table->unique(['message_id', 'recipient_id', 'recipient_type']);
});

4.3 Migration: add_encryption_metadata_to_messages

Schema::table('wirechat_messages', function (Blueprint $table) {
    $table->boolean('is_encrypted')->default(false)->after('body');
    $table->string('encryption_version')->nullable()->after('is_encrypted');
});

// Index for filtering encrypted messages
Schema::table('wirechat_messages', function (Blueprint $table) {
    $table->index('is_encrypted');
});

5. Frontend JavaScript Encryption Implementation

5.1 Encryption Library Choice

Recommended: SubtleCrypto Web API (native browser support)

  • No external dependencies
  • Hardware-accelerated
  • Secure key storage via IndexedDB
  • Supports RSA-OAEP and AES-GCM

Fallback: CryptoJS or forge.js for older browsers

5.2 JavaScript Module Structure

Location: resources/js/encryption/

encryption/
├── crypto-engine.js          # Core encryption/decryption functions
├── key-manager.js            # Key generation, storage, retrieval
├── message-handler.js        # Intercept Livewire messages
└── storage-adapter.js        # IndexedDB for key caching

5.3 Key Management Module

key-manager.js

class KeyManager {
    async generateKeyPair(userId, userPassword) {
        // Generate RSA-OAEP 4096-bit key pair
        const keyPair = await crypto.subtle.generateKey(
            {
                name: "RSA-OAEP",
                modulusLength: 4096,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: "SHA-256"
            },
            true,
            ["encrypt", "decrypt"]
        );

        // Export public key (store in database)
        const publicKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);

        // Export private key
        const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);

        // Derive encryption key from password
        const passwordKey = await this.deriveKeyFromPassword(userPassword);

        // Encrypt private key with password
        const encryptedPrivateKey = await this.encryptPrivateKey(privateKeyJwk, passwordKey);

        return {
            publicKey: JSON.stringify(publicKeyJwk),
            encryptedPrivateKey: encryptedPrivateKey,
            salt: passwordKey.salt
        };
    }

    async deriveKeyFromPassword(password, salt = null) {
        // Use PBKDF2 to derive key from password
        if (!salt) {
            salt = crypto.getRandomValues(new Uint8Array(16));
        }

        const baseKey = await crypto.subtle.importKey(
            "raw",
            new TextEncoder().encode(password),
            "PBKDF2",
            false,
            ["deriveBits", "deriveKey"]
        );

        const derivedKey = await crypto.subtle.deriveKey(
            {
                name: "PBKDF2",
                salt: salt,
                iterations: 100000,
                hash: "SHA-256"
            },
            baseKey,
            { name: "AES-GCM", length: 256 },
            true,
            ["encrypt", "decrypt"]
        );

        return { key: derivedKey, salt: Array.from(salt) };
    }

    async unlockPrivateKey(encryptedPrivateKey, userPassword, salt) {
        // Derive key from password
        const { key } = await this.deriveKeyFromPassword(userPassword, new Uint8Array(salt));

        // Decrypt private key
        const privateKeyJwk = await this.decryptPrivateKey(encryptedPrivateKey, key);

        // Import as CryptoKey
        const privateKey = await crypto.subtle.importKey(
            "jwk",
            JSON.parse(privateKeyJwk),
            { name: "RSA-OAEP", hash: "SHA-256" },
            true,
            ["decrypt"]
        );

        // Cache in IndexedDB (session-based)
        await this.cachePrivateKey(privateKey);

        return privateKey;
    }
}

5.4 Message Encryption Module

crypto-engine.js

class CryptoEngine {
    async encryptMessage(messageBody, recipientPublicKeys) {
        // Generate ephemeral AES-256-GCM key
        const messageKey = await crypto.subtle.generateKey(
            { name: "AES-GCM", length: 256 },
            true,
            ["encrypt", "decrypt"]
        );

        // Generate unique nonce/IV
        const nonce = crypto.getRandomValues(new Uint8Array(12));

        // Encrypt message body
        const encodedMessage = new TextEncoder().encode(messageBody);
        const encryptedBody = await crypto.subtle.encrypt(
            { name: "AES-GCM", iv: nonce },
            messageKey,
            encodedMessage
        );

        // Export message key
        const rawMessageKey = await crypto.subtle.exportKey("raw", messageKey);

        // Encrypt message key for each recipient
        const encryptedKeys = {};
        for (const [recipientId, publicKeyJwk] of Object.entries(recipientPublicKeys)) {
            const publicKey = await crypto.subtle.importKey(
                "jwk",
                JSON.parse(publicKeyJwk),
                { name: "RSA-OAEP", hash: "SHA-256" },
                true,
                ["encrypt"]
            );

            const encryptedMessageKey = await crypto.subtle.encrypt(
                { name: "RSA-OAEP" },
                publicKey,
                rawMessageKey
            );

            encryptedKeys[recipientId] = {
                encryptedKey: this.arrayBufferToBase64(encryptedMessageKey),
                nonce: this.arrayBufferToBase64(nonce)
            };
        }

        return {
            encryptedBody: this.arrayBufferToBase64(encryptedBody),
            nonce: this.arrayBufferToBase64(nonce),
            recipientKeys: encryptedKeys
        };
    }

    async decryptMessage(encryptedBody, encryptedMessageKey, nonce, privateKey) {
        // Decrypt message key
        const messageKeyBuffer = await crypto.subtle.decrypt(
            { name: "RSA-OAEP" },
            privateKey,
            this.base64ToArrayBuffer(encryptedMessageKey)
        );

        // Import message key
        const messageKey = await crypto.subtle.importKey(
            "raw",
            messageKeyBuffer,
            { name: "AES-GCM", length: 256 },
            false,
            ["decrypt"]
        );

        // Decrypt message body
        const decryptedBuffer = await crypto.subtle.decrypt(
            { name: "AES-GCM", iv: this.base64ToArrayBuffer(nonce) },
            messageKey,
            this.base64ToArrayBuffer(encryptedBody)
        );

        return new TextDecoder().decode(decryptedBuffer);
    }

    arrayBufferToBase64(buffer) {
        return btoa(String.fromCharCode(...new Uint8Array(buffer)));
    }

    base64ToArrayBuffer(base64) {
        const binary = atob(base64);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
    }
}

5.5 Livewire Integration

message-handler.js

// Intercept Livewire wire:submit for message sending
document.addEventListener('livewire:init', () => {
    Livewire.hook('element.updated', async (el, component) => {
        if (component.fingerprint.name === 'wirechat.chat') {
            // Decrypt all encrypted messages in loadedMessages
            await decryptLoadedMessages(component);
        }
    });

    // Intercept sendMessage before it fires
    Livewire.hook('commit', async ({ component, commit, respond }) => {
        if (component.fingerprint.name === 'wirechat.chat' && component.body) {
            // Get recipient public keys
            const publicKeys = await component.call('getConversationPublicKeys');

            // Encrypt message
            const encrypted = await cryptoEngine.encryptMessage(
                component.body,
                publicKeys
            );

            // Replace body with encrypted version
            component.body = encrypted.encryptedBody;

            // Store encryption metadata
            component.encryptionData = encrypted;
        }
    });
});

async function decryptLoadedMessages(component) {
    const privateKey = await keyManager.getCachedPrivateKey();
    if (!privateKey) return; // User needs to unlock

    // Decrypt each message
    for (const [groupKey, messages] of Object.entries(component.loadedMessages)) {
        for (const message of messages) {
            if (message.is_encrypted && message.body) {
                try {
                    const encryptionKey = await fetchMessageKey(message.id);
                    message.body = await cryptoEngine.decryptMessage(
                        message.body,
                        encryptionKey.encrypted_message_key,
                        encryptionKey.nonce,
                        privateKey
                    );
                } catch (error) {
                    console.error('Failed to decrypt message:', error);
                    message.body = '[Decryption Failed]';
                }
            }
        }
    }
}

5.6 User Experience Flow

First-Time Setup:

  1. User sends/receives first encrypted message
  2. Modal prompts: "Set up encryption password"
  3. User enters password (NOT their login password)
  4. Generate key pair, encrypt private key, upload to server
  5. Store private key in IndexedDB (session-based)

Subsequent Sessions:

  1. User opens chat
  2. Modal prompts: "Unlock encrypted messages"
  3. User enters encryption password
  4. Decrypt private key from database
  5. Cache in IndexedDB for session
  6. Auto-decrypt all messages

Sending Encrypted Message:

  1. User types message
  2. Frontend encrypts before sending
  3. Backend stores encrypted body + keys
  4. No visual difference for user (transparent)

Receiving Encrypted Message:

  1. WebSocket delivers encrypted message
  2. Frontend auto-decrypts if private key cached
  3. Display decrypted message
  4. If key not cached, show "[Locked Message - Enter Password]"

6. Key Exchange and Session Management

6.1 New Conversation Participant Flow

Scenario: User added to group conversation

  1. New participant generates key pair (if first encrypted message)
  2. New participant's public key uploaded to server
  3. Future messages encrypted with new participant's public key
  4. Past messages remain unreadable (forward secrecy)
  5. Optional: Re-encrypt recent messages for new participant (configuration)

6.2 Key Rotation Strategy

Why Rotate:

  • Compromise detection
  • Periodic security refresh
  • User-initiated (e.g., password change)

Process:

  1. Generate new key pair
  2. Mark old key as is_active = false
  3. New messages use new key
  4. Old messages remain decryptable with old key
  5. UI shows "Key Version" for old messages

6.3 Session Management

Private Key Caching:

  • Store in IndexedDB (not localStorage for security)
  • Clear on browser close (session-based)
  • Clear on manual logout
  • Optional: Biometric unlock (WebAuthn)

Public Key Caching:

  • Cache conversation participants' public keys
  • Refresh on conversation change
  • Cache duration: 1 hour (configurable)

7. Backward Compatibility for Existing Messages

7.1 Migration Strategy

Challenge: Existing messages are plaintext

Options:

Option A: Flag-Based (Recommended)

  • Add is_encrypted boolean to messages table
  • Existing messages: is_encrypted = false (plaintext)
  • New messages: is_encrypted = true (encrypted)
  • Frontend checks flag before decryption attempt
  • No data migration required

Option B: Gradual Encryption

  • Background job encrypts old messages
  • Fetch participants at time of message
  • Encrypt with current public keys
  • Risk: Participants may have left conversation

Option C: Mixed-Mode Forever

  • Display plaintext messages as-is
  • Display encrypted messages with lock icon
  • Users understand transition period

7.2 Implementation (Option A)

// MessageEncryptionObserver.php
public function retrieved(Message $message)
{
    // Check if message is encrypted
    if (!$message->is_encrypted) {
        // Plaintext message, no decryption needed
        $message->setAttribute('encryption_status', 'plaintext');
        return;
    }

    // Encrypted message, frontend will decrypt
    $message->setAttribute('encryption_status', 'encrypted');
}
// Frontend handling
function displayMessage(message) {
    if (message.encryption_status === 'plaintext') {
        return message.body; // Display as-is
    } else if (message.encryption_status === 'encrypted') {
        return await decryptMessage(message); // Decrypt first
    }
}

7.3 User Communication

In-App Notification:

"Encryption enabled! New messages are now end-to-end encrypted. Previous messages remain visible but are not encrypted."

FAQ Entry:

Q: Are my old messages encrypted? A: Messages sent before [DATE] are stored in plaintext. Messages sent after this date are fully end-to-end encrypted.

8. Security Considerations

8.1 Threat Model

Protected Against:

  • Database breach (encrypted body unreadable)
  • Server compromise (no plaintext on server)
  • Man-in-the-middle (RSA key exchange)
  • Past message compromise (forward secrecy)

NOT Protected Against:

  • Compromised client (malicious JavaScript)
  • Keylogger on user device
  • User password theft (if used to encrypt private key)
  • Metadata analysis (conversation graph, timing)
  • Server admin reading message before encryption (active attack)

8.2 Password vs. Login Separation

Critical: Encryption password MUST be separate from login password

Reasoning:

  • Server knows login password (hashed but verifiable)
  • Server must NEVER know encryption password
  • Compromise of login ≠ compromise of messages

Implementation:

  • Prompt for separate encryption password on setup
  • Store encrypted private key, never store encryption password
  • Password recovery = lose access to old messages (by design)

8.3 Key Backup and Recovery

Challenge: User forgets encryption password

Solution Options:

Option A: No Recovery (Most Secure)

  • Lost password = lost messages
  • Warning during setup
  • Export key backup option

Option B: Recovery Key

  • Generate 24-word recovery phrase
  • User must write down and store securely
  • Can regenerate private key from recovery phrase

Option C: Trusted Device Backup

  • Export encrypted key to trusted device
  • Requires device authentication to import

8.4 Audit Logging

Events to Log:

  • Key pair generation
  • Encryption password changes
  • Failed decryption attempts (possible attack)
  • Key rotation events

Storage:

encryption_audit_log:
- id
- user_id (polymorphic)
- event_type (enum)
- ip_address
- user_agent
- metadata (JSON)
- created_at

9. Implementation Steps (Phase-by-Phase)

Phase 1: Foundation (Week 1-2)

  • Create database migrations
  • Create models: UserEncryptionKey, MessageEncryptionKey
  • Create EncryptionServiceProvider
  • Create EncryptionController with basic routes
  • Write unit tests for models

Phase 2: Backend Integration (Week 2-3)

  • Create MessageEncryptionObserver
  • Create MessageCreatedEncryptionListener
  • Override EncryptedChat Livewire component
  • Add encryption key storage endpoints
  • Write integration tests

Phase 3: Frontend Encryption (Week 3-4)

  • Implement crypto-engine.js with SubtleCrypto
  • Implement key-manager.js with IndexedDB
  • Create key generation UI modal
  • Create password unlock UI modal
  • Implement storage adapter

Phase 4: Livewire Integration (Week 4-5)

  • Implement message-handler.js Livewire hooks
  • Intercept sendMessage for encryption
  • Implement automatic decryption on receive
  • Add loading states and error handling
  • Test with real conversations

Phase 5: Key Exchange & Management (Week 5-6)

  • Implement conversation public key fetching
  • Handle new participant key distribution
  • Implement key rotation mechanism
  • Add key backup/export feature
  • Test multi-participant scenarios

Phase 6: Backward Compatibility (Week 6-7)

  • Implement mixed-mode message display
  • Add migration script for existing data
  • Create user notification system
  • Add encryption status indicators
  • Test with existing plaintext messages

Phase 7: Testing & Hardening (Week 7-8)

  • Security audit of crypto implementation
  • Penetration testing
  • Performance testing (1000+ messages)
  • Browser compatibility testing
  • Write comprehensive documentation

Phase 8: Deployment (Week 8-9)

  • Create deployment runbook
  • Staged rollout plan
  • Monitoring and alerting setup
  • User education materials
  • Rollback procedure

10. Configuration and Feature Flags

10.1 Configuration File

Location: config/encryption.php

return [
    // Enable/disable encryption globally
    'enabled' => env('ENCRYPTION_ENABLED', true),

    // Encryption algorithm version
    'version' => 'v1',

    // RSA key size
    'rsa_key_size' => 4096,

    // AES key size
    'aes_key_size' => 256,

    // PBKDF2 iterations for password key derivation
    'pbkdf2_iterations' => 100000,

    // Cache private key in IndexedDB
    'cache_private_key' => true,

    // Cache duration for public keys (seconds)
    'public_key_cache_duration' => 3600,

    // Re-encrypt old messages for new participants
    'encrypt_for_new_participants' => false,

    // Maximum messages to decrypt at once (performance)
    'max_decrypt_batch_size' => 50,

    // Show encryption status in UI
    'show_encryption_status' => true,

    // Require encryption password separate from login
    'require_separate_password' => true,

    // Enable key rotation
    'allow_key_rotation' => true,

    // Audit logging
    'audit_enabled' => true,
];

10.2 Feature Flags

Use Laravel Pennant or custom flags:

Feature::define('message-encryption', function (User $user) {
    // Gradual rollout
    return $user->created_at->isAfter('2024-01-01');
});

11. Testing Strategy

11.1 Unit Tests

Backend:

  • UserEncryptionKey model CRUD
  • MessageEncryptionKey model CRUD
  • Observer hooks fire correctly
  • Listener attaches encryption data

Frontend:

  • Key generation produces valid keys
  • Encryption/decryption roundtrip succeeds
  • Password derivation consistent
  • Base64 encoding/decoding accurate

11.2 Integration Tests

  • Send encrypted message in private conversation
  • Send encrypted message in group conversation
  • Receive and decrypt message
  • Handle missing encryption keys gracefully
  • New participant cannot read old messages
  • Key rotation preserves message access

11.3 Security Tests

  • Private key never sent unencrypted
  • Server cannot decrypt messages
  • XSS doesn't leak private keys
  • CSRF protection on key endpoints
  • Rate limiting on decryption attempts

11.4 Performance Tests

  • Encrypt 100 messages/second
  • Decrypt 100 messages/second
  • Load conversation with 1000 messages
  • Handle 50 concurrent participants
  • IndexedDB cache hit rate > 95%

12. Documentation and Training

12.1 User Documentation

  • "What is End-to-End Encryption?"
  • "Setting Up Your Encryption Password"
  • "What Happens If I Forget My Password?"
  • "Why Can't I Read Old Messages?"
  • "Is My Data Safe?"

12.2 Developer Documentation

  • Architecture overview (this document)
  • API endpoint reference
  • JavaScript module documentation
  • Deployment guide
  • Troubleshooting guide

12.3 Admin Documentation

  • Monitoring encryption health
  • Handling user reports of decryption failures
  • Key rotation procedures
  • Disaster recovery

13. Monitoring and Maintenance

13.1 Metrics to Track

  • Percentage of encrypted messages
  • Decryption success rate
  • Average encryption/decryption time
  • Private key cache hit rate
  • Failed decryption attempts (security)

13.2 Alerts

  • Decryption failure rate > 5%
  • Encryption endpoint errors
  • Unusual key generation patterns
  • Database query performance degradation

13.3 Maintenance Tasks

  • Quarterly security audit
  • Key rotation reminders
  • Database cleanup (orphaned keys)
  • Performance optimization reviews

14. Risks and Mitigations

Risk Impact Mitigation
Browser incompatibility High Fallback to CryptoJS, browser compatibility checks
Performance degradation Medium Batch decryption, IndexedDB caching, Web Workers
User loses encryption password High Clear warnings, recovery key option, export feature
WireChat package update breaks integration High Comprehensive tests, version pinning, override strategy
Server compromise Low E2E design ensures server sees only encrypted data
Key generation fails Medium Retry logic, error reporting, fallback to plaintext (configurable)
IndexedDB quota exceeded Low Periodic cleanup, cache eviction policy

15. Future Enhancements

15.1 Advanced Features

  • Device Verification: QR code for trusted device pairing
  • Message Signatures: Verify sender authenticity
  • Self-Destructing Messages: Auto-delete after reading
  • Encrypted Attachments: Extend encryption to file uploads
  • Encrypted Search: Homomorphic encryption for search
  • Biometric Unlock: WebAuthn for password replacement

15.2 Compliance Features

  • GDPR: Export/delete encrypted messages
  • HIPAA: Audit trails for healthcare compliance
  • SOC 2: Encryption key lifecycle management

16. Cost-Benefit Analysis

16.1 Benefits

  • User data protection from breaches
  • Competitive advantage (privacy-focused)
  • Regulatory compliance readiness
  • User trust and confidence
  • Minimal performance impact

16.2 Costs

  • Development time: ~8-9 weeks
  • Increased complexity: key management
  • Support overhead: password resets
  • Storage increase: ~30% (keys + metadata)
  • Cannot implement server-side search on encrypted messages

16.3 Recommendation

Proceed with implementation given:

  • Growing privacy expectations
  • Relatively low development cost
  • Strong vendor-update-safe architecture
  • Minimal user experience impact
  • High security ROI

17. Conclusion

This plan provides a comprehensive, vendor-update-safe approach to implementing E2E encryption for WireChat messages. By using Laravel's event system, Livewire component overrides, and frontend encryption, we can add this critical security feature without modifying the WireChat package directly.

The implementation prioritizes:

  1. Security: Strong cryptography with forward secrecy
  2. Maintainability: Separation from vendor code
  3. Usability: Transparent to users after setup
  4. Compatibility: Works with existing messages
  5. Performance: Efficient caching and batching

Next step: Get user approval and begin Phase 1 implementation.