diff --git a/src/public/app/dialogs/options/credentials.js b/src/public/app/dialogs/options/credentials.js index cc2ebe2fb..f2050fb75 100644 --- a/src/public/app/dialogs/options/credentials.js +++ b/src/public/app/dialogs/options/credentials.js @@ -3,14 +3,16 @@ import protectedSessionHolder from "../../services/protected_session_holder.js"; import toastService from "../../services/toast.js"; const TPL = ` -

Change password

+

-
+
@@ -25,22 +27,41 @@ const TPL = `
- +
`; export default class ChangePasswordOptions { constructor() { $("#options-credentials").html(TPL); + this.$passwordHeading = $("#password-heading"); this.$form = $("#change-password-form"); this.$oldPassword = $("#old-password"); this.$newPassword1 = $("#new-password1"); this.$newPassword2 = $("#new-password2"); + this.$savePasswordButton = $("#save-password-button"); + this.$resetPasswordButton = $("#reset-password-button"); + + this.$resetPasswordButton.on("click", async () => { + if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) { + await server.post("password/reset?really=yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes"); + + const options = await server.get('options'); + this.optionsLoaded(options); + + alert("Password has been reset. Please set new password"); + } + }); this.$form.on('submit', () => this.save()); } optionsLoaded(options) { + const isPasswordSet = options.isPasswordSet === 'true'; + + $("#old-password-form-group").toggle(isPasswordSet); + this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password'); + this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password'); } save() { diff --git a/src/routes/api/options.js b/src/routes/api/options.js index a2ff192ae..30bdd6e85 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -67,6 +67,8 @@ function getOptions() { } } + resultMap['isPasswordSet'] = !!optionMap['passwordVerificationHash'] ? 'true' : 'false'; + return resultMap; } diff --git a/src/routes/api/password.js b/src/routes/api/password.js index 3478c457f..32bfbffc6 100644 --- a/src/routes/api/password.js +++ b/src/routes/api/password.js @@ -1,11 +1,26 @@ "use strict"; -const changePasswordService = require('../../services/change_password'); +const passwordService = require('../../services/password.js'); function changePassword(req) { - return changePasswordService.changePassword(req.body.current_password, req.body.new_password); + if (passwordService.isPasswordSet()) { + return passwordService.changePassword(req.body.current_password, req.body.new_password); + } + else { + return passwordService.setPassword(req.body.new_password); + } +} + +function resetPassword(req) { + // protection against accidental call (not a security measure) + if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") { + return [400, "Incorrect password reset confirmation"]; + } + + return passwordService.resetPassword(); } module.exports = { - changePassword + changePassword, + resetPassword }; diff --git a/src/routes/login.js b/src/routes/login.js index 40581f24b..80d81c1e2 100644 --- a/src/routes/login.js +++ b/src/routes/login.js @@ -4,8 +4,7 @@ const utils = require('../services/utils'); const optionService = require('../services/options'); const myScryptService = require('../services/my_scrypt'); const log = require('../services/log'); -const sqlInit = require("../services/sql_init.js"); -const optionsInitService = require("../services/options_init.js"); +const passwordService = require("../services/password.js"); function loginPage(req, res) { res.render('login', { failedAuth: false }); @@ -16,7 +15,7 @@ function setPasswordPage(req, res) { } function setPassword(req, res) { - if (sqlInit.isPasswordSet()) { + if (passwordService.isPasswordSet()) { return [400, "Password has been already set"]; } @@ -37,7 +36,7 @@ function setPassword(req, res) { return; } - optionsInitService.initPassword(password1); + passwordService.setPassword(password1); res.redirect('login'); } diff --git a/src/routes/routes.js b/src/routes/routes.js index b76997d07..4b88aaf12 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -290,6 +290,7 @@ function register(app) { apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); + apiRoute(POST, '/api/password/reset', passwordApiRoute.resetPassword); apiRoute(POST, '/api/sync/test', syncApiRoute.testSync); apiRoute(POST, '/api/sync/now', syncApiRoute.syncNow); diff --git a/src/services/auth.js b/src/services/auth.js index 05da7b7e7..70a3448bb 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -5,8 +5,8 @@ const log = require('./log'); const sqlInit = require('./sql_init'); const utils = require('./utils'); const passwordEncryptionService = require('./password_encryption'); -const optionService = require('./options'); const config = require('./config'); +const passwordService = require("./password.js"); const noAuthentication = config.General && config.General.noAuthentication === true; @@ -15,7 +15,7 @@ function checkAuth(req, res, next) { res.redirect("setup"); } else if (!req.session.loggedIn && !utils.isElectron() && !noAuthentication) { - if (sqlInit.isPasswordSet()) { + if (passwordService.isPasswordSet()) { res.redirect("login"); } else { res.redirect("set-password"); @@ -56,7 +56,7 @@ function checkAppInitialized(req, res, next) { } function checkPasswordSet(req, res, next) { - if (!utils.isElectron() && !sqlInit.isPasswordSet()) { + if (!utils.isElectron() && !passwordService.isPasswordSet()) { res.redirect("set-password"); } else { next(); diff --git a/src/services/change_password.js b/src/services/change_password.js deleted file mode 100644 index b06e71328..000000000 --- a/src/services/change_password.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; - -const sql = require('./sql'); -const optionService = require('./options'); -const myScryptService = require('./my_scrypt'); -const utils = require('./utils'); -const passwordEncryptionService = require('./password_encryption'); - -function changePassword(currentPassword, newPassword) { - if (!passwordEncryptionService.verifyPassword(currentPassword)) { - return { - success: false, - message: "Given current password doesn't match hash" - }; - } - - sql.transactional(() => { - const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword); - - optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32)); - optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32)); - - const newPasswordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(newPassword)); - - passwordEncryptionService.setDataKey(newPassword, decryptedDataKey); - - optionService.setOption('passwordVerificationHash', newPasswordVerificationKey); - }); - - return { - success: true - }; -} - -module.exports = { - changePassword -}; diff --git a/src/services/options_init.js b/src/services/options_init.js index acc74a7e7..cb4848404 100644 --- a/src/services/options_init.js +++ b/src/services/options_init.js @@ -1,6 +1,4 @@ const optionService = require('./options'); -const passwordEncryptionService = require('./password_encryption'); -const myScryptService = require('./my_scrypt'); const appInfo = require('./app_info'); const utils = require('./utils'); const log = require('./log'); @@ -12,19 +10,6 @@ function initDocumentOptions() { optionService.createOption('documentSecret', utils.randomSecureToken(16), false); } -function initPassword(password) { - optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true); - optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true); - - const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true); - optionService.createOption('passwordVerificationHash', passwordVerificationKey, true); - - // passwordEncryptionService expects these options to already exist - optionService.createOption('encryptedDataKey', '', true); - - passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true); -} - function initNotSyncedOptions(initialized, opts = {}) { optionService.createOption('openTabs', JSON.stringify([ { @@ -127,7 +112,6 @@ function getKeyboardDefaultOptions() { module.exports = { initDocumentOptions, - initPassword, initNotSyncedOptions, initStartupOptions }; diff --git a/src/services/password.js b/src/services/password.js new file mode 100644 index 000000000..f88de5062 --- /dev/null +++ b/src/services/password.js @@ -0,0 +1,83 @@ +"use strict"; + +const sql = require('./sql'); +const optionService = require('./options'); +const myScryptService = require('./my_scrypt'); +const utils = require('./utils'); +const passwordEncryptionService = require('./password_encryption'); + +function isPasswordSet() { + return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'"); +} + +function changePassword(currentPassword, newPassword) { + if (!isPasswordSet()) { + throw new Error("Password has not been set yet, so it cannot be changed. Use 'setPassword' instead."); + } + + if (!passwordEncryptionService.verifyPassword(currentPassword)) { + return { + success: false, + message: "Given current password doesn't match hash" + }; + } + + sql.transactional(() => { + const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword); + + optionService.setOption('passwordVerificationSalt', utils.randomSecureToken(32)); + optionService.setOption('passwordDerivedKeySalt', utils.randomSecureToken(32)); + + const newPasswordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(newPassword)); + + passwordEncryptionService.setDataKey(newPassword, decryptedDataKey); + + optionService.setOption('passwordVerificationHash', newPasswordVerificationKey); + }); + + return { + success: true + }; +} + +function setPassword(password) { + if (isPasswordSet()) { + throw new Error("Password is set already. Either change it or perform 'reset password' first."); + } + + optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true); + optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true); + + const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true); + optionService.createOption('passwordVerificationHash', passwordVerificationKey, true); + + // passwordEncryptionService expects these options to already exist + optionService.createOption('encryptedDataKey', '', true); + + passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true); + + return { + success: true + }; +} + +function resetPassword() { + // user forgot the password, + sql.transactional(() => { + optionService.setOption('passwordVerificationSalt', ''); + optionService.setOption('passwordDerivedKeySalt', ''); + optionService.setOption('encryptedDataKey', ''); + optionService.setOption('passwordVerificationHash', ''); + }); + + return { + success: true + }; +} + +module.exports = { + isPasswordSet, + changePassword, + setPassword, + resetPassword +}; diff --git a/src/services/sql_init.js b/src/services/sql_init.js index 5393b11ea..ae71390d4 100644 --- a/src/services/sql_init.js +++ b/src/services/sql_init.js @@ -30,10 +30,6 @@ function isDbInitialized() { return initialized === 'true'; } -function isPasswordSet() { - return !!sql.getValue("SELECT value FROM options WHERE name = 'passwordVerificationHash'"); -} - async function initDbConnection() { if (!isDbInitialized()) { log.info(`DB not initialized, please visit setup page` + @@ -93,6 +89,7 @@ async function createInitialDatabase() { optionsInitService.initDocumentOptions(); optionsInitService.initNotSyncedOptions(true, {}); optionsInitService.initStartupOptions(); + require("./password").resetPassword(); }); log.info("Importing demo content ..."); @@ -175,6 +172,5 @@ module.exports = { isDbInitialized, createInitialDatabase, createDatabaseForSync, - setDbAsInitialized, - isPasswordSet + setDbAsInitialized };