Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
293
backend/utils/encryption.js
Normal file
293
backend/utils/encryption.js
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
/**
|
||||
* Centralized Encryption Utility for CWE-311 Compliance
|
||||
* Provides AES-256-GCM encryption for sensitive data at rest
|
||||
*
|
||||
* Security Features:
|
||||
* - AES-256-GCM authenticated encryption
|
||||
* - Unique IV per encryption operation
|
||||
* - HMAC authentication tags
|
||||
* - Key rotation support
|
||||
* - Secure key derivation from master secret
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Encryption configuration
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const KEY_LENGTH = 32; // 256 bits
|
||||
const IV_LENGTH = 16; // 128 bits for GCM
|
||||
const AUTH_TAG_LENGTH = 16;
|
||||
const SALT_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Get master encryption key from environment or generate default
|
||||
* SECURITY WARNING: Always set ENCRYPTION_MASTER_KEY in production!
|
||||
*/
|
||||
function getMasterKey() {
|
||||
const envKey = process.env.ENCRYPTION_MASTER_KEY;
|
||||
|
||||
if (!envKey) {
|
||||
logger.warn('⚠️ ENCRYPTION_MASTER_KEY not set - using default (insecure for production)');
|
||||
// Use JWT_SECRET as fallback, but warn about it
|
||||
const fallbackKey = process.env.JWT_SECRET || 'default-insecure-key-change-in-production';
|
||||
return crypto.createHash('sha256').update(fallbackKey + '-encryption-v1').digest();
|
||||
}
|
||||
|
||||
// Derive proper key from master secret
|
||||
return crypto.createHash('sha256').update(envKey).digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive encryption key for specific purpose using HKDF-like approach
|
||||
* @param {String} purpose - Purpose identifier (e.g., 'settings', 'vpn', 'api-tokens')
|
||||
* @returns {Buffer} Derived encryption key
|
||||
*/
|
||||
function deriveKey(purpose) {
|
||||
const masterKey = getMasterKey();
|
||||
const info = Buffer.from(purpose + '-v1', 'utf8');
|
||||
|
||||
return crypto.createHmac('sha256', masterKey)
|
||||
.update(info)
|
||||
.digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive data with AES-256-GCM
|
||||
* @param {String} plaintext - Data to encrypt
|
||||
* @param {String} purpose - Purpose identifier for key derivation
|
||||
* @returns {String} Encrypted data in format: salt:iv:authTag:ciphertext (all hex encoded)
|
||||
*/
|
||||
function encrypt(plaintext, purpose = 'default') {
|
||||
try {
|
||||
if (!plaintext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate random salt and IV for this encryption operation
|
||||
const salt = crypto.randomBytes(SALT_LENGTH);
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
|
||||
// Derive encryption key with salt for additional security
|
||||
const masterKey = getMasterKey();
|
||||
const derivedKey = crypto.pbkdf2Sync(
|
||||
masterKey,
|
||||
salt,
|
||||
100000, // iterations
|
||||
KEY_LENGTH,
|
||||
'sha256'
|
||||
);
|
||||
|
||||
// Create cipher
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, derivedKey, iv);
|
||||
|
||||
// Encrypt
|
||||
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
// Get authentication tag (GCM provides authenticated encryption)
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Return format: salt:iv:authTag:ciphertext
|
||||
return [
|
||||
salt.toString('hex'),
|
||||
iv.toString('hex'),
|
||||
authTag.toString('hex'),
|
||||
encrypted
|
||||
].join(':');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Encryption error:', { purpose, error: error.message });
|
||||
throw new Error('Failed to encrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data encrypted with encrypt()
|
||||
* @param {String} encryptedData - Encrypted data in format: salt:iv:authTag:ciphertext
|
||||
* @param {String} purpose - Purpose identifier (must match encryption purpose)
|
||||
* @returns {String} Decrypted plaintext
|
||||
*/
|
||||
function decrypt(encryptedData, purpose = 'default') {
|
||||
try {
|
||||
if (!encryptedData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse encrypted data
|
||||
const parts = encryptedData.split(':');
|
||||
if (parts.length !== 4) {
|
||||
throw new Error('Invalid encrypted data format');
|
||||
}
|
||||
|
||||
const salt = Buffer.from(parts[0], 'hex');
|
||||
const iv = Buffer.from(parts[1], 'hex');
|
||||
const authTag = Buffer.from(parts[2], 'hex');
|
||||
const encrypted = parts[3];
|
||||
|
||||
// Validate lengths
|
||||
if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== AUTH_TAG_LENGTH) {
|
||||
throw new Error('Invalid encrypted data structure');
|
||||
}
|
||||
|
||||
// Derive the same key used for encryption
|
||||
const masterKey = getMasterKey();
|
||||
const derivedKey = crypto.pbkdf2Sync(
|
||||
masterKey,
|
||||
salt,
|
||||
100000,
|
||||
KEY_LENGTH,
|
||||
'sha256'
|
||||
);
|
||||
|
||||
// Create decipher
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, derivedKey, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
// Decrypt
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Decryption error:', { purpose, error: error.message });
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive settings value
|
||||
* Automatically detects if already encrypted
|
||||
*/
|
||||
function encryptSetting(value, key) {
|
||||
if (!value) return null;
|
||||
|
||||
// Don't encrypt if already encrypted (starts with salt:iv:authTag:ciphertext format)
|
||||
if (typeof value === 'string' && value.split(':').length === 4) {
|
||||
const parts = value.split(':');
|
||||
if (parts[0].length === SALT_LENGTH * 2 && parts[1].length === IV_LENGTH * 2) {
|
||||
return value; // Already encrypted
|
||||
}
|
||||
}
|
||||
|
||||
const plaintext = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
return encrypt(plaintext, `setting:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt sensitive setting value
|
||||
*/
|
||||
function decryptSetting(encryptedValue, key) {
|
||||
if (!encryptedValue) return null;
|
||||
|
||||
try {
|
||||
const decrypted = decrypt(encryptedValue, `setting:${key}`);
|
||||
|
||||
// Try to parse as JSON if it looks like JSON
|
||||
if (decrypted && (decrypted.startsWith('{') || decrypted.startsWith('['))) {
|
||||
try {
|
||||
return JSON.parse(decrypted);
|
||||
} catch {
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
// If decryption fails, value might not be encrypted (migration scenario)
|
||||
logger.warn(`Failed to decrypt setting ${key}, returning as-is`);
|
||||
return encryptedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if encryption key is properly configured
|
||||
*/
|
||||
function isEncryptionConfigured() {
|
||||
return !!process.env.ENCRYPTION_MASTER_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption health status
|
||||
*/
|
||||
function getEncryptionStatus() {
|
||||
const configured = isEncryptionConfigured();
|
||||
|
||||
return {
|
||||
configured,
|
||||
algorithm: ALGORITHM,
|
||||
keySize: KEY_LENGTH * 8, // bits
|
||||
status: configured ? 'secure' : 'default-key',
|
||||
warning: configured ? null : 'Using default encryption key - set ENCRYPTION_MASTER_KEY in production',
|
||||
recommendations: configured ? [] : [
|
||||
'Set ENCRYPTION_MASTER_KEY environment variable',
|
||||
'Use a strong random key (at least 32 characters)',
|
||||
'Store the key securely (e.g., Docker secrets, AWS Secrets Manager)'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-encrypt data with new master key (for key rotation)
|
||||
* @param {String} oldEncryptedData - Data encrypted with old key
|
||||
* @param {String} purpose - Purpose identifier
|
||||
* @param {String} oldMasterKey - Old master key (optional, uses current env if not provided)
|
||||
* @returns {String} Data re-encrypted with current master key
|
||||
*/
|
||||
function reEncrypt(oldEncryptedData, purpose = 'default', oldMasterKey = null) {
|
||||
try {
|
||||
// Temporarily swap master key if provided
|
||||
const originalKey = process.env.ENCRYPTION_MASTER_KEY;
|
||||
if (oldMasterKey) {
|
||||
process.env.ENCRYPTION_MASTER_KEY = oldMasterKey;
|
||||
}
|
||||
|
||||
// Decrypt with old key
|
||||
const plaintext = decrypt(oldEncryptedData, purpose);
|
||||
|
||||
// Restore original key
|
||||
if (oldMasterKey) {
|
||||
process.env.ENCRYPTION_MASTER_KEY = originalKey;
|
||||
}
|
||||
|
||||
// Encrypt with current key
|
||||
return encrypt(plaintext, purpose);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Re-encryption error:', { purpose, error: error.message });
|
||||
throw new Error('Failed to re-encrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash sensitive data for comparison (one-way, cannot be decrypted)
|
||||
* Use for data that needs to be compared but not retrieved (e.g., backup codes)
|
||||
*/
|
||||
function hashSensitiveData(data) {
|
||||
return crypto.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure random token
|
||||
*/
|
||||
function generateSecureToken(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
encryptSetting,
|
||||
decryptSetting,
|
||||
deriveKey,
|
||||
isEncryptionConfigured,
|
||||
getEncryptionStatus,
|
||||
reEncrypt,
|
||||
hashSensitiveData,
|
||||
generateSecureToken,
|
||||
|
||||
// Export for legacy VPN compatibility
|
||||
encryptVPN: (data) => encrypt(JSON.stringify(data), 'vpn'),
|
||||
decryptVPN: (data) => JSON.parse(decrypt(data, 'vpn'))
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue