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