UNPKG

gen-totp

Version:

A time-based One-time Password generator that uses current time as a source of uniqueness, following RFC 6238.

252 lines (251 loc) 9.88 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyHOTP = exports.genHOTP = exports.generateSecretKey = exports.bytesToBase32 = exports.generateOtpAuthUri = exports.verifyTOTP = exports.genTOTP = exports.decToHex = exports.hexToDec = exports.base32ToHex = exports.leftPad = void 0; var jssha_1 = __importDefault(require("jssha")); var crypto_1 = require("crypto"); /** * Left pads a string to a specified length. * @param {string} str - The string to pad. * @param {number} len - The desired length. * @param {string} pad - The padding character. * @returns {string} The padded string. */ function leftPad(str, len, pad) { return str.length >= len ? str : pad.repeat(len - str.length) + str; } exports.leftPad = leftPad; /** * Decodes a base32-encoded string to hexadecimal. * Supports RFC 4648 Base32. * @param {string} input - The base32 string. * @returns {string} The decoded hexadecimal string. */ function base32ToHex(input) { var base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; var cleanInput = input.toUpperCase().replace(/=+$/, ""); var bits = ""; var hex = ""; for (var _i = 0, cleanInput_1 = cleanInput; _i < cleanInput_1.length; _i++) { var char = cleanInput_1[_i]; var val = base32Chars.indexOf(char); if (val === -1) { throw new Error("Invalid base32 character: ".concat(char)); } bits += leftPad(val.toString(2), 5, "0"); } for (var i = 0; i + 4 <= bits.length; i += 4) { var chunk = bits.slice(i, i + 4); hex += parseInt(chunk, 2).toString(16); } return hex; } exports.base32ToHex = base32ToHex; /** * Converts a hexadecimal string to a decimal number. * @param {string} hex - The hexadecimal string. * @returns {number} The decimal number. */ function hexToDec(hex) { return parseInt(hex, 16); } exports.hexToDec = hexToDec; /** * Converts a decimal number to a hexadecimal string. * @param {number} dec - The decimal number. * @returns {string} The hexadecimal string. */ function decToHex(dec) { return leftPad(Math.round(dec).toString(16), 2, "0"); } exports.decToHex = decToHex; /** * Generates a TOTP (Time-based One-Time Password). * @param {string} key - The secret key. * @param {GenTOTPOptions} [options={}] - Configuration options. * @param {number} [options.period=30] - Time period in seconds. * @param {string} [options.algorithm='SHA-1'] - Hash algorithm. * @param {number} [options.digits=6] - Length of the resulting OTP. * @param {KeyEncoding} [options.encoding='utf8'] - Encoding of the key ('utf8', 'hex', or 'base32'). * @returns {string} The generated OTP. */ function genTOTP(key, options, // Optional unix-milliseconds timestamp for deterministic outputs / testing timestamp) { if (options === void 0) { options = {}; } var _a = options.period, period = _a === void 0 ? 30 : _a, _b = options.algorithm, algorithm = _b === void 0 ? "SHA-1" : _b, _c = options.digits, digits = _c === void 0 ? 6 : _c, _d = options.encoding, encoding = _d === void 0 ? "utf8" : _d; var epoch = Math.floor((typeof timestamp === "number" ? timestamp : Date.now()) / 1000); var timeHex = leftPad(decToHex(Math.floor(epoch / period)), 16, "0"); var hexKey; if (encoding === "hex") { var lower = key.toLowerCase(); if (!/^[0-9a-f]+$/.test(lower)) { throw new Error("Invalid hex character in key"); } hexKey = lower; } else if (encoding === "base32") { hexKey = base32ToHex(key); } else { // utf8 to hex var encoder = new TextEncoder(); hexKey = Array.from(encoder.encode(key)) .map(function (b) { return b.toString(16).padStart(2, "0"); }) .join(""); } var shaObj = new jssha_1.default(algorithm, "HEX"); shaObj.setHMACKey(hexKey, "HEX"); shaObj.update(timeHex); var hmac = shaObj.getHMAC("HEX"); var offset = hexToDec(hmac[hmac.length - 1]); var code = (hexToDec(hmac.slice(offset * 2, offset * 2 + 8)) & 0x7fffffff).toString(); return code.slice(-digits); } exports.genTOTP = genTOTP; /** * Verifies a TOTP (Time-based One-Time Password). * @param {string} key - The secret key. * @param {string} token - The token to verify. * @param {VerifyTOTPOptions} [options={}] - Configuration options. * @returns {boolean} True if the token is valid, false otherwise. */ function verifyTOTP(key, token, options, // Optional unix-milliseconds timestamp for deterministic outputs / testing timestamp) { if (options === void 0) { options = {}; } var _a = options.window, window = _a === void 0 ? 1 : _a, _b = options.period, period = _b === void 0 ? 30 : _b; var now = typeof timestamp === "number" ? timestamp : Date.now(); for (var i = -window; i <= window; i++) { var stepTimestamp = now + i * period * 1000; var generatedToken = genTOTP(key, options, stepTimestamp); if (generatedToken === token) { return true; } } return false; } exports.verifyTOTP = verifyTOTP; /** * Generates an otpauth URI for QR code generation. * @param {string} key - The base32-encoded secret key. * @param {OtpAuthUriOptions} options - Configuration options. * @returns {string} The otpauth URI. */ function generateOtpAuthUri(key, options) { var accountName = options.accountName, issuer = options.issuer, _a = options.period, period = _a === void 0 ? 30 : _a, _b = options.algorithm, algorithm = _b === void 0 ? "SHA-1" : _b, _c = options.digits, digits = _c === void 0 ? 6 : _c; try { base32ToHex(key); } catch (e) { throw new Error("Invalid base32 key for otpauth URI"); } var encodedIssuer = encodeURIComponent(issuer); var encodedAccountName = encodeURIComponent(accountName); var label = "".concat(encodedIssuer, ":").concat(encodedAccountName); var query = new URLSearchParams({ secret: key, issuer: issuer, algorithm: algorithm.replace("SHA-", "SHA"), digits: digits.toString(), period: period.toString(), }); return "otpauth://totp/".concat(label, "?").concat(query.toString()); } exports.generateOtpAuthUri = generateOtpAuthUri; /** * Encodes a byte array to a base32 string. * @param {Uint8Array} bytes - The bytes to encode. * @returns {string} The base32-encoded string. */ function bytesToBase32(bytes) { var base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; var bits = ""; for (var i = 0; i < bytes.length; i++) { bits += leftPad(bytes[i].toString(2), 8, "0"); } var base32 = ""; for (var i = 0; i < bits.length; i += 5) { var chunk = bits.slice(i, i + 5); if (chunk.length < 5) { chunk += "0".repeat(5 - chunk.length); } var val = parseInt(chunk, 2); base32 += base32Chars[val]; } return base32; } exports.bytesToBase32 = bytesToBase32; /** * Generates a cryptographically secure secret key. * @param {number} [length=20] - The length of the key in bytes. * @returns {string} The base32-encoded secret key. */ function generateSecretKey(length) { if (length === void 0) { length = 20; } var bytes = (0, crypto_1.randomBytes)(length); return bytesToBase32(bytes); } exports.generateSecretKey = generateSecretKey; /** * Generates an HOTP (HMAC-based One-Time Password). * @param {string} key - The secret key. * @param {number} counter - The counter value. * @param {GenHOTPOptions} [options={}] - Configuration options. * @returns {string} The generated OTP. */ function genHOTP(key, counter, options) { if (options === void 0) { options = {}; } var _a = options.algorithm, algorithm = _a === void 0 ? "SHA-1" : _a, _b = options.digits, digits = _b === void 0 ? 6 : _b, _c = options.encoding, encoding = _c === void 0 ? "utf8" : _c; var counterHex = leftPad(decToHex(counter), 16, "0"); var hexKey; if (encoding === "hex") { var lower = key.toLowerCase(); if (!/^[0-9a-f]+$/.test(lower)) { throw new Error("Invalid hex character in key"); } hexKey = lower; } else if (encoding === "base32") { hexKey = base32ToHex(key); } else { // utf8 to hex var encoder = new TextEncoder(); hexKey = Array.from(encoder.encode(key)) .map(function (b) { return b.toString(16).padStart(2, "0"); }) .join(""); } var shaObj = new jssha_1.default(algorithm, "HEX"); shaObj.setHMACKey(hexKey, "HEX"); shaObj.update(counterHex); var hmac = shaObj.getHMAC("HEX"); var offset = hexToDec(hmac[hmac.length - 1]); var code = (hexToDec(hmac.slice(offset * 2, offset * 2 + 8)) & 0x7fffffff).toString(); return code.slice(-digits); } exports.genHOTP = genHOTP; /** * Verifies an HOTP (HMAC-based One-Time Password). * @param {string} key - The secret key. * @param {string} token - The token to verify. * @param {number} counter - The current counter value. * @param {VerifyHOTPOptions} [options={}] - Configuration options. * @returns {{newCounter: number} | null} The new counter value if the token is valid, otherwise null. */ function verifyHOTP(key, token, counter, options) { if (options === void 0) { options = {}; } var _a = options.window, window = _a === void 0 ? 10 : _a; for (var i = 0; i <= window; i++) { var currentCounter = counter + i; var generatedToken = genHOTP(key, currentCounter, options); if (generatedToken === token) { return { newCounter: currentCounter + 1 }; } } return null; } exports.verifyHOTP = verifyHOTP; exports.default = genTOTP;