1070 lines
32 KiB
Markdown
1070 lines
32 KiB
Markdown
# 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`
|
|
|
|
```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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// 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`
|
|
|
|
```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`
|
|
|
|
```php
|
|
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`
|
|
|
|
```php
|
|
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`**
|
|
|
|
```javascript
|
|
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`**
|
|
|
|
```javascript
|
|
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`**
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```php
|
|
// 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');
|
|
}
|
|
```
|
|
|
|
```javascript
|
|
// 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`
|
|
|
|
```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:
|
|
|
|
```php
|
|
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.
|