/** * ConsentStorage - Lokale Speicherung des Consent-Status * * Speichert Consent-Daten im localStorage mit HMAC-Signatur * zur Manipulationserkennung. */ import type { ConsentConfig, ConsentState } from '../types'; const STORAGE_KEY = 'bp_consent'; const STORAGE_VERSION = '1'; /** * Gespeichertes Format */ interface StoredConsent { version: string; consent: ConsentState; signature: string; } /** * ConsentStorage - Persistente Speicherung */ export class ConsentStorage { private config: ConsentConfig; private storageKey: string; constructor(config: ConsentConfig) { this.config = config; // Pro Site ein separater Key this.storageKey = `${STORAGE_KEY}_${config.siteId}`; } /** * Consent laden */ get(): ConsentState | null { if (typeof window === 'undefined') { return null; } try { const raw = localStorage.getItem(this.storageKey); if (!raw) { return null; } const stored: StoredConsent = JSON.parse(raw); // Version pruefen if (stored.version !== STORAGE_VERSION) { this.log('Storage version mismatch, clearing'); this.clear(); return null; } // Signatur pruefen if (!this.verifySignature(stored.consent, stored.signature)) { this.log('Invalid signature, clearing'); this.clear(); return null; } return stored.consent; } catch (error) { this.log('Failed to load consent:', error); return null; } } /** * Consent speichern */ set(consent: ConsentState): void { if (typeof window === 'undefined') { return; } try { const signature = this.generateSignature(consent); const stored: StoredConsent = { version: STORAGE_VERSION, consent, signature, }; localStorage.setItem(this.storageKey, JSON.stringify(stored)); // Auch als Cookie setzen (fuer Server-Side Rendering) this.setCookie(consent); this.log('Consent saved to storage'); } catch (error) { this.log('Failed to save consent:', error); } } /** * Consent loeschen */ clear(): void { if (typeof window === 'undefined') { return; } try { localStorage.removeItem(this.storageKey); this.clearCookie(); this.log('Consent cleared from storage'); } catch (error) { this.log('Failed to clear consent:', error); } } /** * Pruefen ob Consent existiert */ exists(): boolean { return this.get() !== null; } // =========================================================================== // Cookie Management // =========================================================================== /** * Consent als Cookie setzen */ private setCookie(consent: ConsentState): void { const days = this.config.consent?.rememberDays ?? 365; const expires = new Date(); expires.setDate(expires.getDate() + days); // Nur Kategorien als Cookie (fuer SSR) const cookieValue = JSON.stringify(consent.categories); const encoded = encodeURIComponent(cookieValue); document.cookie = [ `${this.storageKey}=${encoded}`, `expires=${expires.toUTCString()}`, 'path=/', 'SameSite=Lax', location.protocol === 'https:' ? 'Secure' : '', ] .filter(Boolean) .join('; '); } /** * Cookie loeschen */ private clearCookie(): void { document.cookie = `${this.storageKey}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } // =========================================================================== // Signature (Simple HMAC-like) // =========================================================================== /** * Signatur generieren */ private generateSignature(consent: ConsentState): string { const data = JSON.stringify(consent); const key = this.config.siteId; // Einfache Hash-Funktion (fuer Client-Side) // In Produktion wuerde man SubtleCrypto verwenden return this.simpleHash(data + key); } /** * Signatur verifizieren */ private verifySignature(consent: ConsentState, signature: string): boolean { const expected = this.generateSignature(consent); return expected === signature; } /** * Einfache Hash-Funktion (djb2) */ private simpleHash(str: string): string { let hash = 5381; for (let i = 0; i < str.length; i++) { hash = (hash * 33) ^ str.charCodeAt(i); } return (hash >>> 0).toString(16); } /** * Debug-Logging */ private log(...args: unknown[]): void { if (this.config.debug) { console.log('[ConsentStorage]', ...args); } } } export default ConsentStorage;