Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 1m1s
This commit introduces a mechanism to handle multiple possible encryption keys for data decryption across various modules, including auth.js, members.js, newsletter.js, and encryption.js. It adds functions to retrieve potential old keys for migration purposes and updates the decryption logic to attempt decryption with these keys. Additionally, it includes warnings for users when old keys are used and provides guidance for re-encrypting data. This enhancement improves data migration capabilities and ensures backward compatibility with previously encrypted data.
387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
import bcrypt from 'bcryptjs'
|
|
import jwt from 'jsonwebtoken'
|
|
import { promises as fs } from 'fs'
|
|
import path from 'path'
|
|
import { encryptObject, decryptObject } from './encryption.js'
|
|
|
|
// Export migrateUserRoles für Verwendung in anderen Modulen
|
|
export function migrateUserRoles(user) {
|
|
if (!user) return user
|
|
|
|
// Wenn bereits roles Array vorhanden, nichts tun
|
|
if (Array.isArray(user.roles)) {
|
|
return user
|
|
}
|
|
|
|
// Wenn role vorhanden, zu roles Array konvertieren
|
|
if (user.role) {
|
|
user.roles = [user.role]
|
|
delete user.role
|
|
} else {
|
|
// Fallback: Standard-Rolle
|
|
user.roles = ['mitglied']
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
|
|
|
|
// Handle both dev and production paths
|
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
// filename is always a hardcoded constant (e.g., 'users.json'), never user input
|
|
const getDataPath = (filename) => {
|
|
const cwd = process.cwd()
|
|
|
|
// In production (.output/server), working dir is .output
|
|
if (cwd.endsWith('.output')) {
|
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
return path.join(cwd, '../server/data', filename)
|
|
}
|
|
|
|
// In development, working dir is project root
|
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
|
|
return path.join(cwd, 'server/data', filename)
|
|
}
|
|
|
|
const USERS_FILE = getDataPath('users.json')
|
|
const SESSIONS_FILE = getDataPath('sessions.json')
|
|
|
|
// Get encryption key from environment
|
|
function getEncryptionKey() {
|
|
return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
|
|
}
|
|
|
|
// Liste möglicher alter Verschlüsselungsschlüssel (für Migration)
|
|
function getPossibleEncryptionKeys() {
|
|
const currentKey = getEncryptionKey()
|
|
const oldKeys = [
|
|
'default-key-change-in-production',
|
|
'local_development_encryption_key_change_in_production'
|
|
]
|
|
|
|
// Aktueller Schlüssel zuerst, dann alte Schlüssel
|
|
const keys = [currentKey]
|
|
for (const oldKey of oldKeys) {
|
|
if (oldKey !== currentKey) {
|
|
keys.push(oldKey)
|
|
}
|
|
}
|
|
|
|
// Optional: Alter Schlüssel aus Environment-Variable
|
|
if (process.env.OLD_ENCRYPTION_KEY && process.env.OLD_ENCRYPTION_KEY !== currentKey) {
|
|
keys.push(process.env.OLD_ENCRYPTION_KEY)
|
|
}
|
|
|
|
return keys
|
|
}
|
|
|
|
// Check if data is encrypted by trying to parse as JSON first
|
|
function isEncrypted(data) {
|
|
try {
|
|
const parsed = JSON.parse(data.trim())
|
|
if (Array.isArray(parsed)) {
|
|
return false // Unencrypted array
|
|
}
|
|
if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) {
|
|
return false
|
|
}
|
|
return false
|
|
} catch {
|
|
// JSON parsing failed - likely encrypted base64
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Read users from file (with encryption support and migration)
|
|
export async function readUsers() {
|
|
try {
|
|
const data = await fs.readFile(USERS_FILE, 'utf-8')
|
|
|
|
const encrypted = isEncrypted(data)
|
|
let users = []
|
|
|
|
if (encrypted) {
|
|
// Versuche mit verschiedenen Schlüsseln zu entschlüsseln (für Migration)
|
|
const possibleKeys = getPossibleEncryptionKeys()
|
|
const encryptionKey = getEncryptionKey()
|
|
|
|
let lastError = null
|
|
for (let i = 0; i < possibleKeys.length; i++) {
|
|
const key = possibleKeys[i]
|
|
try {
|
|
users = decryptObject(data, key)
|
|
|
|
// Wenn mit altem Schlüssel entschlüsselt wurde, warnen und neu verschlüsseln
|
|
if (i > 0 && key !== encryptionKey) {
|
|
console.warn(`⚠️ Benutzerdaten wurden mit altem Schlüssel entschlüsselt. Automatische Neuverschlüsselung...`)
|
|
try {
|
|
await writeUsers(users)
|
|
console.log('✅ Benutzerdaten erfolgreich mit neuem Schlüssel neu verschlüsselt')
|
|
} catch (writeError) {
|
|
console.error('❌ Fehler beim Neuverschlüsseln:', writeError.message)
|
|
console.error(' Bitte führen Sie manuell aus: node scripts/re-encrypt-data.js')
|
|
}
|
|
}
|
|
|
|
break // Erfolgreich entschlüsselt
|
|
} catch (decryptError) {
|
|
lastError = decryptError
|
|
// Versuche nächsten Schlüssel
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Wenn alle Schlüssel fehlgeschlagen sind
|
|
if (!users || users.length === 0) {
|
|
console.error('Fehler beim Entschlüsseln der Benutzerdaten:')
|
|
console.error(' Versuchte Schlüssel:', possibleKeys.length)
|
|
console.error(' Letzter Fehler:', lastError?.message || 'Unbekannter Fehler')
|
|
console.error('')
|
|
console.error('💡 Lösung: Führen Sie das Re-Encrypt-Skript aus:')
|
|
console.error(' node scripts/re-encrypt-data.js --old-key="<alter-schlüssel>"')
|
|
console.error(' Oder setzen Sie OLD_ENCRYPTION_KEY als Environment-Variable')
|
|
|
|
// Fallback: try to read as plain JSON
|
|
try {
|
|
users = JSON.parse(data)
|
|
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
|
|
} catch (_parseError) {
|
|
console.error('Konnte Benutzerdaten weder entschlüsseln noch als JSON lesen')
|
|
return []
|
|
}
|
|
}
|
|
} else {
|
|
// Plain JSON - migrate to encrypted format
|
|
users = JSON.parse(data)
|
|
console.log('Migriere unverschlüsselte Benutzerdaten zu verschlüsselter Speicherung...')
|
|
}
|
|
|
|
// Migriere Rollen von role zu roles Array
|
|
let needsMigration = false
|
|
users = users.map(user => {
|
|
const migrated = migrateUserRoles(user)
|
|
if (!Array.isArray(user.roles) && user.role) {
|
|
needsMigration = true
|
|
}
|
|
return migrated
|
|
})
|
|
|
|
// Wenn Migration nötig war, speichere zurück
|
|
if (needsMigration) {
|
|
console.log('Migriere Rollen von role zu roles Array...')
|
|
await writeUsers(users)
|
|
} else if (!encrypted) {
|
|
// Write back encrypted wenn noch nicht verschlüsselt
|
|
await writeUsers(users)
|
|
}
|
|
|
|
return users
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return []
|
|
}
|
|
console.error('Fehler beim Lesen der Benutzerdaten:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Write users to file (always encrypted)
|
|
export async function writeUsers(users) {
|
|
try {
|
|
const encryptionKey = getEncryptionKey()
|
|
const encryptedData = encryptObject(users, encryptionKey)
|
|
await fs.writeFile(USERS_FILE, encryptedData, 'utf-8')
|
|
return true
|
|
} catch (error) {
|
|
console.error('Fehler beim Schreiben der Benutzerdaten:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Prüft ob Sessions-Daten verschlüsselt sind
|
|
function isSessionsEncrypted(data) {
|
|
try {
|
|
const parsed = JSON.parse(data.trim())
|
|
if (Array.isArray(parsed)) {
|
|
return false
|
|
}
|
|
if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) {
|
|
return false
|
|
}
|
|
return false
|
|
} catch (e) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Read sessions from file (with encryption support)
|
|
export async function readSessions() {
|
|
try {
|
|
const data = await fs.readFile(SESSIONS_FILE, 'utf-8')
|
|
const encrypted = isSessionsEncrypted(data)
|
|
|
|
if (encrypted) {
|
|
const encryptionKey = getEncryptionKey()
|
|
try {
|
|
return decryptObject(data, encryptionKey)
|
|
} catch (decryptError) {
|
|
console.error('Fehler beim Entschlüsseln der Sessions:', decryptError)
|
|
try {
|
|
const plainData = JSON.parse(data)
|
|
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
|
|
return plainData
|
|
} catch (_parseError) {
|
|
console.error('Konnte Sessions weder entschlüsseln noch als JSON lesen')
|
|
return []
|
|
}
|
|
}
|
|
} else {
|
|
// Plain JSON - migriere zu verschlüsselter Speicherung
|
|
const sessions = JSON.parse(data)
|
|
console.log('Migriere unverschlüsselte Sessions zu verschlüsselter Speicherung...')
|
|
await writeSessions(sessions)
|
|
return sessions
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
return []
|
|
}
|
|
console.error('Fehler beim Lesen der Sessions:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Write sessions to file (always encrypted)
|
|
export async function writeSessions(sessions) {
|
|
try {
|
|
const encryptionKey = getEncryptionKey()
|
|
const encryptedData = encryptObject(sessions, encryptionKey)
|
|
await fs.writeFile(SESSIONS_FILE, encryptedData, 'utf-8')
|
|
return true
|
|
} catch (error) {
|
|
console.error('Fehler beim Schreiben der Sessions:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Hash password
|
|
export async function hashPassword(password) {
|
|
const salt = await bcrypt.genSalt(10)
|
|
return await bcrypt.hash(password, salt)
|
|
}
|
|
|
|
// Verify password
|
|
export async function verifyPassword(password, hash) {
|
|
return await bcrypt.compare(password, hash)
|
|
}
|
|
|
|
// Generate JWT token
|
|
export function generateToken(user) {
|
|
// Stelle sicher, dass Rollen migriert sind
|
|
const migratedUser = migrateUserRoles({ ...user })
|
|
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
|
|
|
|
return jwt.sign(
|
|
{
|
|
id: user.id,
|
|
email: user.email,
|
|
roles: roles
|
|
},
|
|
JWT_SECRET,
|
|
{ expiresIn: '7d' }
|
|
)
|
|
}
|
|
|
|
// Verify JWT token
|
|
export function verifyToken(token) {
|
|
try {
|
|
return jwt.verify(token, JWT_SECRET)
|
|
} catch (_error) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Get user by ID
|
|
export async function getUserById(id) {
|
|
const users = await readUsers()
|
|
const user = users.find(u => u.id === id)
|
|
return user ? migrateUserRoles(user) : null
|
|
}
|
|
|
|
// Get user by email
|
|
export async function getUserByEmail(email) {
|
|
const users = await readUsers()
|
|
const user = users.find(u => u.email === email)
|
|
return user ? migrateUserRoles(user) : null
|
|
}
|
|
|
|
|
|
// Prüft ob Benutzer eine bestimmte Rolle hat
|
|
export function hasRole(user, role) {
|
|
if (!user) return false
|
|
const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
|
return roles.includes(role)
|
|
}
|
|
|
|
// Prüft ob Benutzer eine der angegebenen Rollen hat
|
|
export function hasAnyRole(user, ...roles) {
|
|
if (!user) return false
|
|
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
|
return roles.some(role => userRoles.includes(role))
|
|
}
|
|
|
|
// Prüft ob Benutzer alle angegebenen Rollen hat
|
|
export function hasAllRoles(user, ...roles) {
|
|
if (!user) return false
|
|
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
|
return roles.every(role => userRoles.includes(role))
|
|
}
|
|
|
|
// Get user from token
|
|
export async function getUserFromToken(token) {
|
|
const decoded = verifyToken(token)
|
|
if (!decoded) return null
|
|
|
|
const users = await readUsers()
|
|
const user = users.find(u => u.id === decoded.id)
|
|
|
|
// Migriere Rollen beim Laden
|
|
if (user) {
|
|
migrateUserRoles(user)
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
// Create session
|
|
export async function createSession(userId, token) {
|
|
const sessions = await readSessions()
|
|
const session = {
|
|
id: Date.now().toString(),
|
|
userId,
|
|
token,
|
|
createdAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days
|
|
}
|
|
sessions.push(session)
|
|
await writeSessions(sessions)
|
|
return session
|
|
}
|
|
|
|
// Delete session
|
|
export async function deleteSession(token) {
|
|
const sessions = await readSessions()
|
|
const filtered = sessions.filter(s => s.token !== token)
|
|
await writeSessions(filtered)
|
|
}
|
|
|
|
// Clean expired sessions
|
|
export async function cleanExpiredSessions() {
|
|
const sessions = await readSessions()
|
|
const now = new Date()
|
|
const valid = sessions.filter(s => new Date(s.expiresAt) > now)
|
|
await writeSessions(valid)
|
|
}
|
|
|