w4wvvv
This commit is contained in:
parent
17f1acfbb0
commit
941dde2154
@ -9,6 +9,7 @@ DATABASE_URL=file:/app/data/production.db
|
|||||||
|
|
||||||
# Security - CHANGE THESE VALUES!
|
# Security - CHANGE THESE VALUES!
|
||||||
SESSION_SECRET=your-super-secure-session-secret-change-this-in-production-min-32-chars
|
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=superadmin
|
||||||
SUPER_ADMIN_EMAIL=admin@yourcompany.com
|
SUPER_ADMIN_EMAIL=admin@yourcompany.com
|
||||||
SUPER_ADMIN_PASSWORD=YourSecurePassword123!
|
SUPER_ADMIN_PASSWORD=YourSecurePassword123!
|
||||||
|
|||||||
145
ENCRYPTION_FIX_SUMMARY.md
Normal file
145
ENCRYPTION_FIX_SUMMARY.md
Normal 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.
|
||||||
@ -5,7 +5,7 @@ import { testEmailConnection } from "~/utils/mail.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { prisma } from "~/utils/db.server";
|
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) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
// Require auth level 3 to access mail settings
|
// 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)
|
// Decrypt settings for display (but mask the password)
|
||||||
let mailSettings = null;
|
let mailSettings = null;
|
||||||
if (encryptedMailSettings) {
|
if (encryptedMailSettings) {
|
||||||
|
try {
|
||||||
const decrypted = decryptMailSettings(encryptedMailSettings);
|
const decrypted = decryptMailSettings(encryptedMailSettings);
|
||||||
mailSettings = {
|
mailSettings = {
|
||||||
...decrypted,
|
...decrypted,
|
||||||
password: '••••••••' // Mask password for security
|
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 });
|
return json({ mailSettings, user });
|
||||||
@ -52,7 +66,20 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Encrypt the password before saving
|
// 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
|
// Check if settings exist
|
||||||
const existingSettings = await prisma.mailSettings.findFirst();
|
const existingSettings = await prisma.mailSettings.findFirst();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { json } from "@remix-run/node";
|
|||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import { requireAuthLevel } from "~/utils/auth.server";
|
import { requireAuthLevel } from "~/utils/auth.server";
|
||||||
import DashboardLayout from "~/components/DashboardLayout";
|
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) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
// Require auth level 3 to access encryption test
|
// Require auth level 3 to access encryption test
|
||||||
@ -24,12 +24,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
try {
|
try {
|
||||||
const encrypted = encrypt(test.data);
|
const encrypted = encrypt(test.data);
|
||||||
const decrypted = decrypt(encrypted);
|
const decrypted = decrypt(encrypted);
|
||||||
|
const migrated = migrateEncryption(test.data);
|
||||||
|
const migratedDecrypted = decrypt(migrated);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...test,
|
...test,
|
||||||
encrypted,
|
encrypted,
|
||||||
decrypted,
|
decrypted,
|
||||||
success: decrypted === test.data,
|
success: decrypted === test.data,
|
||||||
encryptedLength: encrypted.length
|
encryptedLength: encrypted.length,
|
||||||
|
migrationSuccess: migratedDecrypted === test.data,
|
||||||
|
isNewFormat: isEncrypted(encrypted),
|
||||||
|
isLegacyFormat: isLegacyEncrypted(test.data)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
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">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">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">Match</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Migration</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
@ -119,6 +126,12 @@ export default function TestEncryption() {
|
|||||||
{test.success ? '✅' : '❌'}
|
{test.success ? '✅' : '❌'}
|
||||||
{test.error && <div className="text-red-500 text-xs mt-1">{test.error}</div>}
|
{test.error && <div className="text-red-500 text-xs mt-1">{test.error}</div>}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -132,11 +145,23 @@ export default function TestEncryption() {
|
|||||||
<ul className="text-sm text-yellow-700 space-y-1">
|
<ul className="text-sm text-yellow-700 space-y-1">
|
||||||
<li>• Passwords are encrypted using AES-256-CBC algorithm</li>
|
<li>• Passwords are encrypted using AES-256-CBC algorithm</li>
|
||||||
<li>• Each encryption uses a random IV (Initialization Vector)</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>• Encryption key should be set via ENCRYPTION_KEY environment variable</li>
|
||||||
<li>• This test page should only be accessible to Super Admins</li>
|
<li>• This test page should only be accessible to Super Admins</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export function encrypt(text: string): string {
|
|||||||
try {
|
try {
|
||||||
const key = getEncryptionKey();
|
const key = getEncryptionKey();
|
||||||
const iv = crypto.randomBytes(IV_LENGTH);
|
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');
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
encrypted += cipher.final('hex');
|
encrypted += cipher.final('hex');
|
||||||
@ -45,6 +45,9 @@ export function decrypt(encryptedText: string): string {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const key = getEncryptionKey();
|
const key = getEncryptionKey();
|
||||||
|
|
||||||
|
// Check if this is the new format with IV
|
||||||
|
if (isEncrypted(encryptedText)) {
|
||||||
const textParts = encryptedText.split(':');
|
const textParts = encryptedText.split(':');
|
||||||
|
|
||||||
if (textParts.length !== 2) {
|
if (textParts.length !== 2) {
|
||||||
@ -54,14 +57,32 @@ export function decrypt(encryptedText: string): string {
|
|||||||
const iv = Buffer.from(textParts[0], 'hex');
|
const iv = Buffer.from(textParts[0], 'hex');
|
||||||
const encryptedData = textParts[1];
|
const encryptedData = textParts[1];
|
||||||
|
|
||||||
const decipher = crypto.createDecipher(ALGORITHM, key);
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||||
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
||||||
decrypted += decipher.final('utf8');
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
return decrypted;
|
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;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Decryption error:', 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;
|
fromName: string;
|
||||||
fromEmail: string;
|
fromEmail: string;
|
||||||
}) {
|
}) {
|
||||||
|
try {
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
password: decrypt(settings.password)
|
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;
|
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
|
* Safely encrypts a password only if it's not already encrypted
|
||||||
* @param password - Password to encrypt
|
* @param password - Password to encrypt
|
||||||
@ -126,6 +191,42 @@ export function safeEncryptPassword(password: string): string {
|
|||||||
return encrypt(password);
|
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
|
* Test encryption/decryption functionality
|
||||||
* @param testText - Text to test with
|
* @param testText - Text to test with
|
||||||
@ -141,12 +242,14 @@ export function testEncryption(testText: string = 'test-password-123') {
|
|||||||
original: testText,
|
original: testText,
|
||||||
encrypted: encrypted,
|
encrypted: encrypted,
|
||||||
decrypted: decrypted,
|
decrypted: decrypted,
|
||||||
isValid: decrypted === testText
|
isValid: decrypted === testText,
|
||||||
|
format: 'new'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error'
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
format: 'unknown'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user