const crypto = require('crypto') const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' function base32Encode (buffer) { let bits = 0 let value = 0 let output = '' for (let i = 0; i < buffer.length; i += 1) { value = (value << 8) | buffer[i] bits += 8 while (bits >= 5) { output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31] bits -= 5 } } if (bits > 0) { output += BASE32_ALPHABET[(value << (5 - bits)) & 31] } return output } function base32Decode (input) { const normalized = String(input || '') .toUpperCase() .replace(/=+$/g, '') .replace(/[^A-Z2-7]/g, '') let bits = 0 let value = 0 const output = [] for (let i = 0; i < normalized.length; i += 1) { const idx = BASE32_ALPHABET.indexOf(normalized[i]) if (idx === -1) continue value = (value << 5) | idx bits += 5 if (bits >= 8) { output.push((value >>> (bits - 8)) & 255) bits -= 8 } } return Buffer.from(output) } function generateSecret (byteLength = 20) { return base32Encode(crypto.randomBytes(byteLength)) } function hotp (secret, counter) { const key = base32Decode(secret) const buffer = Buffer.alloc(8) let tmp = counter for (let i = 7; i >= 0; i -= 1) { buffer[i] = tmp & 0xff tmp = Math.floor(tmp / 256) } const hmac = crypto.createHmac('sha1', key).update(buffer).digest() const offset = hmac[hmac.length - 1] & 0x0f const code = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff) return String(code % 1_000_000).padStart(6, '0') } function generateTotp (secret, timeStep = 30, timestamp = Date.now()) { const counter = Math.floor(timestamp / 1000 / timeStep) return hotp(secret, counter) } function verifyTotp (secret, token, window = 1, timeStep = 30) { if (!secret || token == null) return false const normalized = String(token).replace(/\D/g, '').padStart(6, '0') if (normalized.length !== 6) return false const counter = Math.floor(Date.now() / 1000 / timeStep) for (let offset = -window; offset <= window; offset += 1) { if (hotp(secret, counter + offset) === normalized) { return true } } return false } function buildOtpAuthUrl ({ secret, accountName, issuer = 'Modstagram' }) { const label = encodeURIComponent(`${issuer}:${accountName}`) const issuerParam = encodeURIComponent(issuer) return `otpauth://totp/${label}?secret=${secret}&issuer=${issuerParam}&algorithm=SHA1&digits=6&period=30` } module.exports = { generateSecret, generateTotp, verifyTotp, buildOtpAuthUrl, }