@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
JavaScript
;
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`))));