Files
harheimertc/server/utils/auth.js
Torsten Schulz (local) 01cf0e58cb
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 1m1s
Add support for multiple encryption keys in data handling
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.
2026-01-09 09:05:05 +01:00

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