# Universal End-to-End Encryption System
## Extending E2E Encryption to All Sensitive Data

## Executive Summary

This document extends the simplified E2E encryption plan to create a **universal encryption system** that works for:
- ✅ WireChat messages (already planned)
- ✅ Transaction descriptions (new)
- ✅ Future sensitive data fields (extensible design)

The system uses a **polymorphic, trait-based architecture** that makes any model field encryptable with minimal code changes.

## 1. Universal Design Philosophy

### 1.1 Core Principles

**Polymorphic Encryption:**
- Any model can have encrypted fields
- Single encryption key management system
- Consistent encryption/decryption flow

**Recipient-Based Access:**
- Data encrypted for specific users/participants
- Each recipient gets their own encrypted key
- Flexible permission system

**Transparent to Application:**
- Encryption/decryption happens automatically
- No changes to existing business logic
- Backward compatible with plaintext data

**Future-Proof:**
- Easy to add encryption to new models
- Extensible architecture
- Version-aware for algorithm upgrades

### 1.2 Use Cases

| Data Type | Who Can Decrypt | Example |
|-----------|----------------|---------|
| WireChat Message | Conversation participants | Private chat between users |
| Transaction Description | From & To account owners | Payment description |
| Post Content | Post author + moderators | Private posts |
| Profile Notes | Profile owner + admins | Sensitive personal info |
| Document Attachments | Document owner + shared users | Private files |

## 2. Universal Encryption Architecture

### 2.1 Polymorphic Schema Design

**Single Universal Tables:**

```sql
-- Universal encryption keys table (replaces user_encryption_keys)
CREATE TABLE encryption_keys (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    owner_id BIGINT NOT NULL COMMENT 'ID of the owner (user, org, etc)',
    owner_type VARCHAR(255) NOT NULL COMMENT 'Model class of owner',
    public_key TEXT NOT NULL,
    encrypted_private_key TEXT NOT NULL COMMENT 'Encrypted with Laravel APP_KEY',
    key_version VARCHAR(10) DEFAULT 'v1',
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    INDEX idx_owner (owner_id, owner_type),
    UNIQUE KEY unique_active_key (owner_id, owner_type, is_active)
);

-- Universal encrypted data keys table (replaces message_encryption_keys)
CREATE TABLE encrypted_data_keys (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    encryptable_id BIGINT NOT NULL COMMENT 'ID of encrypted record',
    encryptable_type VARCHAR(255) NOT NULL COMMENT 'Model class (Message, Transaction, etc)',
    encryptable_field VARCHAR(100) NOT NULL COMMENT 'Field name (body, description, etc)',
    recipient_id BIGINT NOT NULL COMMENT 'Who can decrypt this',
    recipient_type VARCHAR(255) NOT NULL COMMENT 'Recipient model class',
    encrypted_data_key TEXT NOT NULL COMMENT 'AES key encrypted with recipient RSA key',
    nonce VARCHAR(255) NOT NULL,
    algorithm VARCHAR(50) DEFAULT 'AES-256-GCM',
    created_at TIMESTAMP,
    updated_at TIMESTAMP,

    -- Polymorphic indexes
    INDEX idx_encryptable (encryptable_id, encryptable_type, encryptable_field),
    INDEX idx_recipient (recipient_id, recipient_type),

    -- Ensure one key per recipient per field
    UNIQUE KEY unique_recipient_key (
        encryptable_id,
        encryptable_type,
        encryptable_field,
        recipient_id,
        recipient_type
    )
);

-- Encryption metadata on encrypted models (added via trait)
-- Each model that uses encryption gets these columns via migration:
-- - is_encrypted BOOLEAN DEFAULT FALSE
-- - encryption_version VARCHAR(10) NULL
-- - encrypted_fields JSON NULL (list of which fields are encrypted)
```

### 2.2 Transaction-Specific Schema

```sql
-- Add encryption support to transactions table
ALTER TABLE transactions
    ADD COLUMN is_encrypted BOOLEAN DEFAULT FALSE AFTER description,
    ADD COLUMN encryption_version VARCHAR(10) NULL AFTER is_encrypted,
    ADD COLUMN encrypted_fields JSON NULL AFTER encryption_version,
    ADD INDEX idx_is_encrypted (is_encrypted);

-- Example encrypted_fields JSON: ["description"]
-- Could expand to: ["description", "from_reference", "to_reference"]
```

## 3. Universal Encryption Trait

### 3.1 PHP Trait for Models

