106 lines
2.6 KiB
JavaScript
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,
|
|
}
|