import { afterEach, describe, expect, it } from 'vitest' import { assertUsablePin, hashPin, verifyPin } from '@sims/auth' describe('PIN policy & hashing', () => { it('accepts sane PINs, rejects weak ones', () => { expect(() => assertUsablePin('4728')).not.toThrow() expect(() => assertUsablePin('12')).toThrow(/4–6 digits/) expect(() => assertUsablePin('0000')).toThrow(/repeated/) expect(() => assertUsablePin('1234')).toThrow(/sequence/) expect(() => assertUsablePin('8765')).toThrow(/sequence/) }) it('hashes with per-credential salt and verifies', () => { const stored = hashPin('4728') expect(verifyPin('4728', stored)).toBe(true) expect(verifyPin('4729', stored)).toBe(false) expect(hashPin('4728').hash).not.toBe(stored.hash) // fresh salt every time }) }) describe('PIN pepper (M3) — HMAC before scrypt', () => { const KEY = 'SIMS_PIN_PEPPER' afterEach(() => { delete process.env[KEY] }) it('round-trips with a server-held pepper set', () => { process.env[KEY] = 'a-long-random-server-pepper' const stored = hashPin('4728') expect(verifyPin('4728', stored)).toBe(true) expect(verifyPin('4729', stored)).toBe(false) }) it('a peppered hash cannot be verified once the pepper is gone (a DB-only theft is uncrackable)', () => { process.env[KEY] = 'secret-pepper' const stored = hashPin('4728') delete process.env[KEY] expect(verifyPin('4728', stored)).toBe(false) // no pepper in hand → even the right PIN fails process.env[KEY] = 'secret-pepper' expect(verifyPin('4728', stored)).toBe(true) // restored pepper verifies again }) it('a different pepper does not verify (rotation invalidates old hashes)', () => { process.env[KEY] = 'pepper-A' const stored = hashPin('4728') process.env[KEY] = 'pepper-B' expect(verifyPin('4728', stored)).toBe(false) }) it('unset pepper is byte-identical to a hash of the raw PIN (dev fallback unchanged)', () => { // With no pepper, the scrypt input is the raw PIN, so a hash made now must verify — proving // the un-peppered path is exactly the pre-M3 behaviour dev logins depend on. delete process.env[KEY] const stored = hashPin('4728') expect(verifyPin('4728', stored)).toBe(true) expect(verifyPin('9999', stored)).toBe(false) }) })