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:
JUSNandClaude Fable 5 committed 2026-07-15 15:30:19 +02:00
1 parent 12ebf514fb
commit ad1c2bf76c
16 files changed
+639 -18

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
/**
* Tests fürs Auto-Löschen — löschen ist unumkehrbar, deshalb müssen die
* Grenzfälle sitzen (nichts zu früh löschen, "nie" respektieren).
*/
import { filtereAlteBriefe } from '../aufbewahrung';
import { BriefEintrag } from '../../types';
const JETZT = new Date('2026-07-15T12:00:00Z').getTime();
const TAG = 86400000;
function brief(id: string, alterInTagen: number): BriefEintrag {
return {
id,
erstelltAm: new Date(JETZT - alterInTagen * TAG).toISOString(),
analyse: {
brieftyp: 'Test',
absender: 'Testamt',
kernaussage: '',
erklaerung_einfach: '',
fachbegriffe: [],
frist: null,
termin: null,
checkliste: [],
antwort_noetig: false,
antwort_optionen: [],
},
uebersetzungen: {},
};
}
describe('filtereAlteBriefe', () => {
it('0 Tage = nie löschen, alles bleibt', () => {
const briefe = [brief('a', 1), brief('b', 500)];
expect(filtereAlteBriefe(briefe, 0, JETZT)).toHaveLength(2);
});
it('löscht Briefe, die älter als die Frist sind', () => {
const briefe = [brief('neu', 5), brief('alt', 40)];
const ergebnis = filtereAlteBriefe(briefe, 30, JETZT);
expect(ergebnis.map((b) => b.id)).toEqual(['neu']);
});
it('behält Briefe exakt am Grenzwert (30 Tage alt bei 30-Tage-Frist)', () => {
const briefe = [brief('grenze', 30)];
expect(filtereAlteBriefe(briefe, 30, JETZT)).toHaveLength(1);
});
it('löscht bei 90-Tage-Frist erst nach 90 Tagen', () => {
const briefe = [brief('a', 89), brief('b', 91)];
expect(filtereAlteBriefe(briefe, 90, JETZT).map((b) => b.id)).toEqual(['a']);
});
it('behält Briefe mit kaputtem Datum (lieber zu viel als zu wenig)', () => {
const kaputt = { ...brief('kaputt', 0), erstelltAm: 'kein-datum' };
expect(filtereAlteBriefe([kaputt], 30, JETZT)).toHaveLength(1);
});
it('leeres Archiv bleibt leer', () => {
expect(filtereAlteBriefe([], 30, JETZT)).toEqual([]);
});
});
+42
View File
@@ -0,0 +1,42 @@
/**
* Tests für die UTF-8-Helfer — kritisch für die Archiv-Verschlüsselung:
* Ein Kodierungsfehler würde gespeicherte Briefe unlesbar machen.
*/
import { utf8Kodieren, utf8Dekodieren } from '../utf8';
describe('utf8Kodieren / utf8Dekodieren', () => {
const rundreise = (text: string) => utf8Dekodieren(utf8Kodieren(text));
it('einfaches ASCII', () => {
expect(rundreise('Hallo Welt 123')).toBe('Hallo Welt 123');
});
it('deutsche Umlaute und ß', () => {
expect(rundreise('Bescheid über Bürgergeld gemäß § 32 — Änderung')).toBe(
'Bescheid über Bürgergeld gemäß § 32 — Änderung'
);
});
it('arabisch, ukrainisch, türkisch (Übersetzungssprachen)', () => {
const text = 'العربية · Українська · Türkçe İĞÜŞÖÇ';
expect(rundreise(text)).toBe(text);
});
it('Emoji (Surrogate-Paare)', () => {
expect(rundreise('Ampel: 🔴🟡🟢 📬')).toBe('Ampel: 🔴🟡🟢 📬');
});
it('JSON-Sonderzeichen', () => {
const json = JSON.stringify({ a: 'Zeile\nUmbruch "Anführung" \\ Slash' });
expect(rundreise(json)).toBe(json);
});
it('leerer String', () => {
expect(rundreise('')).toBe('');
});
it('kodiert Umlaute als Mehrbyte-Sequenzen (echtes UTF-8)', () => {
// "ü" ist in UTF-8 die Bytefolge 0xC3 0xBC
expect(Array.from(utf8Kodieren('ü'))).toEqual([0xc3, 0xbc]);
});
});