import { describe, expect, it } from 'vitest' import { totpAktuellerCode, totpPruefen, totpQrCode, totpSecretEntschluesseln, totpSecretErzeugen, totpSecretVerschluesseln, totpUri, } from '@/lib/crypto/totp' import { blindindex, codeNormalisieren, hashVeraltet, ipHashen, passwortHashen, passwortPruefen, wiederherstellungscodeFinden, wiederherstellungscodeHashen, wiederherstellungscodesErzeugen, } from '@/lib/crypto/hash' describe('TOTP', () => { it('erzeugt ein Base32-Geheimnis brauchbarer Laenge', () => { const secret = totpSecretErzeugen() expect(secret).toMatch(/^[A-Z2-7]+$/) expect(secret.length).toBeGreaterThanOrEqual(32) }) it('akzeptiert den aktuellen Code und lehnt einen falschen ab', () => { const secret = totpSecretErzeugen() const code = totpAktuellerCode(secret, 'fw@wildeflorian.de') expect(totpPruefen(secret, 'fw@wildeflorian.de', code).gueltig).toBe(true) expect(totpPruefen(secret, 'fw@wildeflorian.de', '000000').gueltig).toBe(false) }) it('toleriert Leerzeichen in der Eingabe, aber keine falsche Laenge', () => { const secret = totpSecretErzeugen() const code = totpAktuellerCode(secret, 'test') const mitLuecke = `${code.slice(0, 3)} ${code.slice(3)}` expect(totpPruefen(secret, 'test', mitLuecke).gueltig).toBe(true) expect(totpPruefen(secret, 'test', '12345').gueltig).toBe(false) expect(totpPruefen(secret, 'test', '1234567').gueltig).toBe(false) }) it('bindet den Code an das Geheimnis', () => { const a = totpSecretErzeugen() const b = totpSecretErzeugen() const code = totpAktuellerCode(a, 'test') expect(totpPruefen(b, 'test', code).gueltig).toBe(false) }) it('baut eine otpauth-URI mit Aussteller und Kennung', () => { const secret = totpSecretErzeugen() const uri = totpUri(secret, 'fw@wildeflorian.de') expect(uri).toContain('otpauth://totp/') expect(uri).toContain('Betreuungs-Cockpit') expect(uri).toContain('fw%40wildeflorian.de') expect(uri).toContain('digits=6') expect(uri).toContain('period=30') }) it('liefert den QR-Code als eingebettetes Bild, nicht als Fremd-URL', async () => { const secret = totpSecretErzeugen() const bild = await totpQrCode(secret, 'fw@wildeflorian.de') expect(bild.startsWith('data:image/png;base64,')).toBe(true) expect(bild).not.toContain('http') }) it('speichert das Geheimnis nur verschluesselt', () => { const secret = totpSecretErzeugen() const umschlag = totpSecretVerschluesseln(secret) expect(umschlag).not.toContain(secret) expect(totpSecretEntschluesseln(umschlag)).toBe(secret) }) }) describe('Passwort-Hashing', () => { it('erzeugt einen Argon2id-Hash', async () => { const hash = await passwortHashen('ein-langes-passwort-2026') expect(hash.startsWith('$argon2id$')).toBe(true) expect(hash).not.toContain('ein-langes-passwort-2026') }) it('prueft richtig und falsch', async () => { const hash = await passwortHashen('ein-langes-passwort-2026') expect(await passwortPruefen(hash, 'ein-langes-passwort-2026')).toBe(true) expect(await passwortPruefen(hash, 'ein-langes-passwort-2025')).toBe(false) }) it('salzt - gleiches Passwort ergibt verschiedene Hashes', async () => { const a = await passwortHashen('gleiches-passwort') const b = await passwortHashen('gleiches-passwort') expect(a).not.toBe(b) }) it('faellt bei kaputtem Hash auf falsch zurueck, statt zu werfen', async () => { expect(await passwortPruefen('kein-hash', 'egal')).toBe(false) expect(await passwortPruefen('', 'egal')).toBe(false) }) it('erkennt zu schwach gerechnete Hashes', async () => { const aktuell = await passwortHashen('x') expect(hashVeraltet(aktuell)).toBe(false) expect(hashVeraltet('$argon2id$v=19$m=4096,t=1,p=1$abc$def')).toBe(true) expect(hashVeraltet('unsinn')).toBe(true) }) }) describe('Blindindex', () => { it('ist stabil und normalisiert Schreibweisen', () => { const a = blindindex('DE02 1203 0000 0000 2020 51') const b = blindindex('de02120300000000202051') expect(a).toBe(b) expect(a).not.toBeNull() }) it('gibt den Wert nicht preis', () => { const index = blindindex('DE02120300000000202051')! expect(index).not.toContain('DE02') expect(index.length).toBeGreaterThan(20) }) it('unterscheidet verschiedene Werte', () => { expect(blindindex('DE02120300000000202051')).not.toBe(blindindex('DE02120300000000202052')) }) it('behandelt Leerwerte als null', () => { expect(blindindex(null)).toBeNull() expect(blindindex('')).toBeNull() expect(blindindex(' ')).toBeNull() }) it('pseudonymisiert IP-Adressen kurz und stabil', () => { const a = ipHashen('192.0.2.44') expect(a).toBe(ipHashen('192.0.2.44')) expect(a).not.toBe(ipHashen('192.0.2.45')) expect(a!.length).toBe(22) }) }) describe('Wiederherstellungscodes', () => { it('erzeugt lesbare Codes ohne verwechselbare Zeichen', () => { const codes = wiederherstellungscodesErzeugen(8) expect(codes).toHaveLength(8) for (const code of codes) { expect(code).toMatch(/^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/) expect(code).not.toMatch(/[01OI]/) // 0/O und 1/I sind verwechselbar, L in Grossschrift nicht } expect(new Set(codes).size).toBe(8) }) it('normalisiert die Eingabe', () => { expect(codeNormalisieren('abcd-efgh-jkmn')).toBe('ABCDEFGHJKMN') expect(codeNormalisieren('ABCD EFGH JKMN')).toBe('ABCDEFGHJKMN') }) it('findet einen gueltigen Code und weist einen falschen ab', async () => { const codes = wiederherstellungscodesErzeugen(3) const hashes = await Promise.all(codes.map(wiederherstellungscodeHashen)) expect(await wiederherstellungscodeFinden(hashes, codes[1]!)).toBe(1) // Auch klein geschrieben und ohne Bindestriche. expect(await wiederherstellungscodeFinden(hashes, codes[2]!.replace(/-/g, '').toLowerCase())).toBe(2) expect(await wiederherstellungscodeFinden(hashes, 'ZZZZ-ZZZZ-ZZZZ')).toBe(-1) expect(await wiederherstellungscodeFinden([], codes[0]!)).toBe(-1) }) })