full
This commit is contained in:
105
utils/totp.js
Normal file
105
utils/totp.js
Normal file
@@ -0,0 +1,105 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user