otpauth
Version:
One Time Password (HOTP/TOTP) library for Node.js, Deno and browsers
211 lines (197 loc) • 5.2 kB
JavaScript
import { uintToBuf } from './utils/encoding/uint';
import { hmacDigest } from './utils/crypto/hmac-digest';
import { pad } from './utils/pad';
import { Secret } from './secret';
import { timingSafeEqual } from './utils/crypto/timing-safe-equal';
/**
* HOTP: An HMAC-based One-time Password Algorithm.
* {@link https://tools.ietf.org/html/rfc4226|RFC 4226}
*/
class HOTP {
/**
* Default configuration.
* @type {{
* issuer: string,
* label: string,
* algorithm: string,
* digits: number,
* counter: number
* window: number
* }}
*/
static get defaults() {
return {
issuer: '',
label: 'OTPAuth',
algorithm: 'SHA1',
digits: 6,
counter: 0,
window: 1,
};
}
/**
* Creates an HOTP object.
* @param {Object} [config] Configuration options.
* @param {string} [config.issuer=''] Account provider.
* @param {string} [config.label='OTPAuth'] Account label.
* @param {Secret|string} [config.secret=Secret] Secret key.
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
* @param {number} [config.digits=6] Token length.
* @param {number} [config.counter=0] Initial counter value.
*/
constructor({
issuer = HOTP.defaults.issuer,
label = HOTP.defaults.label,
secret = new Secret(),
algorithm = HOTP.defaults.algorithm,
digits = HOTP.defaults.digits,
counter = HOTP.defaults.counter,
} = {}) {
/**
* Account provider.
* @type {string}
*/
this.issuer = issuer;
/**
* Account label.
* @type {string}
*/
this.label = label;
/**
* Secret key.
* @type {Secret}
*/
this.secret = typeof secret === 'string'
? Secret.fromBase32(secret)
: secret;
/**
* HMAC hashing algorithm.
* @type {string}
*/
this.algorithm = algorithm.toUpperCase();
/**
* Token length.
* @type {number}
*/
this.digits = digits;
/**
* Initial counter value.
* @type {number}
*/
this.counter = counter;
}
/**
* Generates an HOTP token.
* @param {Object} config Configuration options.
* @param {Secret} config.secret Secret key.
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
* @param {number} [config.digits=6] Token length.
* @param {number} [config.counter=0] Counter value.
* @returns {string} Token.
*/
static generate({
secret,
algorithm = HOTP.defaults.algorithm,
digits = HOTP.defaults.digits,
counter = HOTP.defaults.counter,
}) {
const digest = new Uint8Array(hmacDigest(algorithm, secret.buffer, uintToBuf(counter)));
const offset = digest[digest.byteLength - 1] & 15;
const otp = (
((digest[offset] & 127) << 24)
| ((digest[offset + 1] & 255) << 16)
| ((digest[offset + 2] & 255) << 8)
| (digest[offset + 3] & 255)
) % (10 ** digits);
return pad(otp, digits);
}
/**
* Generates an HOTP token.
* @param {Object} [config] Configuration options.
* @param {number} [config.counter=this.counter++] Counter value.
* @returns {string} Token.
*/
generate({
counter = this.counter++,
} = {}) {
return HOTP.generate({
secret: this.secret,
algorithm: this.algorithm,
digits: this.digits,
counter,
});
}
/**
* Validates an HOTP token.
* @param {Object} config Configuration options.
* @param {string} config.token Token value.
* @param {Secret} config.secret Secret key.
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
* @param {number} config.digits Token length.
* @param {number} [config.counter=0] Counter value.
* @param {number} [config.window=1] Window of counter values to test.
* @returns {number|null} Token delta, or null if the token is not found.
*/
static validate({
token,
secret,
algorithm,
digits,
counter = HOTP.defaults.counter,
window = HOTP.defaults.window,
}) {
// Return early if the token length does not match the digit number.
if (token.length !== digits) return null;
let delta = null;
for (let i = counter - window; i <= counter + window; ++i) {
const generatedToken = HOTP.generate({
secret,
algorithm,
digits,
counter: i,
});
if (timingSafeEqual(token, generatedToken)) {
delta = i - counter;
}
}
return delta;
}
/**
* Validates an HOTP token.
* @param {Object} config Configuration options.
* @param {string} config.token Token value.
* @param {number} [config.counter=this.counter] Counter value.
* @param {number} [config.window=1] Window of counter values to test.
* @returns {number|null} Token delta, or null if the token is not found.
*/
validate({
token,
counter = this.counter,
window,
}) {
return HOTP.validate({
token,
secret: this.secret,
algorithm: this.algorithm,
digits: this.digits,
counter,
window,
});
}
/**
* Returns a Google Authenticator key URI.
* @returns {string} URI.
*/
toString() {
const e = encodeURIComponent;
return 'otpauth://hotp/'
+ `${this.issuer.length > 0
? `${e(this.issuer)}:${e(this.label)}?issuer=${e(this.issuer)}&`
: `${e(this.label)}?`}`
+ `secret=${e(this.secret.base32)}&`
+ `algorithm=${e(this.algorithm)}&`
+ `digits=${e(this.digits)}&`
+ `counter=${e(this.counter)}`;
}
}
export { HOTP };