All files / totp.ts

100.00% Branches 6/6
100.00% Lines 49/49
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x8
 
x8
x8
 
x1
 
x8
x8
x27
x27
x162
x27
x27
x27
x27
x27
 
x1
 
 
 
 
 
 
 
 
x8
x8
x27
x27
 
x1
 
 
 
 
 
 
 
 
x8
x8
x13
x13
 
x1
 
 
 
 
 
 
 
 
 
 
x8
x8
x11
x13
x13
x12
x84
x12
x12
x12
x12
x12
x11
 
x1
 
 
 
 
 
 
 
 
x8
x8
x16
x136
x38
x38
x34
x20
x16



























































































































/**
 * Time-based One-Time Password (TOTP) library.
 *
 * The following code has been ported and rewritten by Simon Lecoq from Rajat's original work at:
 * https://hackernoon.com/how-to-implement-google-authenticator-two-factor-auth-in-javascript-091wy3vh3
 *
 * Significant changes includes:
 * - Use of native WebCrypto and TypedBuffer APIs instead of Node.js APIs.
 * - Follow the Google Authenticator spec at <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>.
 *   - Ignore parameters are hard-coded to their default values.
 * - Condense code.
 *
 * @example
 * ```ts
 * import { otpauth, otpsecret, verify } from "./totp.ts"
 * import { qrcode } from "jsr:@libs/qrcode"
 *
 * // Issue a new TOTP secret
 * const secret = otpsecret()
 * const url = otpauth({ issuer: "example.com", account: "alice", secret })
 * console.log(`Please scan the following QR Code:`)
 * qrcode(url.href, { output: "console" })
 *
 * // Verify a TOTP token
 * const token = prompt("Please enter the token generated by your app:")!
 * console.assert(await verify({ token, secret }))
 * ```
 *
 * The following resource can be used to validate the implementation:
 * https://www.verifyr.com/en/otp/check
 *
 * ```
 * Copyright (c) Simon Lecoq <@lowlighter>. (MIT License)
 * https://github.com/lowlighter/libs/blob/main/LICENSE
 * ```
 *
 * @module
 */

// Imports
import { decodeBase32, encodeBase32 } from "@std/encoding/base32"

/**
 * Returns a HMAC-based OTP.
 */
async function htop(secret: string, counter: bigint): Promise<string> {
  const buffer = new DataView(new ArrayBuffer(8))
  buffer.setBigUint64(0, counter, false)
  const key = await crypto.subtle.importKey("raw", decodeBase32(`${secret}${"=".repeat((8 - (secret.length % 8)) % 8)}`), { name: "HMAC", hash: "SHA-1" }, false, ["sign"])
  const hmac = new Uint8Array(await crypto.subtle.sign("HMAC", key, buffer))
  const offset = hmac[hmac.length - 1] & 0xf
  const code = (hmac[offset] & 0x7f) << 24 | (hmac[offset + 1] & 0xff) << 16 | (hmac[offset + 2] & 0xff) << 8 | (hmac[offset + 3] & 0xff)
  return `${code % 10 ** 6}`.padStart(6, "0")
}

/**
 * Returns a Time-based OTP.
 *
 * @example
 * ```ts
 * import { totp, otpsecret } from "./totp.ts"
 * const secret = otpsecret()
 * console.log(totp(secret, { t: Date.now() }))
 * ```
 */
export async function totp(secret: string, { t = Date.now(), dt = 0 } = {}): Promise<string> {
  return await htop(secret, BigInt(Math.floor(t / 1000 / 30) + dt))
}

/**
 * Issue a new Time-based OTP secret.
 *
 * @example
 * ```ts
 * import { otpsecret } from "./totp.ts"
 * const secret = otpsecret()
 * console.log(secret)
 * ```
 */
export function otpsecret(length = 20): string {
  return encodeBase32(crypto.getRandomValues(new Uint8Array(length))).replaceAll("=", "")
}

/**
 * Returns an URL that can be used to be added in a authenticator application.
 *
 * @example
 * ```ts
 * import { otpauth } from "./totp.ts"
 * import { qrcode } from "jsr:@libs/qrcode"
 * const url = otpauth({ issuer: "example.com", account: "alice" })
 * console.log(`Please scan the following QR Code:`)
 * qrcode(url.href, { output: "console" })
 * ```
 */
export function otpauth({ issuer, account, secret = otpsecret(), image }: { issuer: string; account: string; secret?: string; image?: string }): URL {
  if ((issuer.includes(":")) || (account.includes(":"))) {
    throw new RangeError("Label may not contain a colon character")
  }
  const label = encodeURIComponent(`${issuer}:${account}`)
  const params = new URLSearchParams({ secret, issuer, algorithm: "SHA1", digits: "6", period: "30" })
  if (image) {
    params.set("image", image)
  }
  const url = new URL(`otpauth://totp/${label}?${params}`)
  return url
}

/**
 * Verify Time-based OTP.
 *
 * @example
 * ```ts
 * import { verify } from "./totp.ts"
 * console.assert(await verify({ secret: "JBSWY3DPEHPK3PXP", token: 152125, t: 1708671725109 }))
 * console.assert(!await verify({ secret: "JBSWY3DPEHPK3PXP", token: 0, t: 1708671725109 }))
 * ```
 */
export async function verify({ secret, token, t = Date.now(), tolerance = 1 }: { secret: string; token: string | number; t?: number; tolerance?: number }): Promise<boolean> {
  for (let dt = -tolerance; dt <= tolerance; dt++) {
    if (Number(await totp(secret, { t, dt })) === Number(token)) {
      return true
    }
  }
  return false
}