```php
// app/Traits/HasEncryptedFields.php
namespace App\Traits;

use App\Models\EncryptedDataKey;
use App\Models\EncryptionKey;
use Illuminate\Support\Facades\Log;

trait HasEncryptedFields
{
    /**
     * Define which fields should be encrypted
     * Override in your model
     */
    protected function getEncryptableFields(): array
    {
        return $this->encryptable ?? [];
    }

    /**
     * Define who can decrypt this model's data
     * Override in your model
     */
    abstract protected function getEncryptionRecipients(): array;

    /**
     * Boot the trait
     */
    protected static function bootHasEncryptedFields()
    {
        // After model is retrieved, mark encrypted fields
        static::retrieved(function ($model) {
            if ($model->is_encrypted) {
                $encryptedFields = json_decode($model->encrypted_fields, true) ?? [];
                foreach ($encryptedFields as $field) {
                    $model->setAttribute("{$field}_is_encrypted", true);
                }
            }
        });

        // After model is saved, handle encryption
        static::saved(function ($model) {
            if ($model->shouldEncrypt()) {
                Log::info('Encrypted data saved', [
                    'model' => get_class($model),
                    'id' => $model->id,
                    'fields' => $model->encrypted_fields
                ]);
            }
        });
    }

    /**
     * Check if this model should use encryption
     */
    public function shouldEncrypt(): bool
    {
        return $this->is_encrypted ?? false;
    }

    /**
     * Get encryption keys for recipients of this record
     */
    public function getRecipientKeys($field = null)
    {
        $recipients = $this->getEncryptionRecipients();

        return collect($recipients)->map(function ($recipient) use ($field) {
            $key = EncryptionKey::where([
                'owner_id' => $recipient->id,
                'owner_type' => get_class($recipient),
                'is_active' => true
            ])->first();

            if (!$key) {
                return null;
            }

            return [
                'id' => $recipient->id,
                'type' => get_class($recipient),
                'public_key' => $key->public_key,
                'field' => $field
            ];
        })->filter()->values();
    }

    /**
     * Get decryption key for current user for specific field
     */
    public function getDecryptionKey(string $field)
    {
        $user = auth()->user();
        if (!$user) {
            return null;
        }

        return EncryptedDataKey::where([
            'encryptable_id' => $this->id,
            'encryptable_type' => get_class($this),
            'encryptable_field' => $field,
            'recipient_id' => $user->id,
            'recipient_type' => get_class($user)
        ])->first();
    }

    /**
     * Store encryption keys for a field
     */
    public function storeEncryptionKeys(string $field, array $recipientKeysData)
    {
        foreach ($recipientKeysData as $keyData) {
            EncryptedDataKey::updateOrCreate(
                [
                    'encryptable_id' => $this->id,
                    'encryptable_type' => get_class($this),
                    'encryptable_field' => $field,
                    'recipient_id' => $keyData['recipient_id'],
                    'recipient_type' => $keyData['recipient_type']
                ],
                [
                    'encrypted_data_key' => $keyData['encrypted_key'],
                    'nonce' => $keyData['nonce'],
                    'algorithm' => 'AES-256-GCM'
                ]
            );
        }

        // Update encrypted_fields JSON
        $encryptedFields = json_decode($this->encrypted_fields, true) ?? [];
        if (!in_array($field, $encryptedFields)) {
            $encryptedFields[] = $field;
            $this->encrypted_fields = json_encode($encryptedFields);
            $this->is_encrypted = true;
            $this->encryption_version = 'v1';
            $this->saveQuietly(); // Save without triggering events
        }
    }

    /**
     * Check if user can decrypt this record
     */
    public function canDecrypt($user = null, string $field = null): bool
    {
        $user = $user ?? auth()->user();
        if (!$user) {
            return false;
        }

        // Check if user is a recipient
        $recipients = $this->getEncryptionRecipients();
        foreach ($recipients as $recipient) {
            if ($recipient->id === $user->id && get_class($recipient) === get_class($user)) {
                return true;
            }
        }

        return false;
    }
}
```

### 3.2 Apply to Message Model

```php
// app/Models/Message.php (WireChat override)
namespace App\Models;

use App\Traits\HasEncryptedFields;
use Namu\WireChat\Models\Message as BaseMessage;

class Message extends BaseMessage
{
    use HasEncryptedFields;

    protected $encryptable = ['body'];

    protected function getEncryptionRecipients(): array
    {
        // All conversation participants can decrypt
        return $this->conversation
            ->participants()
            ->with('participantable')
            ->get()
            ->pluck('participantable')
            ->filter()
            ->values()
            ->all();
    }
}
```

### 3.3 Apply to Transaction Model

```php
// app/Models/Transaction.php (extend existing)
namespace App\Models;

use App\Traits\HasEncryptedFields;
use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
    use HasEncryptedFields;

    protected $encryptable = ['description']; // Can add 'from_reference', 'to_reference' later

    protected function getEncryptionRecipients(): array
    {
        $recipients = [];

        // From account owner(s)
        if ($this->accountFrom && $this->accountFrom->accountable) {
            $recipients = array_merge($recipients, $this->accountFrom->accountable->all());
        }

        // To account owner(s)
        if ($this->accountTo && $this->accountTo->accountable) {
            $recipients = array_merge($recipients, $this->accountTo->accountable->all());
        }

        return array_filter($recipients);
    }

    /**
     * Override toSearchableArray to NOT index encrypted description
     */
    public function toSearchableArray()
    {
        $array = parent::toSearchableArray();

        // Don't index encrypted descriptions in Elasticsearch
        if ($this->is_encrypted && in_array('description', json_decode($this->encrypted_fields, true) ?? [])) {
            $array['description'] = '[Encrypted]';
        }

        return $array;
    }
}
```

