UNPKG

one-time-pass

Version:

Zero dependencies Node/Deno/Bun/Browser TOTP and HOTP generator based on RFC 6238 and RFC 4226

147 lines (146 loc) 4.82 kB
const u = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; function f(t) { if (!t || typeof t != "string") throw new Error("Secret key must be a non-empty string"); const r = t.trim().replace(/\s+/g, "").replace(/=/g, "").toUpperCase(); if (r.length < 1) throw new Error("Secret key must be a non-empty string"); if (!/^[A-Z2-7]+$/.test(r)) throw new Error("Invalid Base32 secret key."); const e = r.length * 5 >>> 3, n = new Uint8Array(e); let o = 0, i = 0, a = 0; for (let s = 0; s < r.length; s++) { const l = u.indexOf(r[s]); if (l === -1) throw new Error(`Invalid Base32 character: '${r[s]}'`); i = i << 5 | l, o += 5, o >= 8 && (n[a++] = i >>> o - 8 & 255, o -= 8); } return n; } function h(t) { if (!t || t.length === 0) return ""; const r = []; let e = 0, n = 0; for (let a = 0; a < t.length; a++) for (n = n << 8 | t[a], e += 8; e >= 5; ) r.push(u[n >>> e - 5 & 31]), e -= 5; e > 0 && r.push(u[n << 5 - e & 31]); const o = r.join(""), i = (8 - o.length % 8) % 8; return o + "=".repeat(i); } function d(t, r) { let e = t.length === r.length ? 0 : 1; const n = Math.max(t.length, r.length); for (let o = 0; o < n; o++) e |= (t.charCodeAt(o) || 0) ^ (r.charCodeAt(o) || 0); return e === 0; } const m = 15, p = 2147483647; function b(t, r) { if (!Number.isInteger(r) || r < 1 || r > 10) throw new Error("Digits must be a positive integer between 1 and 10."); if (t.length < 4) throw new Error("HMAC result is too short"); const e = t[t.length - 1] & m; if (e > t.length - 4) throw new Error("Calculated offset is out of bounds for the HMAC result"); let n; try { n = new DataView(t.buffer, t.byteOffset + e, 4).getUint32(0) & p; } catch (i) { throw new Error("Failed to read truncated code: " + i.message); } return (n % 10 ** r).toString().padStart(r, "0"); } async function w(t, r, e) { const n = f(t), o = await (await c()).subtle.importKey( "raw", n, // Uint8Array is BufferSource { name: "HMAC", hash: { name: e.algorithm } }, !1, ["sign"] ), i = new Uint8Array(8); new DataView(i.buffer).setBigUint64(0, BigInt(r), !1); const s = await (await c()).subtle.sign("HMAC", o, i); return b(new Uint8Array(s), e.digits); } let g = null; async function c() { var t; if (g) return g; if (typeof globalThis < "u" && ((t = globalThis.crypto) != null && t.subtle)) return g = globalThis.crypto, g; try { const { webcrypto: r } = await import("node:crypto"); if (r != null && r.subtle) return g = r, g; } catch { } throw new Error("Web Crypto API (subtle) is not available in this environment"); } async function y(t, r) { if (!t || typeof t != "string") throw new Error("Secret key must be a non-empty string"); const e = { algorithm: "SHA-1", digits: 6, counter: 0, ...r }; if (typeof e.counter != "number" || e.counter < 0 || e.counter % 1 !== 0) throw new Error("Counter must be a non-negative integer (not a float)."); if (!Number.isInteger(e.digits) || e.digits < 1 || e.digits > 10) throw new Error("Digits must be a positive integer between 1 and 10."); return w(t, e.counter, { digits: e.digits, algorithm: e.algorithm }); } async function E(t = 160) { if (!Number.isInteger(t) || t < 1 || t > 1024) throw new Error("Length must be a positive integer between 1 and 1024"); const r = await c(), e = new Uint8Array(t); return r.getRandomValues(e), h(e); } function A(t, r = {}) { if (!t || typeof t != "string") throw new Error("Secret key must be a non-empty string"); const e = { algorithm: "SHA-1", period: 30, digits: 6, epoch: Date.now(), ...r }; if (!Number.isInteger(e.period) || e.period <= 0) throw new Error("Period must be a positive integer."); const n = Math.floor(e.epoch / 1e3 / e.period); if (n > Number.MAX_SAFE_INTEGER || n < 0) throw new Error("Counter value exceeds safe integer range"); return y(t, { counter: n, algorithm: e.algorithm, digits: e.digits }); } async function C(t, r, e = {}) { if (typeof t != "string" || !/^\d+$/.test(t)) return null; const n = { algorithm: "SHA-1", period: 30, digits: 6, epoch: Date.now(), window: 1, ...e }; if (!Number.isInteger(n.window) || n.window < 0 || n.window > 10) throw new Error( "Window must be a non-negative integer (recommended: 0-2)." ); if (t.length !== n.digits) return null; const o = Math.floor(n.epoch / 1e3 / n.period); for (let i = -n.window; i <= n.window; i++) { const a = o + i, s = await w(r, a, { digits: n.digits, algorithm: n.algorithm }); if (d(s, t)) return i === 0 ? 0 : i; } return null; } export { y as generateHOTP, E as generateSecret, A as generateTOTP, C as validate };