const crypto = require('crypto'); /** * Utility für ID-Hashing * Konvertiert numerische IDs in Hashes und zurück */ class HashId { constructor() { // Secret aus Umgebungsvariable oder Fallback this.secret = process.env.HASH_ID_SECRET || 'timeclock-hash-secret-change-in-production'; } /** * Konvertiert eine numerische ID in einen Hash * @param {number} id - Numerische ID * @returns {string} Hash-String */ encode(id) { if (!id) return null; // Erstelle einen deterministischen Hash aus ID + Secret const hmac = crypto.createHmac('sha256', this.secret); hmac.update(id.toString()); const hash = hmac.digest('base64url'); // base64url ist URL-sicher // Füge die ID in verschlüsselter Form hinzu für Dekodierung const encrypted = this.encryptId(id); // Format: {verschlüsselte-id}.{hash-prefix} return `${encrypted}.${hash.substring(0, 12)}`; } /** * Konvertiert einen Hash zurück in eine numerische ID * @param {string} hash - Hash-String * @returns {number|null} Numerische ID oder null bei Fehler */ decode(hash) { if (!hash || typeof hash !== 'string') return null; try { // Extrahiere verschlüsselte ID const parts = hash.split('.'); if (parts.length !== 2) return null; const encrypted = parts[0]; const hashPart = parts[1]; // Entschlüssele ID const id = this.decryptId(encrypted); if (!id) return null; // Verifiziere Hash const expectedHash = this.encode(id); if (!expectedHash || !expectedHash.endsWith(hashPart)) { return null; } return id; } catch (error) { console.error('Fehler beim Dekodieren der Hash-ID:', error); return null; } } /** * Verschlüsselt eine ID * @private */ encryptId(id) { const cipher = crypto.createCipheriv( 'aes-256-cbc', crypto.scryptSync(this.secret, 'salt', 32), Buffer.alloc(16, 0) // IV ); let encrypted = cipher.update(id.toString(), 'utf8', 'base64url'); encrypted += cipher.final('base64url'); return encrypted; } /** * Entschlüsselt eine ID * @private */ decryptId(encrypted) { try { const decipher = crypto.createDecipheriv( 'aes-256-cbc', crypto.scryptSync(this.secret, 'salt', 32), Buffer.alloc(16, 0) // IV ); let decrypted = decipher.update(encrypted, 'base64url', 'utf8'); decrypted += decipher.final('utf8'); return parseInt(decrypted, 10); } catch (error) { return null; } } /** * Konvertiert ein Objekt mit IDs in eines mit Hashes * @param {Object} obj - Objekt mit ID-Feldern * @param {Array} idFields - Array von Feldnamen, die IDs enthalten * @returns {Object} Objekt mit gehashten IDs */ encodeObject(obj, idFields = ['id', 'user_id', 'auth_info_id']) { if (!obj) return obj; const result = { ...obj }; for (const field of idFields) { if (result[field] !== null && result[field] !== undefined) { result[field] = this.encode(result[field]); } } return result; } /** * Konvertiert ein Array von Objekten * @param {Array} array - Array von Objekten * @param {Array} idFields - Array von Feldnamen, die IDs enthalten * @returns {Array} Array mit gehashten IDs */ encodeArray(array, idFields = ['id', 'user_id', 'auth_info_id']) { if (!Array.isArray(array)) return array; return array.map(obj => this.encodeObject(obj, idFields)); } } // Singleton-Instanz const hashId = new HashId(); module.exports = hashId;