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
+
- Please take care to remember your new password. Password is used to encrypt protected notes. If you forget your password, then all your protected notes are forever lost with no recovery options.
+ Please take care to remember your new password. Password is used to encrypt protected notes.
+ If you forget your password, then all your protected notes are forever lost.
+ In case you did forget your password,
click here to reset it.
`;
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
};