## 4. Universal Frontend Encryption Module

### 4.1 Enhanced Encryption Service

```javascript
// resources/js/encryption/universal-encryption.js

class UniversalEncryption {
    constructor() {
        this.initialized = false;
        this.myKeys = null;
        this.keyCache = new Map(); // Cache recipient public keys
    }

    async initialize() {
        if (this.initialized) return;

        try {
            const response = await axios.get('/encryption/my-keys');
            this.myKeys = response.data;
            this.initialized = true;
            console.log('Universal encryption initialized');
        } catch (error) {
            if (error.response?.status === 404) {
                console.log('No encryption keys found - will generate on first use');
            } else {
                console.error('Failed to initialize encryption:', error);
            }
        }
    }

    async ensureKeys() {
        if (this.myKeys) return this.myKeys;

        console.log('Generating encryption keys...');
        const keyPair = await crypto.subtle.generateKey(
            {
                name: "RSA-OAEP",
                modulusLength: 2048,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: "SHA-256"
            },
            true,
            ["encrypt", "decrypt"]
        );

        const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
        const privateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey);

        await axios.post('/encryption/store-keys', {
            public_key: JSON.stringify(publicKey),
            private_key: JSON.stringify(privateKey)
        });

        this.myKeys = {
            public_key: JSON.stringify(publicKey),
            private_key: JSON.stringify(privateKey)
        };

        console.log('Encryption keys generated');
        return this.myKeys;
    }

    /**
     * Universal encryption method for any data
     *
     * @param {string} plaintext - Data to encrypt
     * @param {Array} recipients - Array of {id, type, public_key}
     * @returns {Object} {encryptedData, recipientKeys}
     */
    async encryptData(plaintext, recipients) {
        await this.ensureKeys();

        // Generate ephemeral AES key
        const dataKey = await crypto.subtle.generateKey(
            { name: "AES-GCM", length: 256 },
            true,
            ["encrypt", "decrypt"]
        );

        // Encrypt data
        const nonce = crypto.getRandomValues(new Uint8Array(12));
        const encodedData = new TextEncoder().encode(plaintext);
        const encryptedData = await crypto.subtle.encrypt(
            { name: "AES-GCM", iv: nonce },
            dataKey,
            encodedData
        );

        // Export and encrypt data key for each recipient
        const rawDataKey = await crypto.subtle.exportKey("raw", dataKey);
        const recipientKeys = [];

        for (const recipient of recipients) {
            if (!recipient.public_key) {
                console.warn('Recipient missing public key:', recipient);
                continue;
            }

            const recipientPublicKey = await crypto.subtle.importKey(
                "jwk",
                JSON.parse(recipient.public_key),
                { name: "RSA-OAEP", hash: "SHA-256" },
                false,
                ["encrypt"]
            );

            const encryptedDataKey = await crypto.subtle.encrypt(
                { name: "RSA-OAEP" },
                recipientPublicKey,
                rawDataKey
            );

            recipientKeys.push({
                recipient_id: recipient.id,
                recipient_type: recipient.type,
                encrypted_key: this.arrayBufferToBase64(encryptedDataKey),
                nonce: this.arrayBufferToBase64(nonce)
            });
        }

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

    /**
     * Universal decryption method for any data
     *
     * @param {string} encryptedData - Base64 encoded encrypted data
     * @param {string} encryptedDataKey - Base64 encoded encrypted AES key
     * @param {string} nonce - Base64 encoded nonce/IV
     * @returns {string} Decrypted plaintext
     */
    async decryptData(encryptedData, encryptedDataKey, nonce) {
        await this.ensureKeys();

        // Import my private key
        const privateKey = await crypto.subtle.importKey(
            "jwk",
            JSON.parse(this.myKeys.private_key),
            { name: "RSA-OAEP", hash: "SHA-256" },
            false,
            ["decrypt"]
        );

        // Decrypt data key
        const encryptedKeyBuffer = this.base64ToArrayBuffer(encryptedDataKey);
        const dataKeyBuffer = await crypto.subtle.decrypt(
            { name: "RSA-OAEP" },
            privateKey,
            encryptedKeyBuffer
        );

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

        // Decrypt data
        const nonceBuffer = this.base64ToArrayBuffer(nonce);
        const encryptedDataBuffer = this.base64ToArrayBuffer(encryptedData);
        const decryptedBuffer = await crypto.subtle.decrypt(
            { name: "AES-GCM", iv: nonceBuffer },
            dataKey,
            encryptedDataBuffer
        );

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

    /**
     * Encrypt a model field
     *
     * @param {string} modelType - Model class name
     * @param {number} modelId - Model ID
     * @param {string} field - Field name to encrypt
     * @param {string} value - Value to encrypt
     * @returns {Object} Encryption data to send to server
     */
    async encryptModelField(modelType, modelId, field, value) {
        // Get recipients for this model
        const response = await axios.get(`/encryption/recipients/${modelType}/${modelId}`);
        const recipients = response.data;

        if (recipients.length === 0) {
            throw new Error('No recipients found for encryption');
        }

        // Encrypt the data
        const encrypted = await this.encryptData(value, recipients);

        return {
            model_type: modelType,
            model_id: modelId,
            field: field,
            encrypted_value: encrypted.encryptedData,
            recipient_keys: encrypted.recipientKeys
        };
    }

    /**
     * Decrypt a model field
     *
     * @param {string} modelType - Model class name
     * @param {number} modelId - Model ID
     * @param {string} field - Field name to decrypt
     * @param {string} encryptedValue - Encrypted value
     * @returns {string} Decrypted value
     */
    async decryptModelField(modelType, modelId, field, encryptedValue) {
        // Get my decryption key for this field
        const response = await axios.get(`/encryption/key/${modelType}/${modelId}/${field}`);
        const keyData = response.data;

        // Decrypt
        return await this.decryptData(
            encryptedValue,
            keyData.encrypted_data_key,
            keyData.nonce
        );
    }

    // Helper methods
    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;
    }
}

// Global instance
window.universalEncryption = new UniversalEncryption();
```

