141 lines
5.6 KiB
JavaScript
141 lines
5.6 KiB
JavaScript
const crypto = require("crypto");
|
||
|
||
// Ключ шифрования из переменных окружения или дефолтный (для разработки)
|
||
// В продакшене ОБЯЗАТЕЛЬНО установите ENCRYPTION_KEY в .env файле!
|
||
// Ключ должен быть строкой минимум 32 символа для AES-256
|
||
const ENCRYPTION_KEY_STRING =
|
||
process.env.ENCRYPTION_KEY || "default-key-change-in-production-min-32-chars";
|
||
|
||
// Проверка ключа при запуске
|
||
const isProduction = process.env.NODE_ENV === "production";
|
||
const isUsingDefaultKey = !process.env.ENCRYPTION_KEY;
|
||
|
||
// В продакшене требуем установленный ключ
|
||
if (isProduction && isUsingDefaultKey) {
|
||
console.error(
|
||
"❌ ОШИБКА: ENCRYPTION_KEY не установлен в переменных окружения!"
|
||
);
|
||
console.error(
|
||
"Установите ENCRYPTION_KEY в .env файле перед запуском в продакшене."
|
||
);
|
||
console.error("Пример: ENCRYPTION_KEY=ваш-случайный-ключ-минимум-32-символа");
|
||
process.exit(1);
|
||
}
|
||
|
||
// Предупреждение в разработке
|
||
if (!isProduction && isUsingDefaultKey) {
|
||
console.warn("⚠️ ПРЕДУПРЕЖДЕНИЕ: Используется дефолтный ключ шифрования!");
|
||
console.warn("Для безопасности установите ENCRYPTION_KEY в .env файле.");
|
||
console.warn("Пример: ENCRYPTION_KEY=ваш-случайный-ключ-минимум-32-символа");
|
||
}
|
||
|
||
// Преобразуем строку ключа в 32-байтовый ключ для AES-256
|
||
const ENCRYPTION_KEY = crypto
|
||
.createHash("sha256")
|
||
.update(ENCRYPTION_KEY_STRING)
|
||
.digest();
|
||
|
||
// Длина IV (Initialization Vector) для AES-256-GCM
|
||
const IV_LENGTH = 16;
|
||
|
||
// Метка аутентификации для проверки целостности данных
|
||
const AUTH_TAG_LENGTH = 16;
|
||
|
||
/**
|
||
* Шифрует текст заметки
|
||
* @param {string} text - Текст для шифрования
|
||
* @returns {string} - Зашифрованный текст в формате base64:iv:authTag
|
||
*/
|
||
function encrypt(text) {
|
||
if (!text || text.trim() === "") {
|
||
return text;
|
||
}
|
||
|
||
try {
|
||
// Генерируем случайный IV для каждой операции шифрования
|
||
const iv = crypto.randomBytes(IV_LENGTH);
|
||
|
||
// Создаем шифр с алгоритмом AES-256-GCM
|
||
const cipher = crypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);
|
||
|
||
// Шифруем текст
|
||
let encrypted = cipher.update(text, "utf8", "base64");
|
||
encrypted += cipher.final("base64");
|
||
|
||
// Получаем метку аутентификации для проверки целостности
|
||
const authTag = cipher.getAuthTag();
|
||
|
||
// Возвращаем зашифрованные данные в формате: base64:iv:authTag
|
||
return `${encrypted}:${iv.toString("base64")}:${authTag.toString(
|
||
"base64"
|
||
)}`;
|
||
} catch (error) {
|
||
console.error("Ошибка шифрования:", error);
|
||
throw new Error("Ошибка шифрования данных");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Дешифрует текст заметки
|
||
* @param {string} encryptedText - Зашифрованный текст в формате base64:iv:authTag
|
||
* @returns {string} - Расшифрованный текст
|
||
*/
|
||
function decrypt(encryptedText) {
|
||
if (!encryptedText || encryptedText.trim() === "") {
|
||
return encryptedText;
|
||
}
|
||
|
||
// Проверяем формат зашифрованных данных
|
||
// Если формат не соответствует ожидаемому, возможно это старая незашифрованная заметка
|
||
const parts = encryptedText.split(":");
|
||
if (parts.length !== 3) {
|
||
// Если это не зашифрованный текст, возвращаем как есть (для обратной совместимости)
|
||
console.warn("Обнаружен незашифрованный текст заметки");
|
||
return encryptedText;
|
||
}
|
||
|
||
try {
|
||
const [encrypted, ivBase64, authTagBase64] = parts;
|
||
|
||
// Преобразуем IV и метку аутентификации из base64
|
||
const iv = Buffer.from(ivBase64, "base64");
|
||
const authTag = Buffer.from(authTagBase64, "base64");
|
||
|
||
// Создаем дешифратор с алгоритмом AES-256-GCM
|
||
const decipher = crypto.createDecipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);
|
||
|
||
// Устанавливаем метку аутентификации для проверки целостности
|
||
decipher.setAuthTag(authTag);
|
||
|
||
// Дешифруем текст
|
||
let decrypted = decipher.update(encrypted, "base64", "utf8");
|
||
decrypted += decipher.final("utf8");
|
||
|
||
return decrypted;
|
||
} catch (error) {
|
||
console.error("Ошибка дешифрования:", error);
|
||
// Если не удалось дешифровать, возможно это старая заметка
|
||
// Возвращаем оригинальный текст
|
||
return encryptedText;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверяет, является ли текст зашифрованным
|
||
* @param {string} text - Текст для проверки
|
||
* @returns {boolean} - true если текст зашифрован
|
||
*/
|
||
function isEncrypted(text) {
|
||
if (!text || text.trim() === "") {
|
||
return false;
|
||
}
|
||
const parts = text.split(":");
|
||
return parts.length === 3;
|
||
}
|
||
|
||
module.exports = {
|
||
encrypt,
|
||
decrypt,
|
||
isEncrypted,
|
||
};
|