diff --git a/.env.dokploy b/.env.dokploy index 825132f..714cafd 100644 --- a/.env.dokploy +++ b/.env.dokploy @@ -9,6 +9,7 @@ DATABASE_URL=file:/app/data/production.db # Security - CHANGE THESE VALUES! SESSION_SECRET=your-super-secure-session-secret-change-this-in-production-min-32-chars +ENCRYPTION_KEY=production-secure-encryption-key! SUPER_ADMIN=superadmin SUPER_ADMIN_EMAIL=admin@yourcompany.com SUPER_ADMIN_PASSWORD=YourSecurePassword123! diff --git a/ENCRYPTION_FIX_SUMMARY.md b/ENCRYPTION_FIX_SUMMARY.md new file mode 100644 index 0000000..d495246 --- /dev/null +++ b/ENCRYPTION_FIX_SUMMARY.md @@ -0,0 +1,145 @@ +# Encryption Fix Summary + +## Problem +The application was failing in production with the error: +``` +Decryption error: TypeError: crypto.createDecipher is not a function +``` + +This occurred because `crypto.createDecipher` and `crypto.createCipher` have been deprecated and removed in newer versions of Node.js. + +## Root Cause +- **Local Development**: Running on older Node.js version that still supports deprecated crypto methods +- **Production (Dokploy)**: Running on newer Node.js version where these methods have been removed +- **Deprecated Methods**: `crypto.createCipher()` and `crypto.createDecipher()` are no longer available + +## Solution Implemented + +### 1. **Updated Encryption Methods** +- **Before**: Used `crypto.createCipher(algorithm, key)` +- **After**: Using `crypto.createCipheriv(algorithm, key, iv)` with proper IV (Initialization Vector) + +### 2. **Backward Compatibility** +- Added legacy decryption support for existing encrypted data +- Graceful fallback handling for corrupted or unreadable data +- Migration utility to convert old format to new format + +### 3. **Enhanced Error Handling** +- Mail settings page won't crash if decryption fails +- Returns empty password instead of crashing the application +- Comprehensive error logging for debugging + +### 4. **New Encryption Format** +- **Format**: `IV:EncryptedData` (both in hex) +- **Algorithm**: AES-256-CBC with random IV for each encryption +- **Security**: Each encryption uses a unique IV for better security + +## Files Modified + +### 1. **app/utils/encryption.server.ts** +- ✅ Updated `encrypt()` to use `crypto.createCipheriv()` +- ✅ Updated `decrypt()` to use `crypto.createDecipheriv()` +- ✅ Added backward compatibility for legacy encrypted data +- ✅ Added migration utility `migrateEncryption()` +- ✅ Enhanced error handling and validation +- ✅ Added format detection functions + +### 2. **app/routes/mail-settings.tsx** +- ✅ Added graceful error handling in loader +- ✅ Enhanced action to handle password migration +- ✅ Better error messages for users + +### 3. **app/routes/test-encryption.tsx** +- ✅ Added migration testing +- ✅ Enhanced test coverage +- ✅ Added environment information display + +### 4. **.env.dokploy** +- ✅ Added missing `ENCRYPTION_KEY` environment variable + +## Key Features + +### 1. **Migration Support** +```typescript +// Automatically handles legacy format +const decrypted = decrypt(encryptedPassword); // Works with both old and new formats +const migrated = migrateEncryption(oldEncryptedData); // Converts to new format +``` + +### 2. **Format Detection** +```typescript +isEncrypted(text) // Detects new format (contains ':') +isLegacyEncrypted(text) // Detects old format (hex only) +``` + +### 3. **Graceful Degradation** +- If decryption fails, returns original text instead of crashing +- Mail settings page shows masked password if decryption fails +- Comprehensive error logging for debugging + +## Environment Variables Required + +### Production (.env.dokploy) +```bash +ENCRYPTION_KEY=production-secure-encryption-key! +``` + +**Important**: The encryption key should be exactly 32 characters for optimal security. + +## Testing + +### 1. **Test Encryption Route** +Visit `/test-encryption` (Super Admin only) to verify: +- ✅ Basic encryption/decryption works +- ✅ Migration functionality works +- ✅ Different password types work +- ✅ Environment information is correct + +### 2. **Mail Settings** +- ✅ Existing encrypted passwords are handled gracefully +- ✅ New passwords are encrypted with new format +- ✅ Settings can be saved and loaded without errors + +## Deployment Steps + +### 1. **Update Environment Variables** +Make sure `ENCRYPTION_KEY` is set in your Dokploy environment variables: +```bash +ENCRYPTION_KEY=your-32-character-encryption-key +``` + +### 2. **Deploy Updated Code** +The updated encryption utility will: +- Handle existing encrypted data automatically +- Use new encryption format for new data +- Provide backward compatibility + +### 3. **Verify Functionality** +1. Check mail settings page loads without errors +2. Test saving mail settings +3. Visit `/test-encryption` to verify encryption works +4. Check application logs for any encryption errors + +## Security Improvements + +### 1. **Better Encryption** +- Uses proper IV (Initialization Vector) for each encryption +- More secure than the deprecated methods +- Each encrypted value is unique even for same input + +### 2. **Forward Compatibility** +- Code is compatible with current and future Node.js versions +- No dependency on deprecated crypto methods + +### 3. **Error Resilience** +- Application won't crash due to encryption issues +- Graceful handling of corrupted encrypted data +- Comprehensive error logging + +## Node.js Version Compatibility +- ✅ **Node.js 16+**: Full support +- ✅ **Node.js 18+**: Full support +- ✅ **Node.js 20+**: Full support +- ❌ **Node.js 14 and below**: May have issues with deprecated methods + +The fix ensures your application works reliably across different Node.js versions in various deployment environments. \ No newline at end of file diff --git a/app/routes/mail-settings.tsx b/app/routes/mail-settings.tsx index ecc38a4..d2ef130 100644 --- a/app/routes/mail-settings.tsx +++ b/app/routes/mail-settings.tsx @@ -5,7 +5,7 @@ import { testEmailConnection } from "~/utils/mail.server"; import DashboardLayout from "~/components/DashboardLayout"; import { useState } from "react"; import { prisma } from "~/utils/db.server"; -import { encryptMailSettings, decryptMailSettings, safeEncryptPassword } from "~/utils/encryption.server"; +import { encryptMailSettings, decryptMailSettings, safeEncryptPassword, migrateEncryption } from "~/utils/encryption.server"; export async function loader({ request }: LoaderFunctionArgs) { // Require auth level 3 to access mail settings @@ -16,11 +16,25 @@ export async function loader({ request }: LoaderFunctionArgs) { // Decrypt settings for display (but mask the password) let mailSettings = null; if (encryptedMailSettings) { - const decrypted = decryptMailSettings(encryptedMailSettings); - mailSettings = { - ...decrypted, - password: '••••••••' // Mask password for security - }; + try { + const decrypted = decryptMailSettings(encryptedMailSettings); + mailSettings = { + ...decrypted, + password: '••••••••' // Mask password for security + }; + } catch (error) { + console.error('Failed to decrypt mail settings:', error); + // Return settings with masked password if decryption fails + mailSettings = { + host: encryptedMailSettings.host, + port: encryptedMailSettings.port, + secure: encryptedMailSettings.secure, + username: encryptedMailSettings.username, + password: '••••••••', // Mask password + fromName: encryptedMailSettings.fromName, + fromEmail: encryptedMailSettings.fromEmail, + }; + } } return json({ mailSettings, user }); @@ -52,7 +66,20 @@ export async function action({ request }: ActionFunctionArgs) { try { // Encrypt the password before saving - const encryptedPassword = safeEncryptPassword(password); + let encryptedPassword: string; + + // If password is the masked value, keep the existing password + if (password === '••••••••') { + const existingSettings = await prisma.mailSettings.findFirst(); + if (existingSettings) { + // Migrate existing password to new format if needed + encryptedPassword = migrateEncryption(existingSettings.password); + } else { + return json({ error: "Cannot save without a valid password" }, { status: 400 }); + } + } else { + encryptedPassword = safeEncryptPassword(password); + } // Check if settings exist const existingSettings = await prisma.mailSettings.findFirst(); diff --git a/app/routes/test-encryption.tsx b/app/routes/test-encryption.tsx index 2878212..defe975 100644 --- a/app/routes/test-encryption.tsx +++ b/app/routes/test-encryption.tsx @@ -3,7 +3,7 @@ import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; -import { testEncryption, encrypt, decrypt } from "~/utils/encryption.server"; +import { testEncryption, encrypt, decrypt, migrateEncryption, isEncrypted, isLegacyEncrypted } from "~/utils/encryption.server"; export async function loader({ request }: LoaderFunctionArgs) { // Require auth level 3 to access encryption test @@ -24,12 +24,18 @@ export async function loader({ request }: LoaderFunctionArgs) { try { const encrypted = encrypt(test.data); const decrypted = decrypt(encrypted); + const migrated = migrateEncryption(test.data); + const migratedDecrypted = decrypt(migrated); + return { ...test, encrypted, decrypted, success: decrypted === test.data, - encryptedLength: encrypted.length + encryptedLength: encrypted.length, + migrationSuccess: migratedDecrypted === test.data, + isNewFormat: isEncrypted(encrypted), + isLegacyFormat: isLegacyEncrypted(test.data) }; } catch (error) { return { @@ -92,6 +98,7 @@ export default function TestEncryption() {