generated from knso/webklar-preview-template
- Brief-Archiv jetzt AES-256-GCM-verschlüsselt (expo-crypto, Schlüssel im SecureStore); Migration alter Klartext-Bestände beim ersten Laden; im Simulator auf der Platte verifiziert (nur Chiffrat, kein Klartext) - AppSchutz-Komponente: optionale App-Sperre (Face ID / Geräte-Code, expo-local-authentication) + Sichtschutz im App-Umschalter - Auto-Löschen alter Briefe (30/90 Tage, Einstellungen), pure Filter- Funktion mit Tests - UTF-8-Helfer (Umlaute/Arabisch/Emoji-sicher) mit Tests; gesamt 30 Tests - Einstellungen: neuer Abschnitt "Sicherheit" - Datenschutzerklärung (Web + Entwurf) um die neuen Maßnahmen ergänzt - Neu: rechtliches/datenschutz-factsheet.md für Kooperations-Pitches - EU-Verarbeitung geprüft: inference_geo bietet aktuell nur us/global, keine EU-Option (Haiku 4.5 gar nicht) — als Ausbaupfad dokumentiert Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
/**
|
|
* Verschlüsselung des Brief-Archivs (AES-256-GCM, nativ via expo-crypto).
|
|
*
|
|
* Warum: Die analysierten Briefe enthalten hochsensible Daten (Sozialleistungen,
|
|
* Gesundheit, Finanzen). AsyncStorage ist unverschlüsselter Klartext auf der
|
|
* Platte — deshalb wird das Archiv vor dem Speichern verschlüsselt.
|
|
*
|
|
* Schlüsselverwaltung: Ein 256-Bit-Schlüssel wird beim ersten Bedarf erzeugt
|
|
* und im Secure Store des Geräts abgelegt (iOS Keychain / Android Keystore).
|
|
* Er verlässt das Gerät nie. GCM liefert zusätzlich Integritätsschutz —
|
|
* manipulierte Daten fallen beim Entschlüsseln auf.
|
|
*/
|
|
import {
|
|
AESEncryptionKey,
|
|
AESSealedData,
|
|
aesDecryptAsync,
|
|
aesEncryptAsync,
|
|
} from 'expo-crypto';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
import { utf8Dekodieren, utf8Kodieren } from '../utils/utf8';
|
|
|
|
const KEY_ARCHIV_SCHLUESSEL = 'behoerdenklar_archiv_schluessel';
|
|
|
|
/** Im Speicher gehaltener Schlüssel — spart SecureStore-Zugriffe pro Aufruf. */
|
|
let schluesselCache: AESEncryptionKey | null = null;
|
|
|
|
async function holeSchluessel(): Promise<AESEncryptionKey> {
|
|
if (schluesselCache) return schluesselCache;
|
|
const hex = await SecureStore.getItemAsync(KEY_ARCHIV_SCHLUESSEL);
|
|
if (hex) {
|
|
schluesselCache = await AESEncryptionKey.import(hex, 'hex');
|
|
} else {
|
|
schluesselCache = await AESEncryptionKey.generate(); // 256 Bit (Default)
|
|
await SecureStore.setItemAsync(
|
|
KEY_ARCHIV_SCHLUESSEL,
|
|
await schluesselCache.encoded('hex')
|
|
);
|
|
}
|
|
return schluesselCache;
|
|
}
|
|
|
|
/** Verschlüsselt Text -> base64-String (IV + Ciphertext + GCM-Tag kombiniert). */
|
|
export async function verschluesseleText(klartext: string): Promise<string> {
|
|
const schluessel = await holeSchluessel();
|
|
const versiegelt = await aesEncryptAsync(utf8Kodieren(klartext), schluessel);
|
|
return (await versiegelt.combined('base64')) as string;
|
|
}
|
|
|
|
/** Entschlüsselt einen base64-String zurück zu Text. Wirft bei Manipulation. */
|
|
export async function entschluesseleText(chiffrat: string): Promise<string> {
|
|
const schluessel = await holeSchluessel();
|
|
const versiegelt = AESSealedData.fromCombined(chiffrat);
|
|
const bytes = (await aesDecryptAsync(versiegelt, schluessel)) as Uint8Array;
|
|
return utf8Dekodieren(bytes);
|
|
}
|
|
|
|
/** Für "Alle Daten löschen": Schlüssel unwiderruflich entfernen. */
|
|
export async function loescheArchivSchluessel(): Promise<void> {
|
|
schluesselCache = null;
|
|
await SecureStore.deleteItemAsync(KEY_ARCHIV_SCHLUESSEL);
|
|
}
|