Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
438
backend/routes/encryption-management.js
Normal file
438
backend/routes/encryption-management.js
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
/**
|
||||
* 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue