refactor: otplib named imports, cleanup totpService internal naming

This commit is contained in:
null 2026-06-04 04:16:20 -05:00
parent 653dd72e12
commit 92f292dcee
1 changed files with 14 additions and 16 deletions

View File

@ -1,31 +1,30 @@
'use strict'; 'use strict';
const crypto = require('crypto'); const crypto = require('crypto');
const { authenticator } = require('otplib'); const { generateSecret, generateURI, generateSync, verifySync } = require('otplib');
const QRCode = require('qrcode'); const QRCode = require('qrcode');
const { encryptSecret, decryptSecret } = require('./encryptionService'); const { encryptSecret, decryptSecret } = require('./encryptionService');
const APP_NAME = 'Bill Tracker'; const APP_NAME = 'Bill Tracker';
const RECOVERY_CODE_COUNT = 8; 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 newSecret() {
return generateSecret(20);
function generateSecret() {
return authenticator.generateSecret(20);
} }
async function generateQrCode(secret, username) { async function generateQrCode(secret, username) {
const uri = authenticator.keyuri(username, APP_NAME, secret); const uri = generateURI({ secret, account: username, issuer: APP_NAME, type: 'totp' });
const dataUrl = await QRCode.toDataURL(uri, { width: 200, margin: 2 }); const qr_data_url = await QRCode.toDataURL(uri, { width: 200, margin: 2 });
return { uri, qr_data_url: dataUrl }; return { uri, qr_data_url };
} }
function verifyToken(encryptedSecret, token) { function verifyToken(encryptedSecret, token) {
if (!encryptedSecret || !token) return false; if (!encryptedSecret || !token) return false;
try { try {
const secret = decryptSecret(encryptedSecret); 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 { } catch {
return false; return false;
} }
@ -34,13 +33,14 @@ function verifyToken(encryptedSecret, token) {
function verifyTokenRaw(secret, token) { function verifyTokenRaw(secret, token) {
if (!secret || !token) return false; if (!secret || !token) return false;
try { 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 { } catch {
return false; return false;
} }
} }
function generateRecoveryCodes() { function makeRecoveryCodes() {
return Array.from({ length: RECOVERY_CODE_COUNT }, () => { return Array.from({ length: RECOVERY_CODE_COUNT }, () => {
const bytes = crypto.randomBytes(5); const bytes = crypto.randomBytes(5);
const hex = bytes.toString('hex').toUpperCase(); const hex = bytes.toString('hex').toUpperCase();
@ -52,7 +52,6 @@ function hashRecoveryCode(code) {
return crypto.createHash('sha256').update(code.replace(/-/g, '').toUpperCase()).digest('hex'); 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) { function consumeRecoveryCode(db, userId, code) {
const normalized = code.replace(/[-\s]/g, '').toUpperCase(); const normalized = code.replace(/[-\s]/g, '').toUpperCase();
const user = db.prepare('SELECT totp_recovery_codes FROM users WHERE id = ?').get(userId); 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 }; return { used: true, remaining: stored.length };
} }
// Short-lived challenge token issued after password passes, before TOTP is verified.
function createChallenge(db, userId) { function createChallenge(db, userId) {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS) const expiresAt = new Date(Date.now() + CHALLENGE_TTL_MS)
@ -100,11 +98,11 @@ function pruneExpiredChallenges(db) {
} }
module.exports = { module.exports = {
generateSecret, generateSecret: newSecret,
generateQrCode, generateQrCode,
verifyToken, verifyToken,
verifyTokenRaw, verifyTokenRaw,
generateRecoveryCodes, generateRecoveryCodes: makeRecoveryCodes,
hashRecoveryCode, hashRecoveryCode,
consumeRecoveryCode, consumeRecoveryCode,
createChallenge, createChallenge,