@vs-org/authenticator
Version:
VS authenticator package can generate TOTP (RFC6238) for 2FA, and also provide secret and recovery codes for 2FA setup
174 lines (173 loc) • 7.99 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var base_32_1 = require("@vs-org/base-32");
var logger_1 = require("@vs-org/logger");
var random_1 = __importDefault(require("@vs-org/random"));
var TimeUtils_1 = __importDefault(require("./utils/TimeUtils"));
var helper_1 = __importDefault(require("./utils/helper"));
var logLevel = helper_1.default.getLogLevel();
var VsAuthenticator = /** @class */ (function () {
function VsAuthenticator() {
}
/**
* Use this to generate secrete for each user
* @returns {string} - returns secrete object
* @throws {Error}
*
* Eg: vsAuthenticatorInstance.generateSecret("user1","user1@email.com");
*/
VsAuthenticator.generateSecret = function (name, issuer) {
var logger = logger_1.Logger.getInstance(logLevel).getLogger(this.generateSecret.name);
try {
var _secret = (0, random_1.default)({
length: 32,
type: "random"
});
if (!name || !issuer) {
logger.warn("one of parameter name or issuer is not provided, skipping totpIssuerUrl generation");
}
var base32EncodedSecret = (0, base_32_1.base32Encode)(_secret);
return {
plainSecret: _secret,
base32Secret: base32EncodedSecret,
totpIssuerUrl: "otpauth://totp/".concat(name, "?secret=").concat(base32EncodedSecret).concat(issuer ? "&issuer=".concat(encodeURIComponent(issuer)) : "")
};
}
catch (error) {
logger.error("Something went wrong while checking connection with error:".concat(error));
}
return {
plainSecret: "",
base32Secret: "",
totpIssuerUrl: ""
};
};
/**
* Helper method to generate recovery code, if user does have access to TOTP (device).
* @param options
* @param options.codeType --> Code type can be either numbers or alphabet. If it is not provided it will be defaulted to numbers
* @param options.codeLength --> Length of each recover code
* @param options.numberOfCodes --> How many recovery codes needs to be generated
*
* @returns { Array<string> }
* @throws { Error }
*/
VsAuthenticator.generateRecoverCodes = function (options) {
var logger = logger_1.Logger.getInstance(logLevel).getLogger(this.generateRecoverCodes.name);
logger.info("generating recovery codes with options ".concat(JSON.stringify(options)));
if (!["numbers", "uppercase", "lowercase", "symbols", "random"].includes(options.codeType)) {
options.codeType = "numbers";
}
try {
var recoveryCodes = [];
for (var i = 0; i < options.numberOfCodes; i++) {
recoveryCodes.push((0, random_1.default)({
length: options.codeLength,
type: options.codeType
}));
}
return recoveryCodes;
}
catch (error) {
logger.error("Something went wrong while generating recovery codes with error:".concat(error));
}
return [];
};
/**
* @returns {string | never} - returns TOTP as number or undefined incase of errors
*
* @throws {Error}
*/
VsAuthenticator.generateTOTP = function (secret) {
var logger = logger_1.Logger.getInstance(logLevel).getLogger(this.generateTOTP.name);
try {
if (!secret) {
throw new Error("secret is required to generate TOTP");
}
return this._generateTOTP(secret);
}
catch (error) {
logger.error("Something went wrong while generating OTP with error: ".concat(error));
throw new Error("Something went wrong while generation TOTP for connection");
}
};
/**
* Verification is done only for 2 time steps (current and one before). We shouldn't want to allow all OTP's as valid OTP's apart from this.
* In future if there is need to increase this buffer then we can take one more parameter to this function.
* @param totp - totp
* @param secret - secret
* @returns {boolean} - returns true if TOTP is valid and false if it is invalid
*
* @throws {Error}
*/
VsAuthenticator.verifyTOTP = function (totp, secret, prevTimeSteps) {
var logger = logger_1.Logger.getInstance(logLevel).getLogger(this.verifyTOTP.name);
try {
if (!totp || !secret) {
throw new TypeError("totp and secret is required to verify OTP");
}
var verificationStatus = false;
for (var index = 0; index < (prevTimeSteps || 2); index++) {
var timeStep = Math.floor(TimeUtils_1.default.getCurrentUTCTimeInSeconds() / this.timeStep);
var generatedTOTP = this._generateTOTP(secret, timeStep - index);
logger.info("User provided TOTP: ".concat(totp, ", result is ").concat(generatedTOTP === totp.toString()));
if (generatedTOTP === totp.toString()) {
verificationStatus = true;
break;
}
}
return verificationStatus;
}
catch (error) {
logger.error("Something went wrong verifying TOTP: ".concat(error));
return false;
}
};
var _a;
_a = VsAuthenticator;
VsAuthenticator.timeStep = 30;
/**
*
*
* @param secret
* @param _timeStep
* @returns {string} - TOTP
*/
VsAuthenticator._generateTOTP = function (secret, _timeStep) {
/**
* 1. get UTC time in seconds
* 2. Get time step with floor rounding Number of time step = floor(timeInUTC / step)
* 3. Convert step into a hexadecimal value (16 hexadecimal chars = 8 bytes if not then prepend with 0)
* 4. Convert hex value to 8 byte array Buffer.from('7468697320697320612074c3a97374', 'hex'); which is out MSG = M
* 5. Shared secret key is randomly generated 20 bytes number which is base 32 encoded. Convert shared key in 20 bytes array = K
* 6. Calculate HMAC with HMAMC-SHA1(M,K)
* 7. Generated HMAC hash has 160 bits (20 bytes)
* 8. Get last 4 bits of hash value and get its (hex to integer) intger value consider ["AF","16","86",......., "9A"]
* 9. 9A = 10 = Offset
* 10. starting from index 10 get 4 bytes of HMAC HASH ([....,F6, A7, F8, 99,.....])
* 11. Apply binary operations ( F6 & 0x7F) = 76 ( A7 & 0xFF) = A7 ( F8 & 0xFF) =F8 ( 99 & 0xFF)=99
* 12. New binary value is 0x76A7F899. Convert binary to integer 1990719641
* 13. Calculate TOTP = 1990719641 % Math.pow(10,n) where n is size of token by default it is 6 = 717641
* 14. If caculated TOTP has less than 6 digits then prefix it with 0
* 15. Every 30 seconds TOTP is generated by it is valid for 60 secs
*/
var timeStep = _timeStep ||
Math.floor(TimeUtils_1.default.getCurrentUTCTimeInSeconds() / _a.timeStep);
var hexTimeStep = Math.round(Number(timeStep))
.toString(16)
.padStart(16, "0");
var _secret = (0, base_32_1.base32Decode)(secret);
var hash = helper_1.default.getHMACHash(Buffer.from(hexTimeStep, "hex"), Buffer.from(_secret, "ascii"), "sha1");
var offset = hash[hash.length - 1] & 0xf;
var code = ((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
return (code % Math.pow(10, 6)).toString().padStart(6, "0");
};
return VsAuthenticator;
}());
exports.default = VsAuthenticator;