|
|
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)
|
|
|
})
|
|
|
})
|