# 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 --}}

Description will be encrypted for transaction participants

``` ### 7.2 Transaction List ```blade {{-- resources/views/livewire/transactions/transaction-list.blade.php --}}
@foreach($transactions as $transaction) @endforeach
Date From To Amount Description
{{ $transaction->created_at->format('Y-m-d') }} {{ $transaction->accountFrom->name }} {{ $transaction->accountTo->name }} {{ $transaction->amount }} @if($transaction->is_encrypted) Decrypting... @else {{ $transaction->description }} @endif
``` ## 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