DockerERTFF/lib/modules/otp.js

174 lines
8.1 KiB
JavaScript

const crypto = require('crypto');
const base32 = require('../core/base32');
const secretLength = {
'sha1': 20,
'sha224': 28,
'sha256': 32,
'sha384': 48,
'sha512': 64,
'sha3-224': 28,
'sha3-256': 32,
'sha3-384': 48,
'sha3-512': 64,
};
const hotp = {}; // HOTP: An HMAC-Based One-Time Password Algorithm
const totp = {}; // TOTP: Time-Based One-Time Password Algorithm
hotp.counter = 0n;
hotp.defaults = {
algorithm: 'sha1',
digits: 6,
};
hotp.generate = function (secret, opts) {
opts = { ...hotp.defaults, ...opts };
const counter = opts.counter == null ? ++hotp.counter : opts.counter;
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(BigInt(counter));
const hash = crypto.createHmac(opts.algorithm, secret).update(buffer.slice(0, secretLength[opts.algorithm])).digest();
return truncate(hash, opts.digits);
};
hotp.validate = function (code, secret, opts) {
opts = { ...hotp.defaults, ...opts };
if (opts.counter == null) opts.counter = hotp.counter;
if (code === hotp.generate(secret, opts)) return true;
if (typeof opts.window == 'number' && opts.window > 0) {
for (let n = 1; n < opts.window + 1; n++) {
if (code === hotp.generate(secret, { ...opts, counter: opts.counter - n })) {
return -n;
}
}
}
return false;
};
totp.defaults = {
...hotp.defaults,
time: null,
period: 30,
};
totp.generate = function(secret, opts) {
opts = { ...totp.defaults, ...opts };
const counter = ~~((opts.time || Date.now()) / 1000 / opts.period);
return hotp.generate(secret, { ...opts, counter });
};
totp.validate = function(code, secret, opts) {
opts = { ...totp.defaults, ...opts };
const counter = ~~((opts.time || Date.now()) / 1000 / opts.period);
return code === hotp.generate(secret, { ...opts, counter });
};
function truncate(hash, digits) {
const offset = hash[hash.length-1] & 0xf;
const binary = (hash[offset] & 0x7f) << 24
| (hash[offset+1] & 0xff) << 16
| (hash[offset+2] & 0xff) << 8
| (hash[offset+3] & 0xff);
const otp = binary % Math.pow(10, digits);
return String(otp).padStart(digits, '0');
};
module.exports = {
hotpGenerage (options) {
const secret = this.parseRequired(options.secret, 'string', 'otp.hotpGenerate: secret is required.');
const counter = this.parseOptional(options.counter, 'number', null);
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
return hotp.generate(secret, { counter, digits, algorithm });
},
hotpValidate (options) {
const code = this.parseRequired(options.code, 'string', 'otp.hotpValidate: code is required.');
const secret = this.parseRequired(options.secret, 'string', 'otp.hotpValidate: secret is required.');
const counter = this.parseOptional(options.counter, 'number', null);
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
return hotp.validate(code, secret, { counter, digits, algorithm });
},
totpGenerate (options) {
const secret = this.parseRequired(options.secret, 'string', 'otp.totpGenerate: secret is required.');
const time = this.parseOptional(options.time, 'string', null);
const period = this.parseOptional(options.period, 'number', 30); // 30 | 60
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
if (typeof time == 'string') time = +(new Date(time));
return totp.generate(secret, { time, period, digits, algorithm });
},
totpValidate (options) {
const code = this.parseRequired(options.code, 'string', 'otp.totpValidate: code is required.');
const secret = this.parseRequired(options.secret, 'string', 'otp.totpValidate: secret is required.');
const time = this.parseOptional(options.time, 'string', null);
const period = this.parseOptional(options.period, 'number', 30); // 30 | 60
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
if (typeof time == 'string') time = +(new Date(time));
return totp.validate(code, secret, { time, period, digits, algorithm });
},
// https://git.coolaj86.com/coolaj86/browser-authenticator.js
// 20 cryptographically random binary bytes (160-bit key)
generateSecret (options) {
const size = this.parseOptional(options.size, 'number', 20);
return base32.encode(crypto.randomBytes(size), { padding: false }).toString();
},
// generates a 6-digit (20-bit) decimal time-based token
generateToken (options) {
const secret = this.parseRequired(options.secret, 'string', 'otp.generateToken: secret is required.');
const period = this.parseOptional(options.period, 'number', 30); // 30 | 60
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
return totp.generate(base32.decode(secret), { period, digits, algorithm });
},
// validates a time-based token within a +/- 30 second (90 seconds) window
//returns null on failure or an object such as { delta: 0 } on success
verifyToken (options) {
const code = this.parseRequired(options.code, 'string', 'otp.verifyToken: code is required.')
const secret = this.parseRequired(options.secret, 'string', 'otp.verifyToken: secret is required.');
const period = this.parseOptional(options.period, 'number', 30); // 30 | 60
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
return code === totp.generate(base32.decode(secret), { period, digits, algorithm, window: 1 });
},
// generates an OTPAUTH:// scheme URI for QR Code generation.
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
generateOtpUri (options) {
const secret = this.parseRequired(options.secret, 'string', 'otp.generateTotpUri: secret is required.');
const issuer = this.parseRequired(options.issuer, 'string', 'otp.generateTotpUri: issuer is required.');
const account = this.parseRequired(options.account, 'string', 'otp.generateTotpUri: account is required.');
const digits = this.parseOptional(options.digits, 'number', 6); // 6 | 8
const period = this.parseOptional(options.period, 'number', 30); // 30 | 60
const algorithm = this.parseOptional(options.algorithm, 'string', 'sha1'); // 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' | 'sha3-224' | 'sha3-256' | 'sha3-384' | 'sha3-512'
return `otpauth://totp/${encodeURI(issuer)}:${encodeURI(account)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=${encodeURIComponent(algorithm.toUpperCase())}&digits=${digits}&period=${period}`;
},
generateQrCodeUrl (options) {
const data = this.parseRequired(options.data, 'string', 'otp.generateQrCodeUrl: data is required.');
const size = this.parseOptional(options.size, 'number', 200);
const ecl = this.parseOptional(options.ecl, 'string', 'L'); // error_correction_level: L | M | Q | H
const margin = this.parseOptional(options.margin, 'number', 4);
return `https://www.google.com/chart?cht=qr&chs=${size}x${size}&chl=${encodeURIComponent(data)}&chld=${ecl}|${margin}`;
},
};