### 4.2 Transaction Form Integration

```javascript
// resources/js/encryption/transaction-encryption.js

// Initialize on page load
document.addEventListener('DOMContentLoaded', async () => {
    await window.universalEncryption.initialize();
});

// Hook into transaction form submission
document.addEventListener('livewire:init', () => {
    Livewire.hook('commit', async ({ component, commit, respond }) => {
        // Check if this is a transaction form
        if (component.fingerprint.name.includes('transaction') && component.description) {
            try {
                // Get transaction recipients (from/to account owners)
                const recipients = await axios.get(`/encryption/transaction-recipients`, {
                    params: {
                        from_account_id: component.from_account_id,
                        to_account_id: component.to_account_id
                    }
                });

                // Encrypt description
                const encrypted = await window.universalEncryption.encryptData(
                    component.description,
                    recipients.data
                );

                // Replace description with encrypted version
                component.description = encrypted.encryptedData;

                // Store encryption metadata
                component.encryptionData = {
                    field: 'description',
                    recipient_keys: encrypted.recipientKeys,
                    is_encrypted: true
                };

                console.log('Transaction description encrypted');
            } catch (error) {
                console.error('Failed to encrypt transaction:', error);
                // Allow fallback to plaintext
            }
        }
    });

    // Decrypt transactions when loaded
    Livewire.hook('message.received', async ({ component }) => {
        if (component.fingerprint.name.includes('transaction')) {
            await decryptTransactions(component);
        }
    });
});

async function decryptTransactions(component) {
    // Find all encrypted transaction elements
    const encryptedTransactions = component.$wire.transactions?.filter(t => t.is_encrypted) || [];

    for (const transaction of encryptedTransactions) {
        if (transaction.description && transaction.description_is_encrypted) {
            try {
                transaction.description = await window.universalEncryption.decryptModelField(
                    'App\\Models\\Transaction',
                    transaction.id,
                    'description',
                    transaction.description
                );
                transaction.decryption_failed = false;
            } catch (error) {
                console.error('Failed to decrypt transaction:', transaction.id, error);
                transaction.description = '[Encrypted - Cannot Decrypt]';
                transaction.decryption_failed = true;
            }
        }
    }
}
```

## 5. Backend API Controllers

### 5.1 Universal Encryption Controller

