streamflow/backend/routes/encryption-management.js

439 lines
13 KiB
JavaScript
Raw Permalink Normal View History

/**
* 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;