Files
open-isp/config/settingsEncryption.js
T
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
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
2026-10-09 05:51:21 +07:00

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
};