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:
1 parent
12ebf514fb
commit
ad1c2bf76c
16 files changed
+639
-18
No files matched your search
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Auto-Löschen alter Briefe (Datensparsamkeit, DSGVO-Grundsatz).
|
||||
* Reine Funktion — die Anwendung passiert beim App-Start im Store.
|
||||
*/
|
||||
import { BriefEintrag } from '../types';
|
||||
|
||||
/**
|
||||
* Filtert Briefe heraus, die älter als die Aufbewahrungsdauer sind.
|
||||
*
|
||||
* @param briefe - das aktuelle Archiv
|
||||
* @param aufbewahrungTage - 0 = nie löschen (alles behalten)
|
||||
* @param jetztMs - aktueller Zeitpunkt (injizierbar für Tests)
|
||||
*/
|
||||
export function filtereAlteBriefe(
|
||||
briefe: BriefEintrag[],
|
||||
aufbewahrungTage: number,
|
||||
jetztMs: number = Date.now()
|
||||
): BriefEintrag[] {
|
||||
if (aufbewahrungTage <= 0) return briefe;
|
||||
const grenzeMs = jetztMs - aufbewahrungTage * 86400000;
|
||||
return briefe.filter((brief) => {
|
||||
const erstellt = new Date(brief.erstelltAm).getTime();
|
||||
// Unlesbare Datumswerte lieber behalten als versehentlich löschen
|
||||
if (!Number.isFinite(erstellt)) return true;
|
||||
return erstellt >= grenzeMs;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* UTF-8-Kodierung ohne Plattform-Abhängigkeiten.
|
||||
*
|
||||
* Warum selbst gebaut: expo-crypto akzeptiert Klartext nur als Uint8Array
|
||||
* (oder base64-String), und TextEncoder/TextDecoder sind in Hermes nicht
|
||||
* zuverlässig über alle Versionen verfügbar. Der encodeURIComponent-Trick
|
||||
* ist korrekt für alle Unicode-Zeichen (Umlaute, Arabisch, Emoji —
|
||||
* inklusive Surrogate-Paaren) und in Jest testbar.
|
||||
*/
|
||||
|
||||
/** JavaScript-String -> UTF-8-Bytes. */
|
||||
export function utf8Kodieren(text: string): Uint8Array {
|
||||
const prozentKodiert = encodeURIComponent(text);
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < prozentKodiert.length; i++) {
|
||||
if (prozentKodiert[i] === '%') {
|
||||
bytes.push(parseInt(prozentKodiert.slice(i + 1, i + 3), 16));
|
||||
i += 2;
|
||||
} else {
|
||||
bytes.push(prozentKodiert.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
/** UTF-8-Bytes -> JavaScript-String. Wirft bei ungültigem UTF-8 (URIError). */
|
||||
export function utf8Dekodieren(bytes: Uint8Array): string {
|
||||
let prozentKodiert = '';
|
||||
for (const byte of bytes) {
|
||||
prozentKodiert += '%' + byte.toString(16).padStart(2, '0');
|
||||
}
|
||||
return decodeURIComponent(prozentKodiert);
|
||||
}
|
||||
Reference in new issue
Block a user