UNPKG

@colingreybosh/otp-lib

Version:

A TypeScript library for generating, validating, and managing one-time passwords (OTP) for authentication purposes.

102 lines 3.81 kB
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