Datenbank bereinigt / Gitea-Integration gefixt
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
395598c2b0
Commit
c21be47428
@ -7,6 +7,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
const { encryptFile, decryptFile, secureDelete } = require('./encryption');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
|
||||
const BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');
|
||||
@ -17,8 +18,9 @@ if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Backup erstellen
|
||||
* Backup erstellen (mit einfacher Verschlüsselung)
|
||||
*/
|
||||
function createBackup() {
|
||||
try {
|
||||
@ -29,12 +31,27 @@ function createBackup() {
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupName = `backup_${timestamp}.db`;
|
||||
const encryptedName = `backup_${timestamp}.db.enc`;
|
||||
const backupPath = path.join(BACKUP_DIR, backupName);
|
||||
const encryptedPath = path.join(BACKUP_DIR, encryptedName);
|
||||
|
||||
// Datenbank kopieren
|
||||
// 1. Normales Backup erstellen (für Kompatibilität)
|
||||
fs.copyFileSync(DB_FILE, backupPath);
|
||||
|
||||
// WAL-Datei auch sichern falls vorhanden
|
||||
// 2. Verschlüsseltes Backup erstellen (zusätzlich)
|
||||
if (process.env.ENCRYPTION_KEY) {
|
||||
try {
|
||||
if (encryptFile(DB_FILE, encryptedPath)) {
|
||||
logger.info(`Verschlüsseltes Backup erstellt: ${encryptedName}`);
|
||||
} else {
|
||||
logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
|
||||
}
|
||||
} catch (encError) {
|
||||
logger.warn('Verschlüsselung fehlgeschlagen, nur normales Backup erstellt');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. WAL-Datei sichern falls vorhanden
|
||||
const walFile = DB_FILE + '-wal';
|
||||
if (fs.existsSync(walFile)) {
|
||||
fs.copyFileSync(walFile, backupPath + '-wal');
|
||||
@ -53,12 +70,12 @@ function createBackup() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alte Backups löschen
|
||||
* Alte Backups löschen (verschlüsselte)
|
||||
*/
|
||||
function cleanupOldBackups(keepCount = 30) {
|
||||
try {
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
@ -66,15 +83,15 @@ function cleanupOldBackups(keepCount = 30) {
|
||||
|
||||
toDelete.forEach(file => {
|
||||
const filePath = path.join(BACKUP_DIR, file);
|
||||
fs.unlinkSync(filePath);
|
||||
secureDelete(filePath);
|
||||
|
||||
// WAL-Datei auch löschen falls vorhanden
|
||||
// Verschlüsselte WAL-Datei auch löschen falls vorhanden
|
||||
const walPath = filePath + '-wal';
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.unlinkSync(walPath);
|
||||
secureDelete(walPath);
|
||||
}
|
||||
|
||||
logger.info(`Altes Backup gelöscht: ${file}`);
|
||||
logger.info(`Altes Backup sicher gelöscht: ${file}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aufräumen alter Backups:', { error: error.message });
|
||||
@ -82,32 +99,50 @@ function cleanupOldBackups(keepCount = 30) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup wiederherstellen
|
||||
* Backup wiederherstellen (entschlüsselt)
|
||||
*/
|
||||
function restoreBackup(backupName) {
|
||||
try {
|
||||
const backupPath = path.join(BACKUP_DIR, backupName);
|
||||
const encryptedBackupPath = path.join(BACKUP_DIR, backupName);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
if (!fs.existsSync(encryptedBackupPath)) {
|
||||
throw new Error(`Backup nicht gefunden: ${backupName}`);
|
||||
}
|
||||
|
||||
// Aktuelles DB sichern bevor überschrieben wird
|
||||
// Aktuelles DB verschlüsselt sichern bevor überschrieben wird
|
||||
if (fs.existsSync(DB_FILE)) {
|
||||
const safetyBackup = DB_FILE + '.before-restore';
|
||||
fs.copyFileSync(DB_FILE, safetyBackup);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const safetyBackupPath = DB_FILE + `.before-restore-${timestamp}.enc`;
|
||||
if (!encryptFile(DB_FILE, safetyBackupPath)) {
|
||||
logger.warn('Sicherheitsbackup vor Wiederherstellung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
// Backup wiederherstellen
|
||||
fs.copyFileSync(backupPath, DB_FILE);
|
||||
// Temporäre entschlüsselte Datei
|
||||
const tempRestorePath = path.join(BACKUP_DIR, `temp_restore_${Date.now()}.db`);
|
||||
|
||||
// Backup entschlüsseln
|
||||
if (!decryptFile(encryptedBackupPath, tempRestorePath)) {
|
||||
throw new Error('Backup-Entschlüsselung fehlgeschlagen');
|
||||
}
|
||||
|
||||
// Entschlüsselte DB kopieren
|
||||
fs.copyFileSync(tempRestorePath, DB_FILE);
|
||||
|
||||
// WAL-Datei auch wiederherstellen falls vorhanden
|
||||
const walBackup = backupPath + '-wal';
|
||||
if (fs.existsSync(walBackup)) {
|
||||
fs.copyFileSync(walBackup, DB_FILE + '-wal');
|
||||
const encryptedWalBackup = encryptedBackupPath + '-wal';
|
||||
if (fs.existsSync(encryptedWalBackup)) {
|
||||
const tempWalPath = tempRestorePath + '-wal';
|
||||
if (decryptFile(encryptedWalBackup, tempWalPath)) {
|
||||
fs.copyFileSync(tempWalPath, DB_FILE + '-wal');
|
||||
secureDelete(tempWalPath);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Backup wiederhergestellt: ${backupName}`);
|
||||
// Temporäre entschlüsselte Dateien sicher löschen
|
||||
secureDelete(tempRestorePath);
|
||||
|
||||
logger.info(`Verschlüsseltes Backup wiederhergestellt: ${backupName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Restore-Fehler:', { error: error.message });
|
||||
@ -116,12 +151,12 @@ function restoreBackup(backupName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste aller Backups
|
||||
* Liste aller verschlüsselten Backups
|
||||
*/
|
||||
function listBackups() {
|
||||
try {
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db.enc'))
|
||||
.map(f => {
|
||||
const filePath = path.join(BACKUP_DIR, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
237
backend/utils/encryption.js
Normale Datei
237
backend/utils/encryption.js
Normale Datei
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* TASKMATE - Encryption Utilities
|
||||
* ================================
|
||||
* Verschlüsselung für Backups und sensitive Daten
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { promisify } = require('util');
|
||||
const logger = require('./logger');
|
||||
|
||||
const ALGORITHM = 'aes-256-cbc';
|
||||
const KEY_LENGTH = 32; // 256 bits
|
||||
const IV_LENGTH = 16; // 128 bits
|
||||
const SALT_LENGTH = 32;
|
||||
const TAG_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Encryption Key aus Umgebung oder generiert
|
||||
*/
|
||||
function getEncryptionKey() {
|
||||
let key = process.env.ENCRYPTION_KEY;
|
||||
|
||||
if (!key) {
|
||||
// Generiere neuen Key falls nicht vorhanden
|
||||
key = crypto.randomBytes(KEY_LENGTH).toString('hex');
|
||||
logger.warn('Encryption Key wurde automatisch generiert. Speichere ihn in der .env: ENCRYPTION_KEY=' + key);
|
||||
return Buffer.from(key, 'hex');
|
||||
}
|
||||
|
||||
// Validiere Key-Length
|
||||
if (key.length !== KEY_LENGTH * 2) { // Hex-String ist doppelt so lang
|
||||
throw new Error(`Encryption Key muss ${KEY_LENGTH * 2} Hex-Zeichen haben`);
|
||||
}
|
||||
|
||||
return Buffer.from(key, 'hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Key aus Passwort ableiten (PBKDF2)
|
||||
*/
|
||||
function deriveKeyFromPassword(password, salt) {
|
||||
return crypto.pbkdf2Sync(password, salt, 100000, KEY_LENGTH, 'sha256');
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei verschlüsseln
|
||||
*/
|
||||
function encryptFile(inputPath, outputPath, password = null) {
|
||||
try {
|
||||
const data = fs.readFileSync(inputPath);
|
||||
|
||||
// Salt und IV generieren
|
||||
const salt = crypto.randomBytes(SALT_LENGTH);
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
|
||||
// Key ableiten
|
||||
const key = password
|
||||
? deriveKeyFromPassword(password, salt)
|
||||
: getEncryptionKey();
|
||||
|
||||
// Verschlüsselung
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
|
||||
// Header + Salt + IV + verschlüsselte Daten
|
||||
const header = Buffer.from('TMENC001', 'ascii'); // TaskMate Encryption v1
|
||||
const result = Buffer.concat([
|
||||
header,
|
||||
salt,
|
||||
iv,
|
||||
encrypted
|
||||
]);
|
||||
|
||||
fs.writeFileSync(outputPath, result);
|
||||
logger.info(`Datei verschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Verschlüsselung fehlgeschlagen: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei entschlüsseln
|
||||
*/
|
||||
function decryptFile(inputPath, outputPath, password = null) {
|
||||
try {
|
||||
const encryptedData = fs.readFileSync(inputPath);
|
||||
|
||||
// Header prüfen
|
||||
const header = encryptedData.subarray(0, 8);
|
||||
if (header.toString('ascii') !== 'TMENC001') {
|
||||
throw new Error('Ungültiges verschlüsseltes Datei-Format');
|
||||
}
|
||||
|
||||
// Komponenten extrahieren
|
||||
let offset = 8;
|
||||
const salt = encryptedData.subarray(offset, offset + SALT_LENGTH);
|
||||
offset += SALT_LENGTH;
|
||||
|
||||
const iv = encryptedData.subarray(offset, offset + IV_LENGTH);
|
||||
offset += IV_LENGTH;
|
||||
|
||||
const encrypted = encryptedData.subarray(offset);
|
||||
|
||||
// Key ableiten
|
||||
const key = password
|
||||
? deriveKeyFromPassword(password, salt)
|
||||
: getEncryptionKey();
|
||||
|
||||
// Entschlüsselung
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
fs.writeFileSync(outputPath, decrypted);
|
||||
logger.info(`Datei entschlüsselt: ${path.basename(inputPath)} -> ${path.basename(outputPath)}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Entschlüsselung fehlgeschlagen: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String verschlüsseln (für Passwörter etc.)
|
||||
*/
|
||||
function encryptString(plaintext, password = null) {
|
||||
try {
|
||||
const salt = crypto.randomBytes(SALT_LENGTH);
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
|
||||
const key = password
|
||||
? deriveKeyFromPassword(password, salt)
|
||||
: getEncryptionKey();
|
||||
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(plaintext, 'utf8')),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
// Base64 kodiert zurückgeben
|
||||
const result = Buffer.concat([salt, iv, encrypted]);
|
||||
return result.toString('base64');
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`String-Verschlüsselung fehlgeschlagen: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String entschlüsseln
|
||||
*/
|
||||
function decryptString(encryptedString, password = null) {
|
||||
try {
|
||||
const data = Buffer.from(encryptedString, 'base64');
|
||||
|
||||
let offset = 0;
|
||||
const salt = data.subarray(offset, offset + SALT_LENGTH);
|
||||
offset += SALT_LENGTH;
|
||||
|
||||
const iv = data.subarray(offset, offset + IV_LENGTH);
|
||||
offset += IV_LENGTH;
|
||||
|
||||
const encrypted = data.subarray(offset);
|
||||
|
||||
const key = password
|
||||
? deriveKeyFromPassword(password, salt)
|
||||
: getEncryptionKey();
|
||||
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return decrypted.toString('utf8');
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`String-Entschlüsselung fehlgeschlagen: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sicheres Löschen einer Datei (Überschreiben)
|
||||
*/
|
||||
function secureDelete(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
const fileSize = stats.size;
|
||||
|
||||
// Datei mehrfach mit Zufallsdaten überschreiben
|
||||
const fd = fs.openSync(filePath, 'r+');
|
||||
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
const randomData = crypto.randomBytes(fileSize);
|
||||
fs.writeSync(fd, randomData, 0, fileSize, 0);
|
||||
fs.fsyncSync(fd);
|
||||
}
|
||||
|
||||
fs.closeSync(fd);
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
logger.info(`Datei sicher gelöscht: ${path.basename(filePath)}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Sicheres Löschen fehlgeschlagen: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encryptFile,
|
||||
decryptFile,
|
||||
encryptString,
|
||||
decryptString,
|
||||
secureDelete,
|
||||
getEncryptionKey
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren