File size: 1,721 Bytes
dcedb25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import crypto from 'crypto';

// Shared primitive for generating and verifying one-time alphanumeric codes.
//
// Uppercase letters + digits, minus visually ambiguous characters (I, O, 0, 1),
// so codes are easy to read from a terminal log and type by hand.
//
// Usage: call createOneTimeCode() to get an independent code instance; each
// instance keeps its own state and can be generated / cleared / compared
// without affecting any other instance.

const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const CODE_LENGTH = 10;

export interface OneTimeCode {
  /** Mint a new code, replacing any existing one. Returns the raw code string. */
  generate(): string;
  /** Return the current code, or null if none is active. */
  get(): string | null;
  /** Invalidate the current code. */
  clear(): void;
  /**
   * Constant-time comparison against the active code.
   * Returns false if no code is active or the input is not a matching string.
   */
  matches(provided: unknown): boolean;
}

export function createOneTimeCode(): OneTimeCode {
  let current: string | null = null;

  return {
    generate() {
      const bytes = crypto.randomBytes(CODE_LENGTH);
      let out = '';
      for (let i = 0; i < CODE_LENGTH; i++) {
        out += ALPHABET[bytes[i]! % ALPHABET.length];
      }
      current = out;
      return current;
    },

    get() {
      return current;
    },

    clear() {
      current = null;
    },

    matches(provided: unknown): boolean {
      if (!current || typeof provided !== 'string') return false;
      const a = Buffer.from(current);
      const b = Buffer.from(provided);
      if (a.length !== b.length) return false;
      return crypto.timingSafeEqual(a, b);
    },
  };
}