refactor: otplib named imports, cleanup totpService internal naming
This commit is contained in:
parent
653dd72e12
commit
92f292dcee
|
|
@ -1,31 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { authenticator } = require('otplib');
|
||||
const { generateSecret, generateURI, generateSync, verifySync } = require('otplib');
|
||||
const QRCode = require('qrcode');
|
||||
const { encryptSecret, decryptSecret } = require('./encryptionService');
|
||||
|
||||
const APP_NAME = 'Bill Tracker';
|
||||
const RECOVERY_CODE_COUNT = 8;
|
||||
const CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
authenticator.options = { window: 1 }; // accept ±1 time step for clock drift
|
||||
|
||||
function generateSecret() {
|
||||
return authenticator.generateSecret(20);
|
||||
function newSecret() {
|
||||
return generateSecret(20);
|
||||
}
|
||||
|
||||
async function generateQrCode(secret, username) {
|
||||
const uri = authenticator.keyuri(username, APP_NAME, secret);
|
||||
const dataUrl = await QRCode.toDataURL(uri, { width: 200, margin: 2 });
|
||||
return { uri, qr_data_url: dataUrl };
|
||||
const uri = generateURI({ secret, account: username, issuer: APP_NAME, type: 'totp' });
|
||||
const qr_data_url = await QRCode.toDataURL(uri, { width: 200, margin: 2 });
|
||||
return { uri, qr_data_url };
|
||||
}
|
||||
|
||||
function verifyToken(encryptedSecret, token) {
|
||||
if (!encryptedSecret || !token) return false;
|
||||
try {
|
||||
const secret = decryptSecret(encryptedSecret);
|
||||
return authenticator.verify({ token: String(token).replace(/\s/g, ''), secret });
|
||||
const result = verifySync({ secret, token: String(token).replace(/\s/g, ''), type: 'totp' });
|
||||
return result?.valid === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -34,13 +33,14 @@ function verifyToken(encryptedSecret, token) {
|
|||
function verifyTokenRaw(secret, token) {
|
||||
if (!secret || !token) return false;
|
||||
try {
|
||||
return authenticator.verify({ token: String(token).replace(/\s/g, ''), secret });
|
||||
const result = verifySync({ secret, token: String(token).replace(/\s/g, ''), type: 'totp' });
|
||||
return result?.valid === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function generateRecoveryCodes() {
|
||||
function makeRecoveryCodes() {
|
||||
return Array.from({ length: RECOVERY_CODE_COUNT }, () => {
|
||||
const bytes = crypto.randomBytes(5);
|
||||
const hex = bytes.toString('hex').toUpperCase();
|
||||
|
|
@ -52,7 +52,6 @@ function hashRecoveryCode(code) {
|
|||
return crypto.createHash('sha256').update(code.replace(/-/g, '').toUpperCase()).digest('hex');
|
||||
}
|
||||
|
||||
// Returns { used: true } if a matching unused recovery code was found and consumed.
|
||||
function consumeRecoveryCode(db, userId, code) {
|
||||
const normalized = code.replace(/[-\s]/g, '').toUpperCase();
|
||||
const user = db.prepare('SELECT totp_recovery_codes FROM users WHERE id = ?').get(userId);
|
||||
|
|
@ -76,7 +75,6 @@ function consumeRecoveryCode(db, userId, code) {
|
|||
return { used: true, remaining: stored.length };
|
||||
}
|
||||
|
||||
// Short-lived challenge token issued after password passes, before TOTP is verified.
|
||||
function createChallenge(db, userId) {
|
||||
const id = crypto.randomUUID();
|
||||
const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
|
||||
|
|
@ -100,11 +98,11 @@ function pruneExpiredChallenges(db) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
generateSecret,
|
||||
generateSecret: newSecret,
|
||||
generateQrCode,
|
||||
verifyToken,
|
||||
verifyTokenRaw,
|
||||
generateRecoveryCodes,
|
||||
generateRecoveryCodes: makeRecoveryCodes,
|
||||
hashRecoveryCode,
|
||||
consumeRecoveryCode,
|
||||
createChallenge,
|
||||
|
|
|
|||
Loading…
Reference in New Issue