1487 lines
45 KiB
Markdown
1487 lines
45 KiB
Markdown
# 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
|