```php
// app/Http/Controllers/UniversalEncryptionController.php
namespace App\Http\Controllers;

use App\Models\EncryptedDataKey;
use App\Models\EncryptionKey;
use App\Models\Transaction;
use Illuminate\Http\Request;

class UniversalEncryptionController extends Controller
{
    /**
     * Get encryption keys for the authenticated user
     */
    public function getMyKeys()
    {
        $user = auth()->user();

        $keys = EncryptionKey::where([
            'owner_id' => $user->id,
            'owner_type' => get_class($user),
            'is_active' => true
        ])->first();

        if (!$keys) {
            return response()->json(['error' => 'No keys found'], 404);
        }

        return response()->json([
            'public_key' => $keys->public_key,
            'private_key' => decrypt($keys->encrypted_private_key)
        ]);
    }

    /**
     * Store encryption keys
     */
    public function storeKeys(Request $request)
    {
        $user = auth()->user();

        EncryptionKey::updateOrCreate(
            [
                'owner_id' => $user->id,
                'owner_type' => get_class($user),
                'is_active' => true
            ],
            [
                'public_key' => $request->public_key,
                'encrypted_private_key' => encrypt($request->private_key),
                'key_version' => 'v1'
            ]
        );

        return response()->json(['success' => true]);
    }

    /**
     * Get recipients for a model (for encryption)
     */
    public function getRecipients(string $modelType, int $modelId)
    {
        $model = $this->findModel($modelType, $modelId);

        if (!method_exists($model, 'getEncryptionRecipients')) {
            return response()->json(['error' => 'Model does not support encryption'], 400);
        }

        $recipients = $model->getEncryptionRecipients();

        return response()->json(
            collect($recipients)->map(function ($recipient) {
                $key = EncryptionKey::where([
                    'owner_id' => $recipient->id,
                    'owner_type' => get_class($recipient),
                    'is_active' => true
                ])->first();

                return [
                    'id' => $recipient->id,
                    'type' => get_class($recipient),
                    'public_key' => $key->public_key ?? null
                ];
            })->filter(fn($r) => $r['public_key'] !== null)->values()
        );
    }

    /**
     * Get decryption key for a specific model field
     */
    public function getDecryptionKey(string $modelType, int $modelId, string $field)
    {
        $user = auth()->user();
        $model = $this->findModel($modelType, $modelId);

        // Check permission
        if (!method_exists($model, 'canDecrypt') || !$model->canDecrypt($user, $field)) {
            return response()->json(['error' => 'Access denied'], 403);
        }

        $key = EncryptedDataKey::where([
            'encryptable_id' => $modelId,
            'encryptable_type' => $modelType,
            'encryptable_field' => $field,
            'recipient_id' => $user->id,
            'recipient_type' => get_class($user)
        ])->first();

        if (!$key) {
            return response()->json(['error' => 'Key not found'], 404);
        }

        return response()->json([
            'encrypted_data_key' => $key->encrypted_data_key,
            'nonce' => $key->nonce
        ]);
    }

    /**
     * Get transaction recipients (specialized endpoint for transactions)
     */
    public function getTransactionRecipients(Request $request)
    {
        $fromAccountId = $request->from_account_id;
        $toAccountId = $request->to_account_id;

        $recipients = [];

        // Get from account owners
        if ($fromAccountId) {
            $fromAccount = \App\Models\Account::with('accountable')->find($fromAccountId);
            if ($fromAccount && $fromAccount->accountable) {
                $recipients = array_merge($recipients, $fromAccount->accountable->all());
            }
        }

        // Get to account owners
        if ($toAccountId) {
            $toAccount = \App\Models\Account::with('accountable')->find($toAccountId);
            if ($toAccount && $toAccount->accountable) {
                $recipients = array_merge($recipients, $toAccount->accountable->all());
            }
        }

        $recipients = array_unique($recipients, SORT_REGULAR);

        return response()->json(
            collect($recipients)->map(function ($recipient) {
                $key = EncryptionKey::where([
                    'owner_id' => $recipient->id,
                    'owner_type' => get_class($recipient),
                    'is_active' => true
                ])->first();

                return [
                    'id' => $recipient->id,
                    'type' => get_class($recipient),
                    'public_key' => $key->public_key ?? null
                ];
            })->filter(fn($r) => $r['public_key'] !== null)->values()
        );
    }

    /**
     * Store encryption keys for a model field
     */
    public function storeFieldKeys(Request $request)
    {
        $validated = $request->validate([
            'model_type' => 'required|string',
            'model_id' => 'required|integer',
            'field' => 'required|string',
            'recipient_keys' => 'required|array',
            'recipient_keys.*.recipient_id' => 'required|integer',
            'recipient_keys.*.recipient_type' => 'required|string',
            'recipient_keys.*.encrypted_key' => 'required|string',
            'recipient_keys.*.nonce' => 'required|string',
        ]);

        $model = $this->findModel($validated['model_type'], $validated['model_id']);

        if (!method_exists($model, 'storeEncryptionKeys')) {
            return response()->json(['error' => 'Model does not support encryption'], 400);
        }

        $model->storeEncryptionKeys($validated['field'], $validated['recipient_keys']);

        return response()->json(['success' => true]);
    }

    /**
     * Helper to find and authorize model access
     */
    protected function findModel(string $modelType, int $modelId)
    {
        // Whitelist allowed models for security
        $allowedModels = [
            'App\\Models\\Transaction',
            'App\\Models\\Message',
            'App\\Models\\Post', // Future
            // Add more as needed
        ];

        if (!in_array($modelType, $allowedModels)) {
            abort(400, 'Invalid model type');
        }

        $model = $modelType::find($modelId);

        if (!$model) {
            abort(404, 'Model not found');
        }

        return $model;
    }
}
```

### 5.2 Routes

