UNPKG

totp-native

Version:

High-performance TOTP library using native Web Crypto API

319 lines (318 loc) 10.6 kB
/** * TOTP Native - High-performance TOTP library using Web Crypto API * RFC 6238 compliant implementation with TypeScript support */ export Totp = export TotpGenerator = export TotpError = void 0; class TotpError extends Error { constructor(message, code) { super(message); this.code = code; this.name = 'TotpError'; } } export TotpError = TotpError; /** * Base32 alphabet as defined in RFC 4648 */ const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; /** * Decode Base32 string to Uint8Array */ function base32Decode(encoded) { // Remove padding and convert to uppercase const cleanInput = encoded.replace(/=+$/, '').toUpperCase(); let bits = ''; // Convert each character to 5-bit binary for (let i = 0; i < cleanInput.length; i++) { const char = cleanInput[i]; const index = BASE32_ALPHABET.indexOf(char); if (index === -1) { throw new TotpError(`Invalid Base32 character: ${char}`, 'INVALID_BASE32'); } bits += index.toString(2).padStart(5, '0'); } // Convert bits to bytes const bytes = []; for (let i = 0; i < bits.length; i += 8) { const byte = bits.substring(i, i + 8); if (byte.length === 8) { bytes.push(parseInt(byte, 2)); } } return new Uint8Array(bytes); } /** * Encode Uint8Array to Base32 string */ function base32Encode(data) { let result = ''; let bits = ''; // Convert bytes to bits for (const byte of data) { bits += byte.toString(2).padStart(8, '0'); } // Convert 5-bit groups to Base32 characters for (let i = 0; i < bits.length; i += 5) { const chunk = bits.substring(i, i + 5); if (chunk.length === 5) { result += BASE32_ALPHABET[parseInt(chunk, 2)]; } } return result; } /** * Generate HMAC using Web Crypto API */ async function generateHmac(key, message, algorithm) { const keyBuffer = new Uint8Array(key); const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: algorithm }, false, ['sign']); const signature = await crypto.subtle.sign('HMAC', cryptoKey, message); return new Uint8Array(signature); } /** * Generate cryptographically secure random bytes */ function getRandomBytes(length) { const bytes = new Uint8Array(length); crypto.getRandomValues(bytes); return bytes; } /** * Convert number to 8-byte big-endian array */ function numberToBytes(num) { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setUint32(4, num, false); // Big-endian, store in lower 32 bits return buffer; } /** * Validate TOTP configuration */ function validateConfig(config) { if (!config.secret) { throw new TotpError('Secret is required', 'MISSING_SECRET'); } if (config.digits !== undefined && (config.digits < 4 || config.digits > 8)) { throw new TotpError('Digits must be between 4 and 8', 'INVALID_DIGITS'); } if (config.period !== undefined && config.period <= 0) { throw new TotpError('Period must be greater than 0', 'INVALID_PERIOD'); } if (config.skew !== undefined && config.skew < 0) { throw new TotpError('Skew must be non-negative', 'INVALID_SKEW'); } } /** * TOTP Generator class for repeated use with the same configuration */ class TotpGenerator { constructor(config) { validateConfig(config); // Set defaults this.config = { secret: config.secret, digits: config.digits ?? 6, period: config.period ?? 30, algorithm: config.algorithm ?? 'SHA1', skew: config.skew ?? 1, explicitZeroPad: config.explicitZeroPad ?? true, timestamp: config.timestamp ?? Date.now() }; } /** * Generate TOTP token for current time */ async generate() { const timestamp = Math.floor(Date.now() / 1000); return this.generateAt(timestamp); } /** * Generate TOTP token for specific timestamp */ async generateAt(timestamp) { try { // Decode secret const key = base32Decode(this.config.secret); // Calculate time step const timeStep = Math.floor(timestamp / this.config.period); // Convert time step to 8-byte array const timeBytes = numberToBytes(timeStep); // Generate HMAC - map algorithm name for crypto.subtle const algorithmMap = { 'SHA1': 'SHA-1', 'SHA256': 'SHA-256', 'SHA512': 'SHA-512' }; const cryptoAlgorithm = algorithmMap[this.config.algorithm] || this.config.algorithm; const hmac = await generateHmac(key, timeBytes, cryptoAlgorithm); // Dynamic truncation (RFC 6238) const offset = hmac[hmac.length - 1] & 0x0f; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); // Generate OTP const otp = binary % Math.pow(10, this.config.digits); // Format with zero padding if enabled return this.config.explicitZeroPad ? otp.toString().padStart(this.config.digits, '0') : otp.toString(); } catch (error) { if (error instanceof TotpError) { throw error; } throw new TotpError(`Failed to generate TOTP: ${error}`, 'GENERATION_ERROR'); } } /** * Verify TOTP token against current time */ async verify(token) { return this.verifyWithSkew(token, this.config.skew); } /** * Verify TOTP token with custom skew tolerance */ async verifyWithSkew(token, skew) { const timestamp = Math.floor(Date.now() / 1000); const currentStep = Math.floor(timestamp / this.config.period); // Check current time step and surrounding steps within skew for (let i = 0; i <= skew; i++) { const steps = i === 0 ? [currentStep] : [currentStep - i, currentStep + i]; for (const step of steps) { if (step >= 0) { // Ensure we don't go negative const testToken = await this.generateAt(step * this.config.period); if (testToken === token) { return true; } } } } return false; } /** * Generate Google Authenticator compatible URI */ generateUri(issuer, accountName) { const params = new URLSearchParams({ secret: this.config.secret, issuer: issuer, algorithm: this.config.algorithm, digits: this.config.digits.toString(), period: this.config.period.toString() }); const encodedIssuer = encodeURIComponent(issuer); const encodedAccount = encodeURIComponent(accountName); return `otpauth://totp/${encodedIssuer}:${encodedAccount}?${params.toString()}`; } /** * Generate random Base32 secret */ static generateSecret(length = 32) { const bytes = getRandomBytes(length); return base32Encode(bytes); } /** * Parse Google Authenticator URI */ static parseUri(uri) { try { const url = new URL(uri); if (url.protocol !== 'otpauth:' || url.hostname !== 'totp') { throw new TotpError('Invalid TOTP URI format', 'INVALID_URI'); } const secret = url.searchParams.get('secret'); if (!secret) { throw new TotpError('Missing secret in URI', 'MISSING_SECRET'); } const config = { secret }; const digits = url.searchParams.get('digits'); if (digits) config.digits = parseInt(digits, 10); const period = url.searchParams.get('period'); if (period) config.period = parseInt(period, 10); const algorithm = url.searchParams.get('algorithm'); if (algorithm && ['SHA1', 'SHA256', 'SHA512'].includes(algorithm)) { config.algorithm = algorithm; } return config; } catch (error) { if (error instanceof TotpError) { throw error; } throw new TotpError(`Failed to parse URI: ${error}`, 'PARSE_ERROR'); } } } export TotpGenerator = TotpGenerator; /** * Static utility class for one-off TOTP operations */ class Totp { /** * Generate TOTP token directly from secret */ static async generate(secret, options = {}) { const generator = new TotpGenerator({ secret, ...options }); return generator.generate(); } /** * Generate TOTP token for specific timestamp */ static async generateAt(secret, timestamp, options = {}) { const generator = new TotpGenerator({ secret, ...options }); return generator.generateAt(timestamp); } /** * Verify TOTP token directly */ static async verify(secret, token, options = {}) { const generator = new TotpGenerator({ secret, ...options }); return generator.verify(token); } /** * Verify TOTP token with custom skew */ static async verifyWithSkew(secret, token, skew, options = {}) { const generator = new TotpGenerator({ secret, ...options }); return generator.verifyWithSkew(token, skew); } /** * Generate random secret */ static generateSecret(length = 32) { return TotpGenerator.generateSecret(length); } /** * Parse URI to config */ static parseUri(uri) { return TotpGenerator.parseUri(uri); } /** * Create URI from config */ static createUri(secret, issuer, accountName, options = {}) { const generator = new TotpGenerator({ secret, ...options }); return generator.generateUri(issuer, accountName); } /** * Get remaining time in current period */ static getRemainingTime(period = 30) { const now = Math.floor(Date.now() / 1000); return period - (now % period); } } export Totp = Totp; // Export everything export default = { TotpGenerator, Totp, TotpError }; //# sourceMappingURL=index.js.map