This commit is contained in:
yznahmad 2025-08-01 05:51:08 +03:00
parent 17f1acfbb0
commit 941dde2154
6 changed files with 331 additions and 30 deletions

View File

@ -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!

145
ENCRYPTION_FIX_SUMMARY.md Normal file
View File

@ -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.

View File

@ -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();

View File

@ -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() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Original</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Encrypted Length</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Match</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Migration</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
@ -119,6 +126,12 @@ export default function TestEncryption() {
{test.success ? '✅' : '❌'}
{test.error && <div className="text-red-500 text-xs mt-1">{test.error}</div>}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{test.migrationSuccess ? '✅' : '❌'}
<div className="text-xs text-gray-500 mt-1">
Format: {test.isNewFormat ? 'New' : test.isLegacyFormat ? 'Legacy' : 'Plain'}
</div>
</td>
</tr>
))}
</tbody>
@ -132,11 +145,23 @@ export default function TestEncryption() {
<ul className="text-sm text-yellow-700 space-y-1">
<li> Passwords are encrypted using AES-256-CBC algorithm</li>
<li> Each encryption uses a random IV (Initialization Vector)</li>
<li> Encrypted data format: IV:EncryptedData</li>
<li> Encrypted data format: IV:EncryptedData (hex)</li>
<li> Migration support for legacy encrypted data</li>
<li> Encryption key should be set via ENCRYPTION_KEY environment variable</li>
<li> This test page should only be accessible to Super Admins</li>
</ul>
</div>
{/* Node.js Version Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
<h3 className="text-lg font-semibold text-blue-800 mb-2">Environment Info</h3>
<div className="text-sm text-blue-700 space-y-1">
<div> Node.js Version: {process.version}</div>
<div> Platform: {process.platform}</div>
<div> Architecture: {process.arch}</div>
<div> Encryption Format: New (createCipheriv/createDecipheriv)</div>
</div>
</div>
</div>
</DashboardLayout>
);

View File

@ -22,7 +22,7 @@ export function encrypt(text: string): string {
try {
const key = getEncryptionKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipher(ALGORITHM, key);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
@ -45,23 +45,44 @@ export function decrypt(encryptedText: string): string {
try {
const key = getEncryptionKey();
const textParts = encryptedText.split(':');
if (textParts.length !== 2) {
throw new Error('Invalid encrypted text format');
// Check if this is the new format with IV
if (isEncrypted(encryptedText)) {
const textParts = encryptedText.split(':');
if (textParts.length !== 2) {
throw new Error('Invalid encrypted text format');
}
const iv = Buffer.from(textParts[0], 'hex');
const encryptedData = textParts[1];
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Check if this is legacy format
else if (isLegacyEncrypted(encryptedText)) {
try {
return legacyDecrypt(encryptedText);
} catch (legacyError) {
console.warn('Failed to decrypt legacy format, treating as plain text:', legacyError);
// If we can't decrypt legacy format, assume it's plain text
return encryptedText;
}
}
// If it doesn't match any encrypted format, assume it's plain text
else {
return encryptedText;
}
const iv = Buffer.from(textParts[0], 'hex');
const encryptedData = textParts[1];
const decipher = crypto.createDecipher(ALGORITHM, key);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('Decryption error:', error);
throw new Error('Failed to decrypt data');
// If decryption fails completely, return the original text
// This prevents the app from crashing if there's corrupted data
console.warn('Returning original text due to decryption failure');
return encryptedText;
}
}
@ -99,10 +120,44 @@ export function decryptMailSettings(settings: {
fromName: string;
fromEmail: string;
}) {
return {
...settings,
password: decrypt(settings.password)
};
try {
return {
...settings,
password: decrypt(settings.password)
};
} catch (error) {
console.error('Failed to decrypt mail settings password:', error);
// Return settings with empty password if decryption fails
// This prevents the app from crashing
return {
...settings,
password: ''
};
}
}
/**
* Legacy decrypt function for backward compatibility with old encryption method
* @param encryptedText - The encrypted text
* @returns Decrypted plain text string
*/
function legacyDecrypt(encryptedText: string): string {
try {
const key = getEncryptionKey();
// Try to use the old createDecipher method if available (for local development)
// This is a fallback for data encrypted with the old method
if (typeof (crypto as any).createDecipher === 'function') {
const decipher = (crypto as any).createDecipher(ALGORITHM, key);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} else {
// If old method is not available, we can't decrypt old format
throw new Error('Cannot decrypt legacy format in this Node.js version');
}
} catch (error) {
throw error;
}
}
/**
@ -114,6 +169,16 @@ export function isEncrypted(text: string): boolean {
return text.includes(':') && text.split(':').length === 2;
}
/**
* Checks if encrypted text is in legacy format (no IV separator)
* @param text - Text to check
* @returns True if text appears to be in legacy format
*/
export function isLegacyEncrypted(text: string): boolean {
// Legacy format doesn't have ':' separator or has different pattern
return !text.includes(':') && text.length > 0 && /^[a-f0-9]+$/i.test(text);
}
/**
* Safely encrypts a password only if it's not already encrypted
* @param password - Password to encrypt
@ -126,6 +191,42 @@ export function safeEncryptPassword(password: string): string {
return encrypt(password);
}
/**
* Migrates legacy encrypted data to new format
* @param encryptedText - Potentially legacy encrypted text
* @returns Re-encrypted text in new format
*/
export function migrateEncryption(encryptedText: string): string {
if (!encryptedText) return '';
try {
// If it's already in new format, return as-is
if (isEncrypted(encryptedText)) {
// Verify it can be decrypted and re-encrypt to ensure consistency
const decrypted = decrypt(encryptedText);
return encrypt(decrypted);
}
// If it's in legacy format, decrypt and re-encrypt
if (isLegacyEncrypted(encryptedText)) {
const decrypted = legacyDecrypt(encryptedText);
return encrypt(decrypted);
}
// If it's plain text, encrypt it
return encrypt(encryptedText);
} catch (error) {
console.error('Migration error:', error);
// If migration fails, try to encrypt as plain text
try {
return encrypt(encryptedText);
} catch (encryptError) {
console.error('Failed to encrypt during migration:', encryptError);
return encryptedText;
}
}
}
/**
* Test encryption/decryption functionality
* @param testText - Text to test with
@ -141,12 +242,14 @@ export function testEncryption(testText: string = 'test-password-123') {
original: testText,
encrypted: encrypted,
decrypted: decrypted,
isValid: decrypted === testText
isValid: decrypted === testText,
format: 'new'
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
error: error instanceof Error ? error.message : 'Unknown error',
format: 'unknown'
};
}
}

Binary file not shown.