```php
// routes/encryption.php
use App\Http\Controllers\UniversalEncryptionController;

Route::middleware(['auth'])->prefix('encryption')->group(function () {
    // Key management
    Route::post('/store-keys', [UniversalEncryptionController::class, 'storeKeys']);
    Route::get('/my-keys', [UniversalEncryptionController::class, 'getMyKeys']);

    // Universal encryption/decryption
    Route::get('/recipients/{modelType}/{modelId}', [UniversalEncryptionController::class, 'getRecipients']);
    Route::get('/key/{modelType}/{modelId}/{field}', [UniversalEncryptionController::class, 'getDecryptionKey']);
    Route::post('/store-field-keys', [UniversalEncryptionController::class, 'storeFieldKeys']);

    // Transaction-specific helpers
    Route::get('/transaction-recipients', [UniversalEncryptionController::class, 'getTransactionRecipients']);
});
```

## 6. Livewire Components

### 6.1 Transaction Form Component

```php
// app/Livewire/Transactions/CreateTransaction.php
namespace App\Livewire\Transactions;

use App\Models\Transaction;
use Livewire\Component;

class CreateTransaction extends Component
{
    public $from_account_id;
    public $to_account_id;
    public $amount;
    public $description;
    public $encryptionData = null;

    public function save()
    {
        $this->validate([
            'from_account_id' => 'required|exists:accounts,id',
            'to_account_id' => 'required|exists:accounts,id',
            'amount' => 'required|numeric|min:0',
            'description' => 'nullable|string',
        ]);

        // Create transaction
        $transaction = Transaction::create([
            'from_account_id' => $this->from_account_id,
            'to_account_id' => $this->to_account_id,
            'amount' => $this->amount,
            'description' => $this->description, // Already encrypted by frontend
            'creator_user_id' => auth()->id(),
            'is_encrypted' => !empty($this->encryptionData),
            'encryption_version' => 'v1'
        ]);

        // Store encryption keys if encrypted
        if ($this->encryptionData) {
            $transaction->storeEncryptionKeys(
                $this->encryptionData['field'],
                $this->encryptionData['recipient_keys']
            );
        }

        session()->flash('message', 'Transaction created successfully');
        return redirect()->route('transactions.index');
    }

    public function render()
    {
        return view('livewire.transactions.create-transaction');
    }
}
```

### 6.2 Transaction List Component

```php
// app/Livewire/Transactions/TransactionList.php
namespace App\Livewire\Transactions;

use App\Models\Transaction;
use Livewire\Component;
use Livewire\WithPagination;

class TransactionList extends Component
{
    use WithPagination;

    public function render()
    {
        $user = auth()->user();

        // Get user's accounts
        $accountIds = $user->accounts->pluck('id');

        // Get transactions for user's accounts
        $transactions = Transaction::whereIn('from_account_id', $accountIds)
            ->orWhereIn('to_account_id', $accountIds)
            ->with(['accountFrom', 'accountTo'])
            ->orderBy('created_at', 'desc')
            ->paginate(20);

        // Frontend will handle decryption of encrypted descriptions
        return view('livewire.transactions.transaction-list', [
            'transactions' => $transactions
        ]);
    }
}
```

## 7. Blade Templates

### 7.1 Transaction Form

```blade
{{-- resources/views/livewire/transactions/create-transaction.blade.php --}}
<div>
    <form wire:submit.prevent="save" x-data="{ encrypting: false }">
        <div class="mb-4">
            <label>From Account</label>
            <select wire:model="from_account_id" class="form-select">
                <!-- Account options -->
            </select>
        </div>

        <div class="mb-4">
            <label>To Account</label>
            <select wire:model="to_account_id" class="form-select">
                <!-- Account options -->
            </select>
        </div>

        <div class="mb-4">
            <label>Amount</label>
            <input type="number" wire:model="amount" class="form-input">
        </div>

        <div class="mb-4">
            <label>
                Description
                <span class="text-xs text-gray-500" x-show="encrypting">
                    <i class="fas fa-lock"></i> Encrypting...
                </span>
            </label>
            <textarea
                wire:model="description"
                class="form-textarea"
                rows="3"
            ></textarea>
            <p class="text-xs text-gray-600 mt-1">
                <i class="fas fa-lock"></i> Description will be encrypted for transaction participants
            </p>
        </div>

        <button type="submit" class="btn btn-primary">
            Create Transaction
        </button>
    </form>
</div>
```

### 7.2 Transaction List

