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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x2
 
x2
x2
 
x1
 
x2
x2
x20
x20
x120
x20
x20
x20
x20
x20
 
x1
 
 
 
 
 
 
 
 
x2
x2
x20
x20
 
x1
 
 
 
 
 
 
 
 
x2
x2
x6
x6
 
x1
 
 
 
 
 
 
 
 
 
 
x2
x2
x5
x7
x7
x6
x42
x6
x6
x6
x6
x6
x5
 
x1
 
 
 
 
 
 
 
 
x2
x2
x10
x112
x32
x32
x28
x14
x10



























































































































/**
 * 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 "../qrcode/mod.ts"
 *
 * // 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), { 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(): string {
  return encodeBase32(crypto.getRandomValues(new Uint8Array(20))).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
}