Files
DLE/backend/services/encryptedDatabaseService.js

440 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const fs = require('fs');
const path = require('path');
class EncryptedDataService {
constructor() {
this.encryptionKey = this.loadEncryptionKey();
this.isEncryptionEnabled = !!this.encryptionKey;
if (this.isEncryptionEnabled) {
console.log('🔐 Шифрование базы данных активировано');
console.log('📋 Автоматическое определение зашифрованных колонок');
} else {
console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
}
}
loadEncryptionKey() {
try {
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
if (fs.existsSync(keyPath)) {
const key = fs.readFileSync(keyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
return key;
}
// Попробуем альтернативный путь относительно корня приложения
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
if (fs.existsSync(altKeyPath)) {
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
return key;
}
console.log(`[EncryptedDB] No key file found, using default key`);
return 'default-key';
} catch (error) {
console.error('❌ Ошибка загрузки ключа шифрования:', error);
return 'default-key';
}
}
/**
* Получить данные из таблицы с автоматической расшифровкой
*/
async getData(tableName, conditions = {}, limit = null, orderBy = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions, limit, orderBy);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Строим SELECT с расшифровкой
const selectFields = columns.map(col => {
if (col.column_name.endsWith('_encrypted')) {
const originalName = col.column_name.replace('_encrypted', '');
console.log(`🔓 Расшифровываем поле ${col.column_name} -> ${originalName}`);
if (col.data_type === 'jsonb') {
return `decrypt_json(${col.column_name}, $1) as "${originalName}"`;
} else {
return `decrypt_text(${col.column_name}, $1) as "${originalName}"`;
}
} else if (!col.column_name.includes('_encrypted')) {
// Проверяем, есть ли зашифрованная версия этой колонки
const hasEncryptedVersion = columns.some(encCol =>
encCol.column_name === `${col.column_name}_encrypted`
);
// Если есть зашифрованная версия, пропускаем незашифрованную
if (hasEncryptedVersion) {
console.log(`⚠️ Пропускаем незашифрованное поле ${col.column_name} (есть зашифрованная версия)`);
return null;
}
// Заключаем зарезервированные слова в кавычки
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (reservedWords.includes(col.column_name.toLowerCase())) {
return `"${col.column_name}"`;
}
return col.column_name;
}
return null;
}).filter(Boolean).join(', ');
let query = `SELECT ${selectFields} FROM ${tableName}`;
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
const params = hasEncryptedFields ? [this.encryptionKey] : [];
let paramIndex = hasEncryptedFields ? 2 : 1;
// Список зарезервированных слов для WHERE-условий
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => {
const value = conditions[key];
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
// Обрабатываем оператор $in
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
const placeholders = value.$in.map(() => `$${paramIndex++}`).join(', ');
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} IN (${placeholders})`;
}
// Обрабатываем оператор $ne
if (value && typeof value === 'object' && value.$ne !== undefined) {
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} != $${paramIndex++}`;
}
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
// Заключаем зарезервированные слова в кавычки
const columnName = reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key;
return `${columnName} = $${paramIndex++}`;
}
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
// Добавляем параметры для $in операторов
const paramsToAdd = Object.values(conditions).map(value => {
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
return value.$in;
}
if (value && typeof value === 'object' && value.$ne !== undefined) {
return value.$ne;
}
return value;
}).flat();
params.push(...paramsToAdd);
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
console.log(`🔍 [getData] Выполняем запрос:`, query);
console.log(`🔍 [getData] Параметры:`, params);
const { rows } = await db.getQuery()(query, params);
console.log(`📊 Результат запроса из ${tableName}:`, rows);
return rows;
} catch (error) {
console.error(`❌ Ошибка получения данных из ${tableName}:`, error);
throw error;
}
}
/**
* Сохранить данные в таблицу с автоматическим шифрованием
*/
async saveData(tableName, data, whereConditions = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Для таблицы users используем обычные запросы, так как она содержит смешанные колонки
if (tableName === 'users') {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Подготавливаем данные для шифрования
const encryptedData = {};
const unencryptedData = {};
const filteredData = {}; // Отфильтрованные данные для параметров
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
for (const [key, value] of Object.entries(data)) {
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
const unencryptedColumn = columns.find(col => col.column_name === key);
console.log(`🔍 Обрабатываем поле ${key} = "${value}" (тип: ${typeof value})`);
if (encryptedColumn) {
// Если есть зашифрованная колонка, шифруем данные
// Проверяем, что значение не пустое перед шифрованием
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {
// Пропускаем пустые значения
console.log(`⚠️ Пропускаем пустое зашифрованное поле ${key}`);
continue;
}
const currentParamIndex = paramIndex++;
filteredData[key] = value; // Добавляем в отфильтрованные данные
console.log(`✅ Добавили зашифрованное поле ${key} в filteredData`);
if (encryptedColumn.data_type === 'jsonb') {
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
} else {
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
}
} else if (unencryptedColumn) {
// Если есть незашифрованная колонка, сохраняем как есть
// Проверяем, что значение не пустое перед сохранением (кроме role и sender_type)
if ((value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) &&
key !== 'role' && key !== 'sender_type') {
// Пропускаем пустые значения, кроме role и sender_type
console.log(`⚠️ Пропускаем пустое незашифрованное поле ${key}`);
continue;
}
filteredData[key] = value; // Добавляем в отфильтрованные данные
unencryptedData[key] = `$${paramIndex++}`;
console.log(`✅ Добавили незашифрованное поле ${key} в filteredData и unencryptedData`);
} else {
// Если колонка не найдена, пропускаем
console.warn(`⚠️ Колонка ${key} не найдена в таблице ${tableName}`);
}
}
const allData = { ...unencryptedData, ...encryptedData };
// Проверяем, есть ли данные для сохранения
if (Object.keys(allData).length === 0) {
console.warn(`⚠️ Нет данных для сохранения в таблице ${tableName} - все значения пустые`);
console.warn(`⚠️ Исходные данные:`, data);
console.warn(`⚠️ Отфильтрованные данные:`, filteredData);
return null;
}
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
if (whereConditions) {
// UPDATE
const setClause = Object.keys(allData)
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
} else {
// INSERT
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const params = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData)] : [...Object.values(filteredData)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
}
} catch (error) {
console.error(`❌ Ошибка сохранения данных в ${tableName}:`, error);
throw error;
}
}
/**
* Удалить данные из таблицы
*/
async deleteData(tableName, conditions) {
try {
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
// Для зашифрованных таблиц - пока используем обычный DELETE
// TODO: Добавить логику для зашифрованных условий WHERE
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
} catch (error) {
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
throw error;
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return {
enabled: this.isEncryptionEnabled,
keyExists: !!this.encryptionKey,
keyPath: path.join(__dirname, '../../ssl/keys/full_db_encryption.key')
};
}
/**
* Проверить, нужно ли шифровать колонку
*/
shouldEncryptColumn(column) {
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
return encryptableTypes.includes(column.data_type) &&
!column.column_name.includes('_encrypted') &&
!['created_at', 'updated_at', 'id'].includes(column.column_name);
}
/**
* Выполнить незашифрованный запрос (fallback)
*/
async executeUnencryptedQuery(tableName, conditions = {}, limit = null, orderBy = null) {
let query = `SELECT * FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${key} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
/**
* Выполнить незашифрованное сохранение (fallback)
*/
async executeUnencryptedSave(tableName, data, whereConditions = null) {
if (whereConditions) {
// UPDATE
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${key} = $${Object.keys(data).length + index + 1}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const params = [...Object.values(data), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
} else {
// INSERT
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const { rows } = await db.getQuery()(query, values);
return rows[0];
}
}
}
module.exports = new EncryptedDataService();