```blade
{{-- resources/views/livewire/transactions/transaction-list.blade.php --}}
<div x-data="transactionDecryption()">
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>From</th>
                <th>To</th>
                <th>Amount</th>
                <th>Description</th>
            </tr>
        </thead>
        <tbody>
            @foreach($transactions as $transaction)
                <tr x-data="{
                    decrypted: false,
                    decrypting: {{ $transaction->is_encrypted ? 'true' : 'false' }},
                    description: @js($transaction->description)
                }" x-init="if (decrypting) decryptTransaction({{ $transaction->id }}, 'description')">
                    <td>{{ $transaction->created_at->format('Y-m-d') }}</td>
                    <td>{{ $transaction->accountFrom->name }}</td>
                    <td>{{ $transaction->accountTo->name }}</td>
                    <td>{{ $transaction->amount }}</td>
                    <td>
                        @if($transaction->is_encrypted)
                            <span x-show="decrypting && !decrypted">
                                <i class="fas fa-spinner fa-spin"></i> Decrypting...
                            </span>
                            <span x-show="decrypted" x-text="description"></span>
                            <i class="fas fa-lock text-xs text-gray-500 ml-1" title="Encrypted"></i>
                        @else
                            {{ $transaction->description }}
                        @endif
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
</div>

<script>
function transactionDecryption() {
    return {
        async decryptTransaction(transactionId, field) {
            try {
                const decrypted = await window.universalEncryption.decryptModelField(
                    'App\\Models\\Transaction',
                    transactionId,
                    field,
                    this.description
                );
                this.description = decrypted;
                this.decrypted = true;
                this.decrypting = false;
            } catch (error) {
                console.error('Decryption failed:', error);
                this.description = '[Cannot Decrypt]';
                this.decrypted = true;
                this.decrypting = false;
            }
        }
    }
}
</script>
```

## 8. Database Migrations

### 8.1 Create Universal Tables

```php
// database/migrations/YYYY_MM_DD_create_universal_encryption_tables.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        // Universal encryption keys
        Schema::create('encryption_keys', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('owner_id');
            $table->string('owner_type');
            $table->text('public_key');
            $table->text('encrypted_private_key');
            $table->string('key_version')->default('v1');
            $table->boolean('is_active')->default(true);
            $table->timestamps();

            $table->index(['owner_id', 'owner_type']);
            $table->unique(['owner_id', 'owner_type', 'is_active'], 'unique_active_key');
        });

        // Universal encrypted data keys
        Schema::create('encrypted_data_keys', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('encryptable_id');
            $table->string('encryptable_type');
            $table->string('encryptable_field', 100);
            $table->unsignedBigInteger('recipient_id');
            $table->string('recipient_type');
            $table->text('encrypted_data_key');
            $table->string('nonce');
            $table->string('algorithm')->default('AES-256-GCM');
            $table->timestamps();

            $table->index(['encryptable_id', 'encryptable_type', 'encryptable_field'], 'idx_encryptable');
            $table->index(['recipient_id', 'recipient_type'], 'idx_recipient');
            $table->unique([
                'encryptable_id',
                'encryptable_type',
                'encryptable_field',
                'recipient_id',
                'recipient_type'
            ], 'unique_recipient_key');
        });
    }

    public function down()
    {
        Schema::dropIfExists('encrypted_data_keys');
        Schema::dropIfExists('encryption_keys');
    }
};
```

### 8.2 Add Encryption to Transactions

```php
// database/migrations/YYYY_MM_DD_add_encryption_to_transactions.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('transactions', function (Blueprint $table) {
            $table->boolean('is_encrypted')->default(false)->after('description');
            $table->string('encryption_version', 10)->nullable()->after('is_encrypted');
            $table->json('encrypted_fields')->nullable()->after('encryption_version')
                ->comment('JSON array of encrypted field names');

            $table->index('is_encrypted');
        });
    }

    public function down()
    {
        Schema::table('transactions', function (Blueprint $table) {
            $table->dropColumn(['is_encrypted', 'encryption_version', 'encrypted_fields']);
        });
    }
};
```

### 8.3 Add Encryption to WireChat Messages

```php
// database/migrations/YYYY_MM_DD_add_encryption_to_wirechat_messages.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('wirechat_messages', function (Blueprint $table) {
            $table->boolean('is_encrypted')->default(false)->after('body');
            $table->string('encryption_version', 10)->nullable()->after('is_encrypted');
            $table->json('encrypted_fields')->nullable()->after('encryption_version')
                ->comment('JSON array of encrypted field names');

            $table->index('is_encrypted');
        });
    }

    public function down()
    {
        Schema::table('wirechat_messages', function (Blueprint $table) {
            $table->dropColumn(['is_encrypted', 'encryption_version', 'encrypted_fields']);
        });
    }
};
```

## 9. Implementation Timeline

### Phase 1: Universal Foundation (Week 1-2)
- [ ] Create universal encryption tables migrations
- [ ] Create `EncryptionKey` and `EncryptedDataKey` models
- [ ] Create `HasEncryptedFields` trait
- [ ] Create `UniversalEncryptionController`
- [ ] Add encryption routes
- [ ] Write unit tests

### Phase 2: WireChat Integration (Week 2-3)
- [ ] Apply trait to Message model
- [ ] Update WireChat frontend integration
- [ ] Test message encryption/decryption
- [ ] Handle group conversations
- [ ] Test backward compatibility

### Phase 3: Transaction Integration (Week 3-4)
- [ ] Apply trait to Transaction model
- [ ] Create transaction encryption frontend
- [ ] Update transaction forms
- [ ] Update transaction lists
- [ ] Test encryption with from/to accounts
- [ ] Handle Elasticsearch indexing

