Files
modstagram-back/utils/totp.js
payacom 3f416a7a60 full
2026-08-31 20:45:41 +03:30

106 lines
2.6 KiB
JavaScript

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,
}