UNPKG

@imajin/rx-otp

Version:

HMAC-based (HOTP) and Time-based (TOTP) One-Time Password manager. Works with Google Authenticator for Two-Factor Authentication.

113 lines (112 loc) 6.24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HOTP = void 0; const bigInt = require("big-integer"); const buffer_1 = require("buffer"); const ConvertBase = require("convert-base"); const crypto = require("crypto"); const pad = require("pad-component"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const validator_1 = require("../schemas/validator"); const converter = new ConvertBase(); class HOTP { } exports.HOTP = HOTP; HOTP.DIGITS_POWER = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000]; HOTP.DOUBLE_DIGITS = [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]; HOTP.generate = (key, options = {}) => (0, rxjs_1.of)(Object.assign(Object.assign({}, options), { key })) .pipe((0, rxjs_1.mergeMap)((data) => validator_1.Validator.validateDataWithSchemaReference('/rx-otp/schemas/hotp-generate.json', data)), (0, operators_1.map)((_) => ({ key: _.key_format === 'str' ? buffer_1.Buffer.from(_.key) : buffer_1.Buffer.from(_.key, 'hex'), counter: _.counter_format === 'int' ? buffer_1.Buffer.from(pad.left(converter.convert(_.counter, 10, 16), 16, '0'), 'hex') : buffer_1.Buffer.from(pad.left(_.counter, 16, '0'), 'hex'), code_digits: _.code_digits, add_checksum: _.add_checksum, truncation_offset: _.truncation_offset, algorithm: _.algorithm })), (0, rxjs_1.mergeMap)((_) => HOTP._generateOTP(_.key, _.counter, _.code_digits, _.add_checksum, _.truncation_offset, _.algorithm))); HOTP.verify = (token, key, options = {}) => (0, rxjs_1.of)(Object.assign(Object.assign({}, options), { token, key })) .pipe((0, rxjs_1.mergeMap)((data) => validator_1.Validator.validateDataWithSchemaReference('/rx-otp/schemas/hotp-verify.json', data)), (0, operators_1.map)((_) => ({ token: _.token, key: _.key_format === 'str' ? buffer_1.Buffer.from(_.key) : buffer_1.Buffer.from(_.key, 'hex'), window: bigInt(_.window), counter: _.counter_format === 'int' ? bigInt(_.counter) : bigInt(_.counter, 16), counter_format: _.counter_format, code_digits: _.token.length, add_checksum: _.add_checksum, truncation_offset: _.truncation_offset, algorithm: _.algorithm, previous_otp_allowed: _.previous_otp_allowed })), (0, operators_1.map)((_) => Object.assign({}, _, { min: _.counter, max: _.counter.add(_.window) })), (0, rxjs_1.mergeMap)((_) => (0, rxjs_1.of)(_) .pipe((0, operators_1.filter)(__ => !!__.previous_otp_allowed), (0, operators_1.map)(__ => Object.assign({}, __, { min: __.min.subtract(__.window) })), (0, operators_1.map)(__ => !!__.min.isNegative() ? Object.assign({}, __, { min: bigInt() }) : __), (0, operators_1.defaultIfEmpty)(_))), (0, rxjs_1.mergeMap)((_) => new rxjs_1.Observable(subscriber => { let iterator = []; for (let i = _.min; i.lesserOrEquals(_.max); i = i.next()) { iterator = iterator.concat(i); } const data = Object.assign(Object.assign({}, _), { iterator }); delete data.window; delete data.min; delete data.max; subscriber.next(data); subscriber.complete(); })), (0, rxjs_1.mergeMap)((_) => HOTP._verifyWithIteration(_))); HOTP._calcChecksum = (num, digits) => { let doubleDigit = true; let total = 0; while (0 < digits--) { let digit = parseInt(`${num % 10}`); num /= 10; if (doubleDigit) { digit = HOTP.DOUBLE_DIGITS[digit]; } total += digit; doubleDigit = !doubleDigit; } let result = total % 10; if (result > 0) { result = 10 - result; } return result; }; HOTP._generateOTP = (key, counter, code_digits, add_checksum, truncation_offset, algorithm) => (0, rxjs_1.of)(crypto.createHmac(algorithm, key)) .pipe((0, operators_1.map)(hmac => buffer_1.Buffer.from(hmac.update(counter).digest('hex'), 'hex')), (0, operators_1.map)(hash => ({ hash, offset: ((0 <= truncation_offset) && (truncation_offset < (hash.length - 4))) ? truncation_offset : (hash[hash.length - 1] & 0xf) })), (0, operators_1.map)((_) => ((_.hash[_.offset] & 0x7f) << 24) | ((_.hash[_.offset + 1] & 0xff) << 16) | ((_.hash[_.offset + 2] & 0xff) << 8) | (_.hash[_.offset + 3] & 0xff)), (0, operators_1.map)(binary => binary % HOTP.DIGITS_POWER[code_digits]), (0, operators_1.map)(otp => add_checksum ? ((otp * 10) + HOTP._calcChecksum(otp, code_digits)) : otp), (0, operators_1.map)(otp => ({ result: otp.toString(), digits: add_checksum ? (code_digits + 1) : code_digits })), (0, operators_1.map)((_) => pad.left(_.result, _.digits, '0'))); HOTP._verifyWithIteration = (data) => (0, rxjs_1.from)(data.iterator) .pipe((0, rxjs_1.mergeMap)((i) => (0, rxjs_1.of)({ key: data.key, counter: data.counter_format === 'int' ? buffer_1.Buffer.from(pad.left(converter.convert(parseInt(i.toString()), 10, 16), 16, '0'), 'hex') : buffer_1.Buffer.from(pad.left(i.toString(16).toUpperCase(), 16, '0'), 'hex'), code_digits: data.code_digits, add_checksum: data.add_checksum, truncation_offset: data.truncation_offset, algorithm: data.algorithm }) .pipe((0, rxjs_1.mergeMap)((params) => HOTP._generateOTP(params.key, params.counter, params.code_digits, params.add_checksum, params.truncation_offset, params.algorithm)), (0, operators_1.map)(_ => ({ token: _, it: i })))), (0, operators_1.filter)(_ => _.token === data.token), (0, operators_1.take)(1), (0, operators_1.map)(_ => _.it), (0, operators_1.map)(_ => _.subtract(data.counter)), (0, rxjs_1.mergeMap)((delta) => (0, rxjs_1.of)(data.counter_format) .pipe((0, operators_1.filter)(_ => _ === 'hex'), (0, operators_1.map)(() => ({ delta: delta.isNegative() ? `-${pad.left(delta.toString(16).toUpperCase().substr(1), 16, '0')}` : pad.left(delta.toString(16).toUpperCase(), 16, '0'), delta_format: 'hex' })), (0, operators_1.defaultIfEmpty)({ delta: parseInt(delta.toString()), delta_format: 'int' }))), (0, operators_1.defaultIfEmpty)(undefined), (0, rxjs_1.mergeMap)(_ => !!_ ? (0, rxjs_1.of)(_) : (0, rxjs_1.throwError)(() => new Error(`The token '${data.token}' doesn't match for the given parameters`))));