Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
180 lines
4.7 KiB
JavaScript
180 lines
4.7 KiB
JavaScript
/**
|
|
* Settings Encryption & Decryption
|
|
* Untuk encrypt/decrypt sensitive fields di settings.json
|
|
*/
|
|
const crypto = require('crypto');
|
|
const { logger } = require('./logger');
|
|
|
|
// Master key untuk encryption (bisa dari environment variable)
|
|
// PENTING: Ganti dengan key yang aman di production
|
|
const MASTER_KEY = process.env.SETTINGS_MASTER_KEY || 'default-master-key-change-this-in-production';
|
|
|
|
function getMasterKeyForString(keyStr) {
|
|
const hash = crypto.createHash('sha256');
|
|
hash.update(keyStr || '');
|
|
return hash.digest();
|
|
}
|
|
|
|
// Normalize master key ke 32 bytes untuk AES-256
|
|
function getMasterKey() {
|
|
return getMasterKeyForString(MASTER_KEY);
|
|
}
|
|
|
|
// List field yang harus di-encrypt
|
|
const SENSITIVE_FIELDS = [
|
|
'genieacs_password',
|
|
'admin_password',
|
|
'admin_api_key',
|
|
'mikrotik_password',
|
|
'telegram_bot_token',
|
|
'session_secret'
|
|
];
|
|
|
|
/**
|
|
* Encrypt value menggunakan AES-256-GCM
|
|
*/
|
|
function encryptValue(value) {
|
|
if (!value || typeof value !== 'string') return value;
|
|
|
|
try {
|
|
const masterKey = getMasterKey();
|
|
const iv = crypto.randomBytes(16);
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', masterKey, iv);
|
|
|
|
let encrypted = cipher.update(value, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
// Format: iv:authTag:encrypted
|
|
return `enc:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
} catch (error) {
|
|
logger.error(`[encryption] Error encrypting value: ${error.message}`);
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function decryptWithKey(encryptedValue, key) {
|
|
const parts = encryptedValue.split(':');
|
|
if (parts.length !== 4) {
|
|
throw new Error('Invalid encrypted value format');
|
|
}
|
|
|
|
const [prefix, ivHex, authTagHex, encrypted] = parts;
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
const authTag = Buffer.from(authTagHex, 'hex');
|
|
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
decipher.setAuthTag(authTag);
|
|
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
|
|
return decrypted;
|
|
}
|
|
|
|
/**
|
|
* Decrypt value
|
|
*/
|
|
function decryptValue(encryptedValue) {
|
|
if (!encryptedValue || typeof encryptedValue !== 'string') return encryptedValue;
|
|
if (!encryptedValue.startsWith('enc:')) return encryptedValue; // Belum di-encrypt
|
|
|
|
const primaryKey = getMasterKey();
|
|
try {
|
|
return decryptWithKey(encryptedValue, primaryKey);
|
|
} catch (error) {
|
|
// If decryption fails, and primaryKey is not the default fallback key, try the fallback key
|
|
const defaultSecret = 'default-master-key-change-this-in-production';
|
|
if (MASTER_KEY !== defaultSecret) {
|
|
try {
|
|
const fallbackKey = getMasterKeyForString(defaultSecret);
|
|
const decrypted = decryptWithKey(encryptedValue, fallbackKey);
|
|
logger.info('[encryption] Successfully decrypted value using default fallback key. It will be re-encrypted using the new key upon next save.');
|
|
return decrypted;
|
|
} catch (fallbackError) {
|
|
// Both primary key and fallback key failed
|
|
logger.error(`[encryption] Error decrypting value (both primary and fallback keys failed): ${fallbackError.message}`);
|
|
}
|
|
} else {
|
|
logger.error(`[encryption] Error decrypting value: ${error.message}`);
|
|
}
|
|
return encryptedValue;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encrypt sensitive fields dalam settings object
|
|
*/
|
|
function encryptSettings(settings) {
|
|
const encrypted = { ...settings };
|
|
|
|
SENSITIVE_FIELDS.forEach(field => {
|
|
if (encrypted[field]) {
|
|
encrypted[field] = encryptValue(encrypted[field]);
|
|
}
|
|
});
|
|
|
|
return encrypted;
|
|
}
|
|
|
|
/**
|
|
* Decrypt sensitive fields dalam settings object
|
|
*/
|
|
function decryptSettings(settings) {
|
|
const decrypted = { ...settings };
|
|
|
|
SENSITIVE_FIELDS.forEach(field => {
|
|
if (decrypted[field]) {
|
|
decrypted[field] = decryptValue(decrypted[field]);
|
|
}
|
|
});
|
|
|
|
return decrypted;
|
|
}
|
|
|
|
/**
|
|
* Mask sensitive values untuk display (show first 4 & last 4 chars)
|
|
*/
|
|
function maskValue(value) {
|
|
if (!value || typeof value !== 'string') return value;
|
|
if (value.length <= 8) return '****';
|
|
|
|
const first = value.substring(0, 4);
|
|
const last = value.substring(value.length - 4);
|
|
return `${first}****${last}`;
|
|
}
|
|
|
|
/**
|
|
* Get masked settings untuk display di UI
|
|
*/
|
|
function getMaskedSettings(settings) {
|
|
const masked = { ...settings };
|
|
|
|
SENSITIVE_FIELDS.forEach(field => {
|
|
if (masked[field]) {
|
|
masked[field] = maskValue(masked[field]);
|
|
}
|
|
});
|
|
|
|
return masked;
|
|
}
|
|
|
|
/**
|
|
* Check apakah field adalah sensitive
|
|
*/
|
|
function isSensitiveField(field) {
|
|
return SENSITIVE_FIELDS.includes(field);
|
|
}
|
|
|
|
module.exports = {
|
|
encryptValue,
|
|
decryptValue,
|
|
encryptSettings,
|
|
decryptSettings,
|
|
maskValue,
|
|
getMaskedSettings,
|
|
isSensitiveField,
|
|
SENSITIVE_FIELDS
|
|
};
|