import { describe, expect, it } from 'vitest' import { entschluesseln, entschluesselnWennMoeglich, gleichSicher, istUmschlag, neuVerschluesseln, oeffnen, schluesselversionVon, sealen, VERSCHLUESSELTE_FELDER, verschluesseln, } from '@/lib/crypto/feld' describe('Feldverschluesselung', () => { it('verschluesselt und entschluesselt verlustfrei', () => { const klartext = 'COPD GOLD III, Langzeitsauerstofftherapie seit 03/2025' const umschlag = verschluesseln(klartext, 'diagnosenVerschluesselt') expect(umschlag).not.toBeNull() expect(umschlag).not.toContain('COPD') expect(entschluesseln(umschlag, 'diagnosenVerschluesselt')).toBe(klartext) }) it('haelt Umlaute und Sonderzeichen aus', () => { const klartext = 'Straße 3½, Grüße – „Anführungszeichen“, 100 €' const umschlag = verschluesseln(klartext, 'notizenVerschluesselt') expect(entschluesseln(umschlag, 'notizenVerschluesselt')).toBe(klartext) }) it('erzeugt bei gleichem Klartext unterschiedliche Geheimtexte', () => { // Sonst liesse sich aus der Datenbank ablesen, welche Datensaetze // denselben Wert tragen. const a = verschluesseln('DE02120300000000202051', 'ibanVerschluesselt') const b = verschluesseln('DE02120300000000202051', 'ibanVerschluesselt') expect(a).not.toBe(b) }) it('behandelt leere Werte als NULL', () => { expect(verschluesseln(null)).toBeNull() expect(verschluesseln(undefined)).toBeNull() expect(verschluesseln('')).toBeNull() expect(entschluesseln(null)).toBeNull() }) it('erkennt manipulierte Geheimtexte', () => { const umschlag = verschluesseln('Rueckkaufswert 12.480,00 EUR', 'notizenVerschluesselt')! const teile = umschlag.split('.') // Ein Bit im Geheimtext kippen. const roh = Buffer.from(teile[4]!, 'base64url') roh[0] = roh[0]! ^ 0x01 teile[4] = roh.toString('base64url') expect(() => entschluesseln(teile.join('.'), 'notizenVerschluesselt')).toThrow() }) it('verhindert das Umhaengen eines Geheimtexts in ein anderes Feld', () => { // Der Feldname wird als AAD mitauthentifiziert. const umschlag = verschluesseln('geheim', 'diagnosenVerschluesselt') expect(() => entschluesseln(umschlag, 'notizenVerschluesselt')).toThrow() }) it('weist fremde Formate ab', () => { expect(() => entschluesseln('einfach nur text')).toThrow() expect(() => entschluesseln('bc1.1.zu.wenig')).toThrow() expect(entschluesselnWennMoeglich('einfach nur text')).toBeNull() }) it('erkennt Umschlaege und ihre Schluesselversion', () => { const umschlag = verschluesseln('x', 'notizenVerschluesselt')! expect(istUmschlag(umschlag)).toBe(true) expect(istUmschlag('nein')).toBe(false) expect(schluesselversionVon(umschlag)).toBe(1) }) it('rotiert Schluessel, ohne alte Daten unlesbar zu machen', () => { const klartext = 'DE02120300000000202051' const alt = verschluesseln(klartext, 'ibanVerschluesselt')! expect(schluesselversionVon(alt)).toBe(1) process.env.ENC_KEY_AKTIV = '2' // env cached die aktive Version bereits - deshalb prueft dieser Test // nur, dass ein Umschlag der Version 1 weiterhin lesbar bleibt. process.env.ENC_KEY_AKTIV = '1' expect(entschluesseln(alt, 'ibanVerschluesselt')).toBe(klartext) // Gleiche Version -> kein Neuverschluesseln noetig. expect(neuVerschluesseln(alt, 'ibanVerschluesselt')).toBeNull() }) }) describe('sealen und oeffnen', () => { it('setzt die Suffix-Felder aus Klartextfeldern', () => { const daten = sealen('Betreute', { diagnosen: 'COPD GOLD III', notizen: 'Bevorzugt Besuche am Nachmittag', }) expect(Object.keys(daten).sort()).toEqual([ 'diagnosenVerschluesselt', 'notizenVerschluesselt', ]) expect(daten.diagnosenVerschluesselt).toMatch(/^bc1\./) }) it('laesst undefinierte Felder weg, damit Updates sie nicht loeschen', () => { const daten = sealen('Betreute', { diagnosen: 'COPD', notizen: undefined }) expect('notizenVerschluesselt' in daten).toBe(false) }) it('setzt ein leeres Feld auf NULL', () => { const daten = sealen('Betreute', { notizen: '' }) expect(daten.notizenVerschluesselt).toBeNull() }) it('weist unbekannte Felder ab', () => { expect(() => sealen('Betreute', { gibtEsNicht: 'x' })).toThrow(/nicht als verschluesseltes/) }) it('oeffnet einen Datensatz und ergaenzt die Klartextfelder', () => { const datenbankzeile = { id: 'abc', vorname: 'Erika', ...sealen('Betreute', { diagnosen: 'COPD GOLD III', notizen: null }), } const offen = oeffnen('Betreute', datenbankzeile) expect(offen.vorname).toBe('Erika') expect(offen.diagnosen).toBe('COPD GOLD III') expect(offen.notizen).toBeNull() // Der Umschlag bleibt stehen. expect(offen.diagnosenVerschluesselt).toMatch(/^bc1\./) }) }) describe('Feldkatalog', () => { it('haelt fuer jedes Modell nur Felder mit dem richtigen Suffix', () => { for (const [modell, felder] of Object.entries(VERSCHLUESSELTE_FELDER)) { for (const feld of felder) { expect(feld, `${modell}.${feld}`).toMatch(/Verschluesselt$/) } } }) it('deckt die besonderen Datenkategorien ab', () => { // Gesundheit, Finanzen, Freitext - das ist die Zusage aus der README. expect(VERSCHLUESSELTE_FELDER.Betreute).toContain('diagnosenVerschluesselt') expect(VERSCHLUESSELTE_FELDER.Konto).toContain('ibanVerschluesselt') expect(VERSCHLUESSELTE_FELDER.Vertrag).toContain('notizenVerschluesselt') expect(VERSCHLUESSELTE_FELDER.Gesundheitsereignis).toContain('beschreibungVerschluesselt') expect(VERSCHLUESSELTE_FELDER.Kontaktprotokoll).toContain('inhaltVerschluesselt') expect(VERSCHLUESSELTE_FELDER.AuditLog).toContain('aenderungVerschluesselt') }) }) describe('gleichSicher', () => { it('vergleicht korrekt', () => { expect(gleichSicher('abc', 'abc')).toBe(true) expect(gleichSicher('abc', 'abd')).toBe(false) expect(gleichSicher('abc', 'abcd')).toBe(false) }) })