438 lines
13 KiB
JavaScript
438 lines
13 KiB
JavaScript
/**
|
|
* Encryption Management API
|
|
* CWE-311: Encrypt Sensitive Data
|
|
*
|
|
* Provides endpoints for:
|
|
* - Checking encryption status
|
|
* - Migrating plaintext data to encrypted format
|
|
* - Key rotation
|
|
* - Encryption health monitoring
|
|
*/
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
|
const { readLimiter, modifyLimiter } = require('../middleware/rateLimiter');
|
|
const { db } = require('../database/db');
|
|
const encryption = require('../utils/encryption');
|
|
const logger = require('../utils/logger');
|
|
const SecurityAuditLogger = require('../utils/securityAudit');
|
|
|
|
/**
|
|
* GET /api/encryption/status
|
|
* Get encryption configuration and health status
|
|
*/
|
|
router.get('/status', authenticate, requireAdmin, readLimiter, async (req, res) => {
|
|
try {
|
|
const status = encryption.getEncryptionStatus();
|
|
|
|
// Count encrypted vs unencrypted sensitive data
|
|
const stats = await getEncryptionStats();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
...status,
|
|
statistics: stats
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error getting encryption status:', error);
|
|
res.status(500).json({ error: 'Failed to get encryption status' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/encryption/scan
|
|
* Scan database for sensitive unencrypted data
|
|
*/
|
|
router.get('/scan', authenticate, requireAdmin, readLimiter, async (req, res) => {
|
|
try {
|
|
const findings = await scanForUnencryptedData();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
findings,
|
|
totalIssues: findings.reduce((sum, f) => sum + f.count, 0),
|
|
recommendation: findings.length > 0
|
|
? 'Run migration to encrypt sensitive data'
|
|
: 'All sensitive data is encrypted'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error scanning for unencrypted data:', error);
|
|
res.status(500).json({ error: 'Failed to scan database' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/encryption/migrate
|
|
* Migrate unencrypted sensitive data to encrypted format
|
|
*/
|
|
router.post('/migrate', authenticate, requireAdmin, modifyLimiter, async (req, res) => {
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
try {
|
|
logger.info('Starting encryption migration...');
|
|
|
|
const results = {
|
|
settings: 0,
|
|
vpnConfigs: 0,
|
|
apiTokens: 0,
|
|
twoFactorSecrets: 0,
|
|
errors: []
|
|
};
|
|
|
|
// Migrate settings (sensitive keys only)
|
|
try {
|
|
const sensitiveKeys = ['api_key', 'api_secret', 'vpn_password', 'smtp_password'];
|
|
const settingsResult = await migrateSettings(sensitiveKeys);
|
|
results.settings = settingsResult.migrated;
|
|
if (settingsResult.error) results.errors.push(settingsResult.error);
|
|
} catch (error) {
|
|
results.errors.push(`Settings migration error: ${error.message}`);
|
|
}
|
|
|
|
// Migrate VPN configs
|
|
try {
|
|
const vpnResult = await migrateVPNConfigs();
|
|
results.vpnConfigs = vpnResult.migrated;
|
|
if (vpnResult.error) results.errors.push(vpnResult.error);
|
|
} catch (error) {
|
|
results.errors.push(`VPN migration error: ${error.message}`);
|
|
}
|
|
|
|
// Migrate API tokens (if any exist in plaintext)
|
|
try {
|
|
const tokenResult = await migrateAPITokens();
|
|
results.apiTokens = tokenResult.migrated;
|
|
if (tokenResult.error) results.errors.push(tokenResult.error);
|
|
} catch (error) {
|
|
results.errors.push(`API tokens migration error: ${error.message}`);
|
|
}
|
|
|
|
// Note: 2FA secrets are NOT migrated as they're already handled securely
|
|
// Passwords are bcrypt hashed (not encrypted), which is correct
|
|
|
|
const totalMigrated = results.settings + results.vpnConfigs + results.apiTokens;
|
|
|
|
// Log admin activity
|
|
await SecurityAuditLogger.logAdminActivity(req.user.userId, 'encryption_migration', {
|
|
ip,
|
|
userAgent,
|
|
results: {
|
|
totalMigrated,
|
|
...results
|
|
}
|
|
});
|
|
|
|
logger.info('Encryption migration completed', results);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Encryption migration completed',
|
|
data: {
|
|
totalMigrated,
|
|
...results
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error during encryption migration:', error);
|
|
res.status(500).json({ error: 'Encryption migration failed' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/encryption/verify
|
|
* Verify encrypted data integrity
|
|
*/
|
|
router.post('/verify', authenticate, requireAdmin, readLimiter, async (req, res) => {
|
|
try {
|
|
const results = await verifyEncryptedData();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: results
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error verifying encrypted data:', error);
|
|
res.status(500).json({ error: 'Verification failed' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Helper: Get encryption statistics
|
|
*/
|
|
async function getEncryptionStats() {
|
|
return new Promise((resolve, reject) => {
|
|
db.serialize(() => {
|
|
const stats = {
|
|
vpnConfigs: { total: 0, encrypted: 0 },
|
|
settings: { total: 0, encrypted: 0 },
|
|
twoFactorSecrets: { total: 0, encrypted: 0 }
|
|
};
|
|
|
|
// Count VPN configs
|
|
db.get('SELECT COUNT(*) as total FROM vpn_configs', [], (err, row) => {
|
|
if (!err && row) {
|
|
stats.vpnConfigs.total = row.total;
|
|
|
|
// Check how many are encrypted (encrypted format has 4 colon-separated parts)
|
|
db.get(
|
|
`SELECT COUNT(*) as encrypted FROM vpn_configs
|
|
WHERE config_data LIKE '%:%:%:%'`,
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row) stats.vpnConfigs.encrypted = row.encrypted;
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
// Count settings with sensitive data
|
|
db.get(
|
|
`SELECT COUNT(*) as total FROM settings
|
|
WHERE key IN ('api_key', 'api_secret', 'vpn_password', 'smtp_password')`,
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row) {
|
|
stats.settings.total = row.total;
|
|
|
|
// Count encrypted ones
|
|
db.get(
|
|
`SELECT COUNT(*) as encrypted FROM settings
|
|
WHERE key IN ('api_key', 'api_secret', 'vpn_password', 'smtp_password')
|
|
AND value LIKE '%:%:%:%'`,
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row) stats.settings.encrypted = row.encrypted;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
);
|
|
|
|
// Count 2FA secrets (these should always be protected, not necessarily encrypted)
|
|
db.get(
|
|
'SELECT COUNT(*) as total FROM users WHERE two_factor_secret IS NOT NULL',
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row) {
|
|
stats.twoFactorSecrets.total = row.total;
|
|
stats.twoFactorSecrets.encrypted = row.total; // Consider all protected
|
|
}
|
|
|
|
// Wait a bit for async queries to complete
|
|
setTimeout(() => resolve(stats), 100);
|
|
}
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Scan for unencrypted sensitive data
|
|
*/
|
|
async function scanForUnencryptedData() {
|
|
const findings = [];
|
|
|
|
return new Promise((resolve, reject) => {
|
|
db.serialize(() => {
|
|
// Check VPN configs
|
|
db.get(
|
|
`SELECT COUNT(*) as count FROM vpn_configs
|
|
WHERE config_data NOT LIKE '%:%:%:%' OR config_data IS NULL`,
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row && row.count > 0) {
|
|
findings.push({
|
|
table: 'vpn_configs',
|
|
field: 'config_data',
|
|
count: row.count,
|
|
severity: 'high',
|
|
description: 'VPN configuration files contain credentials and private keys'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Check sensitive settings
|
|
db.get(
|
|
`SELECT COUNT(*) as count FROM settings
|
|
WHERE key IN ('api_key', 'api_secret', 'vpn_password', 'smtp_password')
|
|
AND (value NOT LIKE '%:%:%:%' OR value IS NULL)`,
|
|
[],
|
|
(err, row) => {
|
|
if (!err && row && row.count > 0) {
|
|
findings.push({
|
|
table: 'settings',
|
|
field: 'value',
|
|
count: row.count,
|
|
severity: 'medium',
|
|
description: 'Settings may contain API keys, passwords, and secrets'
|
|
});
|
|
}
|
|
|
|
// Wait for async queries to complete
|
|
setTimeout(() => resolve(findings), 100);
|
|
}
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Migrate settings to encrypted format
|
|
*/
|
|
async function migrateSettings(sensitiveKeys) {
|
|
return new Promise((resolve) => {
|
|
db.all(
|
|
`SELECT id, user_id, key, value FROM settings
|
|
WHERE key IN (${sensitiveKeys.map(() => '?').join(',')})
|
|
AND value IS NOT NULL
|
|
AND value NOT LIKE '%:%:%:%'`,
|
|
sensitiveKeys,
|
|
async (err, settings) => {
|
|
if (err || !settings || settings.length === 0) {
|
|
return resolve({ migrated: 0, error: err ? err.message : null });
|
|
}
|
|
|
|
let migrated = 0;
|
|
for (const setting of settings) {
|
|
try {
|
|
const encrypted = encryption.encryptSetting(setting.value, setting.key);
|
|
|
|
await new Promise((res, rej) => {
|
|
db.run(
|
|
'UPDATE settings SET value = ? WHERE id = ?',
|
|
[encrypted, setting.id],
|
|
(err) => (err ? rej(err) : res())
|
|
);
|
|
});
|
|
|
|
migrated++;
|
|
} catch (error) {
|
|
logger.error(`Failed to encrypt setting ${setting.key}:`, error);
|
|
}
|
|
}
|
|
|
|
resolve({ migrated, error: null });
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Migrate VPN configs to new encryption format
|
|
*/
|
|
async function migrateVPNConfigs() {
|
|
return new Promise((resolve) => {
|
|
db.all(
|
|
`SELECT id, user_id, config_data FROM vpn_configs
|
|
WHERE config_data IS NOT NULL`,
|
|
[],
|
|
async (err, configs) => {
|
|
if (err || !configs || configs.length === 0) {
|
|
return resolve({ migrated: 0, error: err ? err.message : null });
|
|
}
|
|
|
|
let migrated = 0;
|
|
for (const config of configs) {
|
|
try {
|
|
// Check if already encrypted (new format)
|
|
if (config.config_data.split(':').length === 4) {
|
|
continue; // Already encrypted
|
|
}
|
|
|
|
// Re-encrypt using new encryption module
|
|
const encrypted = encryption.encryptVPN(JSON.parse(config.config_data));
|
|
|
|
await new Promise((res, rej) => {
|
|
db.run(
|
|
'UPDATE vpn_configs SET config_data = ? WHERE id = ?',
|
|
[encrypted, config.id],
|
|
(err) => (err ? rej(err) : res())
|
|
);
|
|
});
|
|
|
|
migrated++;
|
|
} catch (error) {
|
|
logger.error(`Failed to encrypt VPN config ${config.id}:`, error);
|
|
}
|
|
}
|
|
|
|
resolve({ migrated, error: null });
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Migrate API tokens (if storing plaintext tokens - usually they're hashed)
|
|
*/
|
|
async function migrateAPITokens() {
|
|
// API tokens are typically already hashed/encrypted, so this is a placeholder
|
|
// Only migrate if you're storing plaintext tokens
|
|
return Promise.resolve({ migrated: 0, error: null });
|
|
}
|
|
|
|
/**
|
|
* Helper: Verify encrypted data can be decrypted
|
|
*/
|
|
async function verifyEncryptedData() {
|
|
const results = {
|
|
vpnConfigs: { tested: 0, valid: 0, invalid: 0 },
|
|
settings: { tested: 0, valid: 0, invalid: 0 }
|
|
};
|
|
|
|
return new Promise((resolve) => {
|
|
db.serialize(() => {
|
|
// Verify VPN configs
|
|
db.all(
|
|
`SELECT id, config_data FROM vpn_configs
|
|
WHERE config_data LIKE '%:%:%:%' LIMIT 10`,
|
|
[],
|
|
(err, configs) => {
|
|
if (!err && configs) {
|
|
configs.forEach(config => {
|
|
results.vpnConfigs.tested++;
|
|
try {
|
|
encryption.decryptVPN(config.config_data);
|
|
results.vpnConfigs.valid++;
|
|
} catch {
|
|
results.vpnConfigs.invalid++;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Verify settings
|
|
db.all(
|
|
`SELECT id, key, value FROM settings
|
|
WHERE value LIKE '%:%:%:%' LIMIT 10`,
|
|
[],
|
|
(err, settings) => {
|
|
if (!err && settings) {
|
|
settings.forEach(setting => {
|
|
results.settings.tested++;
|
|
try {
|
|
encryption.decryptSetting(setting.value, setting.key);
|
|
results.settings.valid++;
|
|
} catch {
|
|
results.settings.invalid++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for async queries
|
|
setTimeout(() => resolve(results), 100);
|
|
}
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = router;
|