### Phase 4: Frontend Universal Module (Week 4-5)
- [ ] Complete `universal-encryption.js`
- [ ] Create helper functions
- [ ] Add error handling
- [ ] Add loading states
- [ ] Performance optimization
- [ ] Browser compatibility testing

### Phase 5: Testing & Polish (Week 5-6)
- [ ] End-to-end testing
- [ ] Multi-user testing
- [ ] Permission testing
- [ ] Performance testing
- [ ] Security audit
- [ ] Documentation

### Phase 6: Deployment (Week 6-7)
- [ ] Gradual rollout with feature flag
- [ ] Monitor for errors
- [ ] User communication
- [ ] Admin training
- [ ] Performance monitoring

## 10. Adding Encryption to New Models (Future)

### Example: Encrypting Post Content

```php
// app/Models/Post.php
use App\Traits\HasEncryptedFields;

class Post extends Model
{
    use HasEncryptedFields;

    protected $encryptable = ['content', 'private_notes'];

    protected function getEncryptionRecipients(): array
    {
        $recipients = [$this->user]; // Post author

        // Add moderators if needed
        if ($this->is_private) {
            $moderators = User::role('moderator')->get();
            $recipients = array_merge($recipients, $moderators->all());
        }

        return $recipients;
    }
}
```

Then run migration:
```php
Schema::table('posts', function (Blueprint $table) {
    $table->boolean('is_encrypted')->default(false);
    $table->string('encryption_version', 10)->nullable();
    $table->json('encrypted_fields')->nullable();
});
```

Frontend automatically works:
```javascript
// Encryption
const encrypted = await window.universalEncryption.encryptModelField(
    'App\\Models\\Post',
    postId,
    'content',
    contentValue
);

// Decryption
const decrypted = await window.universalEncryption.decryptModelField(
    'App\\Models\\Post',
    postId,
    'content',
    encryptedContent
);
```

## 11. Configuration

```php
// config/encryption.php
return [
    'enabled' => env('ENCRYPTION_ENABLED', false),
    'enforce' => env('ENCRYPTION_ENFORCE', false),

    // Which models have encryption enabled
    'models' => [
        'messages' => env('ENCRYPT_MESSAGES', true),
        'transactions' => env('ENCRYPT_TRANSACTIONS', true),
        'posts' => env('ENCRYPT_POSTS', false),
    ],

    // Algorithm settings
    'rsa_key_size' => 2048,
    'aes_key_size' => 256,
    'algorithm' => 'AES-256-GCM',

    // Performance
    'cache_keys' => true,
    'cache_duration' => 3600, // 1 hour
];
```

## 12. Security Considerations

### 12.1 Transaction-Specific Concerns

**Elasticsearch Indexing:**
- Encrypted descriptions not searchable
- Mark as `[Encrypted]` in index
- Consider separate plaintext "public description" field if search needed

**Database Constraints:**
- Transaction table has UPDATE/DELETE restrictions
- Encryption metadata immutable after creation
- Audit all changes

**Multi-Account Transactions:**
- Organization accounts may have multiple users
- All account owners get decryption keys
- Consider role-based access within organizations

### 12.2 General Best Practices

✅ **DO:**
- Validate recipients before encrypting
- Log all encryption/decryption attempts
- Regular key rotation policy
- Monitor failed decryption attempts
- Backup encryption keys securely

❌ **DON'T:**
- Store plaintext alongside encrypted
- Log decrypted content
- Allow encryption without recipients
- Expose decryption keys in API responses
- Cache decrypted data client-side

## 13. Advantages of Universal System

| Aspect | Benefit |
|--------|---------|
| **Code Reusability** | Single trait for all models |
| **Consistency** | Same encryption everywhere |
| **Maintainability** | One place to fix bugs |
| **Extensibility** | Add new models easily |
| **Testing** | Test once, works everywhere |
| **Security** | Consistent security model |
| **Performance** | Shared key cache |

## 14. Migration from Separate Systems

If you implemented WireChat encryption separately, migration is straightforward:

1. Create universal tables
2. Migrate existing `user_encryption_keys` → `encryption_keys`
3. Migrate existing `message_encryption_keys` → `encrypted_data_keys`
4. Update foreign keys
5. Test thoroughly
6. Drop old tables

No data loss, just schema unification.

## 15. Conclusion

This universal encryption system provides:

✅ **Single Source of Truth** - One encryption system for everything
✅ **Easy Extension** - Add encryption to any model with one trait
✅ **Consistent Security** - Same strong encryption everywhere
✅ **Future-Proof** - Built for growth
✅ **Maintainable** - Less code duplication
✅ **Performant** - Shared infrastructure and caching

**Next Steps:**
1. Review and approve this plan
2. Begin Phase 1 implementation
3. Test with WireChat first
4. Add transactions
5. Expand to other models as needed

---

**Document Version:** 1.0
**Created:** 2025-11-30
**Extends:** e2e-encryption-simplified-plan.md
**For:** Timebank.cc Universal Encryption System
