diff --git a/package-lock.json b/package-lock.json index 05afb6ee6..83e37b584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19447,7 +19447,7 @@ }, "isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/ie/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { diff --git a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js index e755c62bb..c96fcd084 100644 --- a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js +++ b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js @@ -57,7 +57,7 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { this.generateKey(); this.$totpEnabled.on("change", async () => { - this.updateCheckboxOption("totpEnabled", this.$totpEnabled); + this.updateSecret(); }); this.$regenerateTotpButton.on("click", async () => { @@ -79,6 +79,11 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { ); } + async updateSecret() { + if (this.$totpEnabled.prop("checked")) server.post("totp/enable"); + else server.post("totp/disable"); + } + async generateKey() { server.get("totp/generate").then((result) => { if (result.success) { @@ -89,17 +94,6 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { }); } - // async toggleTOTP() { - // if (this.$totpEnabled) - // server.post("totp/enable").then((result) => { - // if (!result.success) toastService.showError(result.message); - // }); - // else - // server.post("totp/disable").then((result) => { - // if (!result.success) toastService.showError(result.message); - // }); - // } - optionsLoaded(options) { server.get("totp/enabled").then((result) => { if (result.success) { @@ -112,10 +106,6 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { } }); - server.get("totp/get").then((result) => { - this.$totpSecretInput.text(result.secret); - }); - this.$protectedSessionTimeout.val(options.protectedSessionTimeout); } diff --git a/src/routes/api/totp.ts b/src/routes/api/totp.ts index 020e7e55c..66ecf65c0 100644 --- a/src/routes/api/totp.ts +++ b/src/routes/api/totp.ts @@ -1,6 +1,7 @@ import options = require("../../services/options"); import totp_secret = require("../../services/encryption/totp_secret"); import { Request } from "express"; +import totp_fs = require("../../services/totp_secret") const speakeasy = require("speakeasy"); function verifyOTPToken(guessedToken: any) { @@ -27,22 +28,23 @@ function checkForTOTP() { } function enableTOTP() { - options.setOption("totpEnab| voidled", true); + options.setOption("totpEnabled", true); return { success: "true" }; } function disableTOTP() { options.setOption("totpEnabled", false); - return { success: "true" }; + + return { success: totp_fs.removeTotpSecret() }; } function setTotpSecret(req: Request) { - console.log("TODO: Save Secret"); - // totp_secret.setTotpSecret(req.body.secret); + options.setOption + totp_fs.saveTotpSecret(req.body.secret) } function getSecret() { - return "TODO: Get Secret"; + return totp_fs.getTotpSecret() } export = { diff --git a/src/routes/login.ts b/src/routes/login.ts index 14f12fdc0..93526d9c1 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -8,14 +8,17 @@ import passwordService = require('../services/encryption/password'); import assetPath = require('../services/asset_path'); import appPath = require('../services/app_path'); import ValidationError = require('../errors/validation_error'); +import totp_secret = require('../services/totp_secret'); import { Request, Response } from 'express'; import { AppRequest } from './route-interface'; +const speakeasy = require("speakeasy") + function loginPage(req: Request, res: Response) { res.render('login', { failedAuth: false, // totpEnabled: optionService.getOption("hasOTPEnabled"), - totpEnabled: false, + totpEnabled: optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret(), assetPath: assetPath, appPath: appPath }); @@ -63,29 +66,54 @@ function login(req: AppRequest, res: Response) { const guessedPassword = req.body.password; const guessedTotp = req.body.token; - if (verifyPassword(guessedPassword)) { - const rememberMe = req.body.rememberMe; - - req.session.regenerate(() => { - if (rememberMe) { - req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks - } else { - req.session.cookie.expires = null; - } - - req.session.loggedIn = true; - res.redirect('.'); - }); + if (!verifyPassword(guessedPassword)) + { + sendLoginError( req, res) + return } - else { + + if (optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret()) + if (!verifyTOTP(guessedTotp)) + { + sendLoginError( req, res) + return + } + + + const rememberMe = req.body.rememberMe; + + req.session.regenerate(() => { + if (rememberMe) { + req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks + } else { + req.session.cookie.expires = null; + } + + req.session.loggedIn = true; + res.redirect('.'); + }); +} + +function sendLoginError(req: AppRequest, res: Response) { // note that logged IP address is usually meaningless since the traffic should come from a reverse proxy - log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); + log.info(`WARNING: Wrong password or TOTP from ${req.ip}, rejecting.`); res.status(401).render('login', { failedAuth: true, + totpEnabled: optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret(), assetPath: assetPath }); - } +} + +function verifyTOTP( guessedToken: string) { + const tokenValidates = speakeasy.totp.verify({ + secret: totp_secret.getTotpSecret(), + encoding: "base32", + token: guessedToken, + window: 1, + }); + + return tokenValidates } function verifyPassword(guessedPassword: string) { diff --git a/src/services/options_init.ts b/src/services/options_init.ts index 49fa85461..493787141 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -97,7 +97,7 @@ const defaultOptions = [ { name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true }, { name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true }, { name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true }, - // { name: 'totpEnabled', value: 'false', isSynced: false } + { name: 'totpEnabled', value: 'false', isSynced: true } ]; function initStartupOptions() { diff --git a/src/services/totp_secret.ts b/src/services/totp_secret.ts new file mode 100644 index 000000000..cc4aa86a5 --- /dev/null +++ b/src/services/totp_secret.ts @@ -0,0 +1,37 @@ +"use strict"; + +import fs = require('fs'); +import crypto = require('crypto'); +import dataDir = require('./data_dir'); +import log = require('./log'); + +const totpSecretPath = `${dataDir.TRILIUM_DATA_DIR}/totp_secret.txt`; + +function saveTotpSecret(secret: string) +{ + if (!fs.existsSync(totpSecretPath)) + log.info("Generated totp secret file"); + + fs.writeFileSync(totpSecretPath, secret, "utf8"); +} + +function getTotpSecret(){ + + return fs.readFileSync(totpSecretPath, "utf8"); +} + +function checkForTotSecret() { + return fs.existsSync(totpSecretPath) +} + +function removeTotpSecret() { + console.log("Attempting to remove secret") + fs.unlink( totpSecretPath, a => { + console.log("Unable to remove totp secret") + console.log(a) + return false + }) + return true +} + +export = {saveTotpSecret, getTotpSecret, checkForTotSecret, removeTotpSecret};