Merge pull request #69 from standardnotes/change-pw

Change pw
This commit is contained in:
Mo Bitar
2017-02-10 15:15:17 -06:00
committed by GitHub
8 changed files with 160 additions and 98 deletions

View File

@@ -73,13 +73,12 @@ angular.module('app.frontend')
}
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
var mk = keys.mk;
var requestUrl = url + "/auth/sign_in";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: keys.pw, email: email};
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
callback(response);
}.bind(this))
.catch(function(response){
@@ -91,7 +90,9 @@ angular.module('app.frontend')
}
this.handleAuthResponse = function(response, email, url, authParams, mk, pw) {
localStorage.setItem("server", url);
if(url) {
localStorage.setItem("server", url);
}
localStorage.setItem("user", JSON.stringify(response.plain().user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
localStorage.setItem("mk", mk);
@@ -101,13 +102,12 @@ angular.module('app.frontend')
this.register = function(url, email, password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
var mk = keys.mk;
var requestUrl = url + "/auth";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = _.merge({password: keys.pw, email: email}, authParams);
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, email, url, authParams, mk, keys.pw);
this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw);
callback(response);
}.bind(this))
.catch(function(response){
@@ -117,55 +117,26 @@ angular.module('app.frontend')
}.bind(this));
}
// this.changePassword = function(current_password, new_password) {
// this.getAuthParamsForEmail(email, function(authParams){
// if(!authParams) {
// callback(null);
// return;
// }
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: current_password, email: user.email}, authParams), function(currentKeys) {
// Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: new_password, email: user.email}, authParams), function(newKeys){
// var data = {};
// data.current_password = currentKeys.pw;
// data.password = newKeys.pw;
// data.password_confirmation = newKeys.pw;
//
// var user = this.user;
//
// this._performPasswordChange(currentKeys, newKeys, function(response){
// if(response && !response.error) {
// // this.showNewPasswordForm = false;
// // reencrypt data with new mk
// this.reencryptAllItemsAndSave(user, newKeys.mk, currentKeys.mk, function(success){
// if(success) {
// this.setMk(newKeys.mk);
// alert("Your password has been changed and your data re-encrypted.");
// } else {
// // rollback password
// this._performPasswordChange(newKeys, currentKeys, function(response){
// alert("There was an error changing your password. Your password has been rolled back.");
// window.location.reload();
// })
// }
// }.bind(this));
// } else {
// // this.showNewPasswordForm = false;
// alert("There was an error changing your password. Please try again.");
// }
// }.bind(this))
// }.bind(this));
// }.bind(this));
// }.bind(this));
// }
this.changePassword = function(email, new_password, callback) {
Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
var requestUrl = localStorage.getItem("server") + "/auth/change_pw";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = _.merge({new_password: keys.pw}, authParams);
_.merge(request, params);
this._performPasswordChange = function(url, email, current_keys, new_keys, callback) {
var requestUrl = url + "/auth";
var request = Restangular.oneUrl(requestUrl, requestUrl);
var params = {password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email};
_.merge(request, params);
request.patch().then(function(response){
callback(response);
})
request.post().then(function(response){
this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw);
callback(response.plain());
}.bind(this))
.catch(function(response){
var error = response.data;
if(!error) {
error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}
}
console.log("Change pw error", response);
callback({error: error});
})
}.bind(this));
}
this.staticifyObject = function(object) {

View File

@@ -20,11 +20,15 @@ angular
});
function showSpinner() {
if(scope.hidePromise) {
$timeout.cancel(scope.hidePromise);
scope.hidePromise = null;
}
showElement(true);
}
function hideSpinner() {
$timeout(showElement.bind(this, false), getDelay());
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
}
function showElement(show) {

View File

@@ -15,10 +15,6 @@ class AccountMenu {
$scope.syncStatus = syncManager.syncStatus;
$scope.changePasswordPressed = function() {
$scope.showNewPasswordForm = !$scope.showNewPasswordForm;
}
$scope.encryptionKey = function() {
return syncManager.masterKey;
}
@@ -31,20 +27,57 @@ class AccountMenu {
return `${$scope.server}/dashboard/?server=${$scope.server}&id=${$scope.user.email}&pw=${$scope.serverPassword()}`;
}
$scope.newPasswordData = {};
$scope.showPasswordChangeForm = function() {
$scope.newPasswordData.showForm = true;
}
$scope.submitPasswordChange = function() {
$scope.passwordChangeData.status = "Generating New Keys...";
$timeout(function(){
if(data.password != data.password_confirmation) {
alert("Your new password does not match its confirmation.");
return;
}
if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) {
alert("Your new password does not match its confirmation.");
$scope.newPasswordData.status = null;
return;
}
authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
var email = $scope.user.email;
if(!email) {
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
$scope.newPasswordData.status = null;
return;
}
$scope.newPasswordData.status = "Generating New Keys...";
$scope.newPasswordData.showForm = false;
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
syncManager.sync(function(response){
authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){
if(response.error) {
alert("There was an error changing your password. Please try again.");
$scope.newPasswordData.status = null;
return;
}
// re-encrypt all items
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
modelManager.setAllItemsDirty();
syncManager.sync(function(response){
if(response.error) {
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
return;
}
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
$timeout(function(){
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
$scope.newPasswordData = {};
}, 1000)
});
})
})
}
$scope.loginSubmitPressed = function() {
@@ -116,6 +149,8 @@ class AccountMenu {
$scope.importData = null;
if(!response) {
alert("There was an error importing your data. Please try again.");
} else {
alert("Your data was successfully imported.")
}
})
})
@@ -149,8 +184,6 @@ class AccountMenu {
}
$scope.importJSONData = function(data, password, callback) {
console.log("Importing data", data);
var onDataReady = function() {
var items = modelManager.mapResponseItemsToLocalModels(data.items);
items.forEach(function(item){

View File

@@ -1,14 +1,8 @@
class EncryptionHelper {
static encryptItem(item, key) {
var item_key = null;
if(item.enc_item_key) {
// we reuse the key, but this is optional
item_key = Neeto.crypto.decryptText(item.enc_item_key, key);
} else {
item_key = Neeto.crypto.generateRandomEncryptionKey();
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
}
var item_key = Neeto.crypto.generateRandomEncryptionKey();
item.enc_item_key = Neeto.crypto.encryptText(item_key, key);
var ek = Neeto.crypto.firstHalfOfKey(item_key);
var ak = Neeto.crypto.secondHalfOfKey(item_key);

View File

@@ -8,6 +8,7 @@ class ModelManager {
this.itemChangeObservers = [];
this.items = [];
this._extensions = [];
this.acceptableContentTypes = ["Note", "Tag", "Extension"];
}
get allItems() {
@@ -62,7 +63,7 @@ class ModelManager {
for (var json_obj of items) {
json_obj = _.omit(json_obj, omitFields || [])
var item = this.findItem(json_obj["uuid"]);
if(json_obj["deleted"] == true) {
if(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
if(item) {
this.removeItemLocally(item)
}
@@ -225,6 +226,17 @@ class ModelManager {
item.removeAllRelationships();
}
/* Used when changing encryption key */
setAllItemsDirty() {
var relevantItems = this.allItems.filter(function(item){
return _.includes(this.acceptableContentTypes, item.content_type);
}.bind(this));
for(var item of relevantItems) {
item.setDirty(true);
}
}
removeItemLocally(item, callback) {
_.pull(this.items, item);

View File

@@ -98,10 +98,37 @@ class SyncManager {
return this._cursorToken;
}
get queuedCallbacks() {
if(!this._queuedCallbacks) {
this._queuedCallbacks = [];
}
return this._queuedCallbacks;
}
clearQueuedCallbacks() {
this._queuedCallbacks = [];
}
callQueuedCallbacksAndCurrent(currentCallback, response) {
var allCallbacks = this.queuedCallbacks;
if(currentCallback) {
allCallbacks.push(currentCallback);
}
if(allCallbacks.length) {
for(var eachCallback of allCallbacks) {
eachCallback(response);
}
this.clearQueuedCallbacks();
}
}
sync(callback, options = {}) {
if(this.syncStatus.syncOpInProgress) {
this.repeatOnCompletion = true;
if(callback) {
this.queuedCallbacks.push(callback);
}
console.log("Sync op in progress; returning.");
return;
}
@@ -116,18 +143,17 @@ class SyncManager {
return;
}
var isContinuationSync = this.needsMoreSync;
var isContinuationSync = this.syncStatus.needsMoreSync;
this.repeatOnCompletion = false;
this.syncStatus.syncOpInProgress = true;
let submitLimit = 100;
var subItems = allDirtyItems.slice(0, submitLimit);
if(subItems.length < allDirtyItems.length) {
// more items left to be synced, repeat
this.needsMoreSync = true;
this.syncStatus.needsMoreSync = true;
} else {
this.needsMoreSync = false;
this.syncStatus.needsMoreSync = false;
}
if(!isContinuationSync) {
@@ -153,10 +179,10 @@ class SyncManager {
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
// merge only metadata for saved items
// Update 2/9/17: I just realized we may not need to handle saved_items anymore. We used to do this because we wanted to merge presentation-related metadata,
// but that has since been removed. Since this function is an important part of the functioning of the app, I'm not going to remove it just yet without careful
// testing.
// we write saved items to disk now because it clears their dirty status then saves
// if we saved items before completion, we had have to save them as dirty and save them again on success as clean
var omitFields = ["content", "auth_hash"];
var saved = this.handleItemsResponse(response.saved_items, omitFields);
@@ -172,14 +198,17 @@ class SyncManager {
this.syncToken = response.sync_token;
this.cursorToken = response.cursor_token;
if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) {
if(this.cursorToken || this.syncStatus.needsMoreSync) {
setTimeout(function () {
this.sync(callback, options);
}.bind(this), 10); // wait 10ms to allow UI to update
} else if(this.repeatOnCompletion) {
this.repeatOnCompletion = false;
setTimeout(function () {
this.sync(callback, options);
}.bind(this), 10); // wait 10ms to allow UI to update
} else {
if(callback) {
callback(response);
}
this.callQueuedCallbacksAndCurrent(callback, response);
}
}.bind(this))
@@ -193,9 +222,7 @@ class SyncManager {
this.$rootScope.$broadcast("sync:error", error);
if(callback) {
callback({error: "Sync error"});
}
this.callQueuedCallbacksAndCurrent(callback, {error: "Sync error"});
}.bind(this))
}
@@ -214,7 +241,7 @@ class SyncManager {
var item = this.modelManager.findItem(itemResponse.uuid);
var error = mapping.error;
if(error.tag == "uuid_conflict") {
// uuid conflicts can occur if a user attempts to import an old data archive with uuids form the old account into a new account
// uuid conflicts can occur if a user attempts to import an old data archive with uuids from the old account into a new account
this.modelManager.alternateUUIDForItem(item, handleNext);
}
++i;

View File

@@ -124,6 +124,10 @@
font-weight: bold !important;
}
.italic {
font-style: italic !important;
}
.normal {
font-weight: normal !important;
}

View File

@@ -28,6 +28,13 @@
%div{"ng-if" => "user"}
%h2 {{user.email}}
%p {{server}}
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
.spinner.inline.mr-5.blue
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
%a.block.mt-15{"href" => "{{dashboardURL()}}", "target" => "_blank"} → Standard File Dashboard
%a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials
%section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"}
%label.block
@@ -36,13 +43,23 @@
%label.block.mt-5.mb-0
Server password:
.wrap.normal.mt-1 {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}}
%a.block.mt-5{"href" => "{{dashboardURL()}}", "target" => "_blank"} Standard File Dashboard
%div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"}
.spinner.inline.mr-5.blue
Syncing
%span{"ng-if" => "syncStatus.total > 0"}: {{syncStatus.current}}/{{syncStatus.total}}
%p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
%a.block.mt-5{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password
%section.gray-bg.mt-10.medium-padding{"ng-if" => "newPasswordData.changePassword"}
%p.bold Change Password (Beta)
%p.mt-10 Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key.
%p.mt-5 If you have thousands of items, this can take several minutes — you must keep the application window open during this process.
%p.mt-5 After changing your password, you must log out of all other applications currently signed in to your account.
%p.bold.mt-5 It is highly recommended you download a backup of your data before proceeding.
%div.mt-10{"ng-if" => "!newPasswordData.status"}
%a.red.mr-5{"ng-if" => "!newPasswordData.showForm", "ng-click" => "showPasswordChangeForm()"} Continue
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
%div.mt-10{"ng-if" => "newPasswordData.showForm"}
%form
%input.form-control{"type" => "text", "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"}
%input.form-control{"type" => "text", "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"}
%button.btn.dark-button.btn-block{"ng-click" => "submitPasswordChange()"} Submit
%p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
.medium-v-space