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
JavaScript
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
};