// Copyright Abridged, Inc. 2022,2024. All Rights Reserved. // Node module: @collabland/common // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import crypto from 'crypto'; import {totp} from 'node-otp'; import QRCode, {QRCodeSegment} from 'qrcode'; import {URLSearchParams} from 'url'; export * from 'qrcode'; /** * RFC4648 Base32 encode * @param data - Data to be encoded * @returns */ export function base32Encode(data: Buffer | string, padding = false) { if (typeof data === 'string') { data = Buffer.from(data, 'utf-8'); } const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = 0; let value = 0; let output = ''; for (let i = 0; i < data.byteLength; i++) { value = (value << 8) | data.at(i)!; bits += 8; while (bits >= 5) { output += alphabet[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) { output += alphabet[(value << (5 - bits)) & 31]; } if (padding) { while (output.length % 8 !== 0) { output += '='; } } return output; } // Generate a key function generateOtpKey(seed?: string, size = 20) { if (seed != null) { return Buffer.alloc(size, seed); } // 20 cryptographically random binary bytes (160-bit key) const key = crypto.randomBytes(20); return key; } export function generateTotpSecret( seed?: string, hmacAlgorithm: 'sha1' | 'sha256' | 'sha512' = 'sha1', ) { const size = hmacAlgorithm === 'sha1' ? 20 : hmacAlgorithm === 'sha256' ? 32 : 64; const buffer = generateOtpKey(seed, size); return buffer; } /** * Generate a time based one-time password (TOTP) * @param seed - Seed string * @param hmacAlgorithm - 'sha1' | 'sha256' | 'sha512' * @param time - Timestamp in seconds, default to current time * @returns */ export function totpCode( seed?: string, hmacAlgorithm: 'sha1' | 'sha256' | 'sha512' = 'sha1', time?: number, ) { const secret = generateTotpSecret(seed, hmacAlgorithm); const code = totp({ secret, time, hmacAlgorithm, }); return {code, secret}; } /** * Verify a totp code * @param secret - Secret (raw buffer, not base32 encoded) * @param code - Totp code * @param hmacAlgorithm - 'sha1' | 'sha256' | 'sha512' * @param steps - Allowed steps in delta (30 seconds per step) * @returns */ export function verifyTotpCode( secret: Buffer, code: string, hmacAlgorithm: 'sha1' | 'sha256' | 'sha512' = 'sha1', steps = 2, ) { const now = Math.floor(Date.now() / 1000); for (let i = -steps; i <= steps; i++) { const time = now + i * 30; const token = totp({secret, hmacAlgorithm, time}); if (token === code) { return true; } } return false; } /** * Generate a URI for totp - https://github.com/google/google-authenticator/wiki/Key-Uri-Format * @param issuer - Company name * @param subject - User name * @param seed - Seed * @returns */ export function totpUri(issuer: string, subject: string, seed: string) { const encodedSecret = base32Encode(generateTotpSecret(seed)); const query = new URLSearchParams({ secret: encodedSecret, issuer, // https://github.com/google/google-authenticator/wiki/Key-Uri-Format#algorithm algorithm: 'SHA1', // uppercase is required for Google }); return `otpauth://totp/${issuer}:${subject}?${query}`; } /** * Generate QR code for OTP * @param issuer - Issuer * @param subject - Subject * @param seed - Seed * @param type - png */ export function qrCodeForTotpSecret( issuer: string, subject: string, seed: string, type: 'png', ): Promise; /** * Generate QR code for OTP * @param issuer - Issuer * @param subject - Subject * @param seed - Seed * @param type - terminal or url */ export function qrCodeForTotpSecret( issuer: string, subject: string, seed: string, type: 'terminal' | 'url', ): Promise; export function qrCodeForTotpSecret( issuer: string, subject: string, seed: string, type: 'terminal' | 'png' | 'url' = 'terminal', ) { const uri = totpUri(issuer, subject, seed); switch (type) { case 'png': return qrCodeAsPng(uri); case 'url': return qrCodeAsDataURL(uri); default: return qrCodeAsText(uri); } } export async function qrCodeAsText(data: string | QRCodeSegment[]) { const code = await QRCode.toString(data); return code; } export async function qrCodeAsDataURL(data: string | QRCodeSegment[]) { const code = await QRCode.toDataURL(data); return code; } export async function qrCodeAsPng(data: string | QRCodeSegment[]) { const code = await QRCode.toBuffer(data, {type: 'png'}); return code; }