@colingreybosh/otp-lib
Version:
A TypeScript library for generating, validating, and managing one-time passwords (OTP) for authentication purposes.
102 lines • 3.81 kB
JavaScript
import { createHmac } from 'node:crypto';
import { decodeSecretForHMAC } from '../utils/crypto';
import { validateAlgorithm, validateCounter, validateDigits, validatePeriod, validateSecret, validateToken, } from '../utils/validation';
export class TOTP {
config;
constructor(config) {
this.validateConfig(config);
this.config = { ...config };
}
generate(timestamp) {
const currentTime = timestamp ?? Date.now();
const timeStep = this.getCurrentTimeStep(currentTime);
const token = this.generateToken(timeStep);
const remainingTime = this.config.period - ((currentTime / 1000) % this.config.period);
return {
token,
remainingTime: Math.ceil(remainingTime),
};
}
validate(token, timestamp, window = 1) {
validateToken(token, this.config.digits);
const currentTimeStep = this.getCurrentTimeStep(timestamp);
let validationResult = { isValid: false };
for (let i = -window; i <= window; i++) {
const timeStep = currentTimeStep + i;
const expectedToken = this.generateToken(timeStep);
if (this.constantTimeEquals(token, expectedToken)) {
validationResult = {
isValid: true,
delta: i,
};
}
}
return validationResult;
}
getCurrentTimeStep(timestamp) {
const currentTime = timestamp ?? Date.now();
return Math.floor(currentTime / 1000 / this.config.period);
}
validateConfig(config) {
validateAlgorithm(config.algorithm);
validateSecret(config.secret, config.algorithm);
validateDigits(config.digits);
validatePeriod(config.period);
}
generateToken(timeStep) {
validateCounter(timeStep);
const timeBuffer = this.createTimeBuffer(timeStep);
const hash = this.generateHMAC(timeBuffer);
const truncatedHash = this.performDynamicTruncation(hash);
const token = truncatedHash % Math.pow(10, this.config.digits);
return token.toString().padStart(this.config.digits, '0');
}
createTimeBuffer(timeStep) {
const timeBuffer = Buffer.alloc(8);
timeBuffer.writeUInt32BE(Math.floor(timeStep / 0x100000000), 0);
timeBuffer.writeUInt32BE(timeStep & 0xffffffff, 4);
return timeBuffer;
}
generateHMAC(timeBuffer) {
const hmac = createHmac(this.config.algorithm.toLowerCase(), decodeSecretForHMAC(this.config.secret));
hmac.update(timeBuffer);
return hmac.digest();
}
performDynamicTruncation(hash) {
const lastByte = hash[hash.length - 1];
if (lastByte === undefined) {
throw new Error('Hash generation failed');
}
const offset = lastByte & 0x0f;
if (hash.length < offset + 4) {
throw new Error('Invalid hash offset for truncation');
}
const [byte0, byte1, byte2, byte3] = [
hash[offset],
hash[offset + 1],
hash[offset + 2],
hash[offset + 3],
];
if (byte0 === undefined ||
byte1 === undefined ||
byte2 === undefined ||
byte3 === undefined) {
throw new Error('Invalid hash bytes for truncation');
}
return (((byte0 & 0x7f) << 24) |
((byte1 & 0xff) << 16) |
((byte2 & 0xff) << 8) |
(byte3 & 0xff));
}
constantTimeEquals(a, b) {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}
//# sourceMappingURL=totp.js.map