generated from knso/webklar-preview-template
Datenschutz-Ausbau: Archiv-Verschlüsselung, App-Sperre, Auto-Löschen
- 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>
This commit is contained in:
61
src/services/krypto.ts
Normal file
61
src/services/krypto.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
/**
|
||||
* Lokale Speicherung.
|
||||
* - API-Key: expo-secure-store (verschlüsselt, nie im Klartext auf der Platte)
|
||||
* - Brief-Archiv + Einwilligung: AsyncStorage (bleibt auf dem Gerät)
|
||||
* - Brief-Archiv: AsyncStorage, aber AES-256-GCM-verschlüsselt (siehe krypto.ts)
|
||||
* - Einwilligung & Einstellungen: AsyncStorage (unkritische Flags)
|
||||
*
|
||||
* Datenschutz: Alle Daten bleiben lokal. Nichts wird an Server gesendet,
|
||||
* außer dem Briefinhalt zur KI-Analyse (nach expliziter Einwilligung).
|
||||
* loescheAlleDaten() entfernt restlos alles.
|
||||
* loescheAlleDaten() entfernt restlos alles — inklusive Archiv-Schlüssel.
|
||||
*/
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { BriefEintrag } from '../types';
|
||||
import {
|
||||
entschluesseleText,
|
||||
loescheArchivSchluessel,
|
||||
verschluesseleText,
|
||||
} from './krypto';
|
||||
|
||||
const KEY_API = 'anthropic_api_key';
|
||||
const KEY_BRIEFE = 'behoerdenklar_briefe';
|
||||
@@ -112,21 +118,65 @@ export async function speichereConsent(): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_CONSENT, 'ja');
|
||||
}
|
||||
|
||||
// ---- Brief-Archiv ----
|
||||
// ---- Brief-Archiv (verschlüsselt) ----
|
||||
|
||||
export async function ladeBriefe(): Promise<BriefEintrag[]> {
|
||||
const roh = await AsyncStorage.getItem(KEY_BRIEFE);
|
||||
if (!roh) return [];
|
||||
try {
|
||||
return JSON.parse(roh) as BriefEintrag[];
|
||||
// Migration: Bestände aus Versionen vor der Verschlüsselung liegen als
|
||||
// Klartext-JSON-Array vor ('['), base64-Chiffrate nie. Einmalig
|
||||
// verschlüsselt neu schreiben.
|
||||
if (roh.startsWith('[')) {
|
||||
const briefe = JSON.parse(roh) as BriefEintrag[];
|
||||
await speichereBriefe(briefe);
|
||||
return briefe;
|
||||
}
|
||||
return JSON.parse(await entschluesseleText(roh)) as BriefEintrag[];
|
||||
} catch {
|
||||
// Korrupte Daten nicht crashen lassen — leeres Archiv liefern
|
||||
// Korrupte/unlesbare Daten nicht crashen lassen — leeres Archiv liefern
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function speichereBriefe(briefe: BriefEintrag[]): Promise<void> {
|
||||
await AsyncStorage.setItem(KEY_BRIEFE, JSON.stringify(briefe));
|
||||
await AsyncStorage.setItem(
|
||||
KEY_BRIEFE,
|
||||
await verschluesseleText(JSON.stringify(briefe))
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Sicherheits-Einstellungen ----
|
||||
|
||||
const KEY_APP_SPERRE = 'behoerdenklar_app_sperre';
|
||||
const KEY_AUTO_LOESCHEN = 'behoerdenklar_auto_loeschen_tage';
|
||||
|
||||
/** Ist die App-Sperre (Face ID / Geräte-Code beim Öffnen) aktiviert? */
|
||||
export async function holeAppSperre(): Promise<boolean> {
|
||||
return (await AsyncStorage.getItem(KEY_APP_SPERRE)) === 'ja';
|
||||
}
|
||||
|
||||
export async function setzeAppSperre(aktiv: boolean): Promise<void> {
|
||||
if (aktiv) {
|
||||
await AsyncStorage.setItem(KEY_APP_SPERRE, 'ja');
|
||||
} else {
|
||||
await AsyncStorage.removeItem(KEY_APP_SPERRE);
|
||||
}
|
||||
}
|
||||
|
||||
/** Nach wie vielen Tagen Briefe automatisch gelöscht werden (0 = nie). */
|
||||
export async function holeAutoLoeschTage(): Promise<number> {
|
||||
const roh = await AsyncStorage.getItem(KEY_AUTO_LOESCHEN);
|
||||
const tage = roh ? parseInt(roh, 10) : 0;
|
||||
return Number.isFinite(tage) && tage > 0 ? tage : 0;
|
||||
}
|
||||
|
||||
export async function setzeAutoLoeschTage(tage: number): Promise<void> {
|
||||
if (tage > 0) {
|
||||
await AsyncStorage.setItem(KEY_AUTO_LOESCHEN, String(tage));
|
||||
} else {
|
||||
await AsyncStorage.removeItem(KEY_AUTO_LOESCHEN);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Alles löschen (Datenschutz-Anforderung) ----
|
||||
@@ -141,6 +191,10 @@ export async function loescheAlleDaten(): Promise<void> {
|
||||
KEY_GERAETE_ID,
|
||||
KEY_SCAN_ANZAHL,
|
||||
KEY_BONUS,
|
||||
KEY_APP_SPERRE,
|
||||
KEY_AUTO_LOESCHEN,
|
||||
]);
|
||||
await SecureStore.deleteItemAsync(KEY_API);
|
||||
// Archiv-Schlüssel zuletzt: danach wären Restdaten ohnehin unlesbar
|
||||
await loescheArchivSchluessel();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user