portal-code.ts 1.3 KB

123456789101112131415161718192021222324252627282930313233343536
  1. import { createHash, randomInt, timingSafeEqual } from 'node:crypto'
  2. /**
  3. * The one-time code a customer signs into the portal with.
  4. *
  5. * Six digits, because it is read off a lock screen and typed with a thumb.
  6. * What makes six digits safe is not the number of them, it is that a code
  7. * dies after a handful of wrong guesses: limiting by address does nothing
  8. * about a hundred machines sharing the work between them.
  9. */
  10. export const PORTAL_CODE_MAX_ATTEMPTS = 5
  11. export function generatePortalCode(): string {
  12. // randomInt, not Math.random. This is a credential.
  13. return String(randomInt(0, 1_000_000)).padStart(6, '0')
  14. }
  15. /**
  16. * What goes in the database.
  17. *
  18. * A plain hash rather than a password hash: the input is already six random
  19. * digits and the row is worthless fifteen minutes later. It means reading the
  20. * table hands over nobody's session.
  21. */
  22. export function hashPortalCode(code: string): string {
  23. return createHash('sha256').update(code.replace(/\D/g, '')).digest('hex')
  24. }
  25. /** Compares two hex digests without giving away where they diverge. */
  26. export function portalCodeMatches(stored: string, supplied: string): boolean {
  27. const a = Buffer.from(stored, 'hex')
  28. const b = Buffer.from(hashPortalCode(supplied), 'hex')
  29. if (a.length !== b.length) return false
  30. return timingSafeEqual(a, b)
  31. }