」 feat: added 2fa (closes #21)

This commit is contained in:
2025-07-19 16:32:09 +02:00
parent ff8a4863ba
commit e523c1710c
11 changed files with 234 additions and 15 deletions

View File

@ -16,10 +16,12 @@ if (!env || env === 'development') {
*/
function prepareDB() {
database.exec(`
CREATE TABLE IF NOT EXISTS credentials (
username TEXT PRIMARY KEY,
passwordHash TEXT
) STRICT
CREATE TABLE IF NOT EXISTS credentials (
username TEXT PRIMARY KEY,
passwordHash TEXT,
totpHash TEXT DEFAULT NULL,
totpEnabled INTEGER DEFAULT 0
) STRICT
`);
}
@ -49,11 +51,48 @@ function passwordQuery(user) {
return passwordQuery.get(user)
}
function setTOTPSecret(user, secret) {
let setTOTP = database.prepare('UPDATE credentials SET totpHash = ? WHERE username = ?');
setTOTP.run(secret, user);
}
function isTOTPEnabled(user) {
const stmt = database.prepare('SELECT totpHash, totpEnabled FROM credentials WHERE username = ?');
const result = stmt.get(user);
return result && result.totpHash !== null && result.totpEnabled === 1;
}
function disableTOTP(user) {
let stmt = database.prepare('UPDATE credentials SET totpHash = NULL, totpEnabled = 0 WHERE username = ?');
stmt.run(user);
}
function queryTOTP(user) {
let totpQuery = database.prepare('SELECT totpHash FROM credentials WHERE username = ?;');
return totpQuery.get(user);
}
function enableTOTP(user) {
let stmt = database.prepare('UPDATE credentials SET totpEnabled = 1 WHERE username = ?');
stmt.run(user);
}
function getUser(user) {
const stmt = database.prepare('SELECT * FROM credentials WHERE username = ?');
return stmt.get(user);
}
const authDB = {
prepareDB,
checkUser,
addUser,
passwordQuery,
setTOTPSecret,
isTOTPEnabled,
disableTOTP,
queryTOTP,
enableTOTP,
getUser,
RESERVED_USERNAMES
};

43
src/utils/totp.js Normal file
View File

@ -0,0 +1,43 @@
import { randomBytes, createHmac } from 'crypto';
import base32 from 'base32.js';
const { Decoder, Encoder } = base32;
const timeStep = 30;
const T0 = 0;
export function generateRandomSecret() {
const buf = randomBytes(20);
return new Encoder().write(buf).finalize();
}
export function getTimeCounter() {
return Math.floor((Date.now() / 1000 - T0) / timeStep);
}
export function verifyTOTP(base32Secret, userToken) {
const window = 1;
const decoder = new Decoder();
const key = decoder.write(base32Secret).finalize();
const currentCounter = getTimeCounter();
for (let errorWindow = -window; errorWindow <= window; errorWindow++) {
const counter = currentCounter + errorWindow;
const generated = generateTOTP(key, counter);
if (generated === userToken) return true;
}
return false;
}
function generateTOTP(key, counter) {
const buf = Buffer.alloc(8);
buf.writeUInt32BE(0, 0);
buf.writeUInt32BE(counter, 4);
const hmac = createHmac('sha1', key).update(buf).digest();
const offset = hmac[hmac.length - 1] & 0xf;
const code = (hmac.readUInt32BE(offset) & 0x7fffffff) % 1_000_000;
return code.toString().padStart(6, '0');
}