Password wizard wip

This commit is contained in:
Mo Bitar
2018-05-17 12:32:05 -05:00
parent 8176fcc3ab
commit d8fbab52bc
9 changed files with 398 additions and 223 deletions

View File

@@ -162,5 +162,12 @@ angular.module('app')
room.showRoom = !room.showRoom;
}
// 002 Update
this.securityUpdateAvailable = function() {
var keys = authManager.keys()
return keys && !keys.ak;
}
});

View File

@@ -9,13 +9,18 @@ class AccountMenu {
};
}
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager,
$timeout, storageManager, $compile, archiveManager) {
'ngInject';
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
$scope.user = authManager.user;
$scope.server = syncManager.serverURL;
$timeout(() => {
$scope.openPasswordWizard("change-pw");
}, 0)
$scope.close = function() {
$timeout(() => {
$scope.closeFunction()();
@@ -29,63 +34,6 @@ class AccountMenu {
$scope.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus;
$scope.newPasswordData = {};
$scope.showPasswordChangeForm = function() {
$scope.newPasswordData.showForm = true;
}
$scope.submitPasswordChange = function() {
let newPass = $scope.newPasswordData.newPassword;
if(!newPass || newPass.length == 0) {
return;
}
if(newPass != $scope.newPasswordData.newPasswordConfirmation) {
alert("Your new password does not match its confirmation.");
$scope.newPasswordData.status = null;
return;
}
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, newPass, 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)
});
})
}, null, "submitPasswordChange")
}
$scope.submitMfaForm = function() {
var params = {};
@@ -204,6 +152,13 @@ class AccountMenu {
}
}
$scope.openPasswordWizard = function(type) {
var scope = $rootScope.$new(true);
scope.type = type;
var el = $compile( "<password-wizard type='type'></password-wizard>" )(scope);
angular.element(document.body).append(el);
}
// Allows indexeddb unencrypted logs to be deleted
// clearAllModels will remove data from backing store, but not from working memory
// See: https://github.com/standardnotes/desktop/issues/131
@@ -345,142 +300,10 @@ class AccountMenu {
Export
*/
function loadZip(callback) {
if(window.zip) {
callback();
return;
}
var scriptTag = document.createElement('script');
scriptTag.src = "/assets/zip/zip.js";
scriptTag.async = false;
var headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
scriptTag.onload = function() {
zip.workerScriptsPath = "assets/zip/";
callback();
}
}
function downloadZippedNotes(notes) {
loadZip(function(){
zip.createWriter(new zip.BlobWriter("application/zip"), function(zipWriter) {
var index = 0;
function nextFile() {
var note = notes[index];
var blob = new Blob([note.text], {type: 'text/plain'});
zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), function() {
index++;
if(index < notes.length) {
nextFile();
} else {
zipWriter.close(function(blob) {
downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
zipWriter = null;
});
}
});
}
nextFile();
}, onerror);
})
}
var textFile = null;
function hrefForData(data) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (textFile !== null) {
window.URL.revokeObjectURL(textFile);
}
textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return textFile;
}
function downloadData(data, fileName) {
var link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
$scope.downloadDataArchive = function() {
// download in Standard File format
var keys, authParams, protocolVersion;
if($scope.archiveFormData.encrypted) {
if(authManager.offline() && passcodeManager.hasPasscode()) {
keys = passcodeManager.keys();
authParams = passcodeManager.passcodeAuthParams();
protocolVersion = authParams.version;
} else {
keys = authManager.keys();
authParams = authManager.getAuthParams();
protocolVersion = authManager.protocolVersion();
}
}
var data = $scope.itemsData(keys, authParams, protocolVersion);
downloadData(data, `SN Archive - ${new Date()}.txt`);
// download as zipped plain text files
if(!keys) {
var notes = modelManager.allItemsMatchingTypes(["Note"]);
downloadZippedNotes(notes);
}
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
}
$scope.itemsData = function(keys, authParams, protocolVersion) {
let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
let blobData = new Blob([data], {type: 'text/json'});
return blobData;
}
// Advanced
$scope.reencryptPressed = function() {
if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) {
return;
}
if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) {
return;
}
modelManager.setAllItemsDirty();
syncManager.sync(function(response){
if(response.error) {
alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.")
return;
}
$timeout(function(){
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
$scope.newPasswordData = {};
}, 1000)
}, null, "reencryptPressed");
}
// 002 Update
$scope.securityUpdateAvailable = function() {
var keys = authManager.keys()
return keys && !keys.ak;
}
/*
Encryption Status
*/

View File

@@ -0,0 +1,165 @@
class PasswordWizard {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/password-wizard.html";
this.scope = {
type: "="
};
}
controller($scope, modelManager, archiveManager, $timeout) {
'ngInject';
const IntroStep = 0;
const BackupStep = 1;
const SignoutStep = 2;
const PasswordStep = 3;
const SyncStep = 4;
const FinishStep = 5;
let DefaultContinueTitle = "Continue";
$scope.continueTitle = DefaultContinueTitle;
$scope.step = PasswordStep;
$scope.titleForStep = function(step) {
switch (step) {
case BackupStep:
return "Download a backup of your data";
case SignoutStep:
return "Sign out of all your devices";
case PasswordStep:
return $scope.changePassword ? "Enter password information" : "Enter your current password";
case SyncStep:
return "Encrypt and sync data with new keys";
default:
return null;
}
}
$scope.configure = function() {
if($scope.type == "change-pw") {
$scope.title = "Change Password";
$scope.changePassword = true;
} else if($scope.type == "upgrade-security") {
$scope.title = "Security Update";
$scope.securityUpdate = true;
}
}();
$scope.continue = function() {
let next = () => {
$scope.step += 1;
$scope.initializeStep($scope.step);
}
var preprocessor = $scope.preprocessorForStep($scope.step);
if(preprocessor) {
preprocessor(() => {
next();
})
} else {
next();
}
}
$scope.downloadBackup = function(encrypted) {
archiveManager.downloadBackup(encrypted);
}
$scope.preprocessorForStep = function(step) {
if(step == PasswordStep) {
return (callback) => {
$scope.showSpinner = true;
$scope.continueTitle = "Generating Keys...";
$timeout(() => {
$scope.validatePasswordInformation(() => {
$scope.showSpinner = false;
$scope.continueTitle = DefaultContinueTitle;
callback();
});
})
}
}
}
$scope.initializeStep = function(step) {
if(step == SyncStep) {
$scope.lockContinue = true;
$scope.resyncData(() => {
$scope.lockContinue = false;
})
}
}
$scope.validatePasswordInformation = function(callback) {
$timeout(() => {
callback();
}, 1000)
}
$scope.resyncData = function(callback) {
$timeout(() => {
callback();
}, 2000)
}
$scope.submitPasswordChange = function() {
let newPass = $scope.newPasswordData.newPassword;
let currentPass = $scope.newPasswordData.currentPassword;
if(!newPass || newPass.length == 0) {
return;
}
if(newPass != $scope.newPasswordData.newPasswordConfirmation) {
alert("Your new password does not match its confirmation.");
$scope.newPasswordData.status = null;
return;
}
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, currentPass, newPass, 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)
});
})
}, null, "submitPasswordChange")
}
}
}
angular.module('app').directive('passwordWizard', () => new PasswordWizard);

View File

@@ -0,0 +1,117 @@
class ArchiveManager {
constructor(passcodeManager, authManager, modelManager) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.modelManager = modelManager;
}
/*
Public
*/
downloadBackup(encrypted) {
// download in Standard File format
var keys, authParams, protocolVersion;
if(encrypted) {
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
keys = this.passcodeManager.keys();
authParams = this.passcodeManager.passcodeAuthParams();
protocolVersion = authParams.version;
} else {
keys = this.authManager.keys();
authParams = this.authManager.getAuthParams();
protocolVersion = this.authManager.protocolVersion();
}
}
var data = this.__itemsData(keys, authParams, protocolVersion);
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
// download as zipped plain text files
if(!keys) {
var notes = this.modelManager.allItemsMatchingTypes(["Note"]);
this.__downloadZippedNotes(notes);
}
}
/*
Private
*/
__itemsData(keys, authParams, protocolVersion) {
let data = this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
let blobData = new Blob([data], {type: 'text/json'});
return blobData;
}
__loadZip(callback) {
if(window.zip) {
callback();
return;
}
var scriptTag = document.createElement('script');
scriptTag.src = "/assets/zip/zip.js";
scriptTag.async = false;
var headTag = document.getElementsByTagName('head')[0];
headTag.appendChild(scriptTag);
scriptTag.onload = function() {
zip.workerScriptsPath = "assets/zip/";
callback();
}
}
__downloadZippedNotes(notes) {
this.__loadZip(() => {
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
var index = 0;
let nextFile = () => {
var note = notes[index];
var blob = new Blob([note.text], {type: 'text/plain'});
zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), () => {
index++;
if(index < notes.length) {
nextFile();
} else {
zipWriter.close((blob) => {
this.__downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
zipWriter = null;
});
}
});
}
nextFile();
}, onerror);
})
}
__hrefForData(data) {
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (this.textFile !== null) {
window.URL.revokeObjectURL(this.textFile);
}
this.textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return this.textFile;
}
__downloadData(data, fileName) {
var link = document.createElement('a');
link.setAttribute('download', fileName);
link.href = this.__hrefForData(data);
document.body.appendChild(link);
link.click();
link.remove();
}
}
angular.module('app').service('archiveManager', ArchiveManager);

View File

@@ -82,6 +82,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
display: block !important;
}
.inline {
display: inline-block;
}
.wrap {
word-wrap: break-word;
word-break: break-all;

View File

@@ -74,35 +74,29 @@
.panel-row
%a.panel-row.condensed{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password
.notification.warning{"ng-if" => "newPasswordData.changePassword"}
%h1.title Change Password
.text
%p 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 If you have thousands of items, this can take several minutes — you must keep the application window open during this process.
%p After changing your password, you must log out of all other applications currently signed in to your account.
%p.bold It is highly recommended you download a backup of your data before proceeding.
.panel-row{"ng-if" => "!newPasswordData.status"}
.horizontal-group{"ng-if" => "!newPasswordData.showForm"}
%a.red{"ng-click" => "showPasswordChangeForm()"} Continue
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
.panel-row{"ng-if" => "newPasswordData.showForm"}
%form.panel-form.stretch
%input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"}
%input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"}
.button-group.stretch.panel-row.form-submit
.button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"}
.label Submit
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
%p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
%a.panel-row.condensed{"ng-click" => "showAdvanced = !showAdvanced"} Advanced
%div{"ng-if" => "showAdvanced"}
%a.panel-row{"ng-click" => "reencryptPressed()"} Resync All Items
%a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password
-# .notification.warning{"ng-if" => "newPasswordData.changePassword"}
-# %h1.title Change Password
-# .text
-# %p 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 If you have thousands of items, this can take several minutes — you must keep the application window open during this process.
-# %p After changing your password, you must log out of all other applications currently signed in to your account.
-# %p.bold It is highly recommended you download a backup of your data before proceeding.
-# .panel-row{"ng-if" => "!newPasswordData.status"}
-# .horizontal-group{"ng-if" => "!newPasswordData.showForm"}
-# %a.red{"ng-click" => "showPasswordChangeForm()"} Continue
-# %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
-# .panel-row{"ng-if" => "newPasswordData.showForm"}
-# %form.panel-form.stretch
-# %input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"}
-# %input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"}
-# .button-group.stretch.panel-row.form-submit
-# .button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"}
-# .label Submit
-# %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
-#
-# %p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
%a.panel-row.condensed{"ng-if" => "securityUpdateAvailable()", "ng-click" => "clickedSecurityUpdate()"} Security Update Available
.notification.default{"ng-if" => "securityUpdateData.showForm"}

View File

@@ -0,0 +1,65 @@
.modal.medium
.content
.sn-component
.panel
.header
%h1.title {{title}}
%a.close-button{"ng-click" => "close()"} Close
.content
%div{"ng-if" => "step == 0"}
%div{"ng-if" => "changePassword"}
%p 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 If you have thousands of items, this can take several minutes—you must keep the application window open during this process.
%div{"ng-if" => "securityUpdate"}
%p Welcome to the security update process.
%p.info Press Continue to proceed.
.panel-section{"ng-if" => "step > 0"}
%h3.title.panel-row Step {{step}} — {{titleForStep(step)}}
%div{"ng-if" => "step == 1"}
%p.panel-row
The entirety of your data will be re-encrypted and re-uploaded to your account. This is a generally safe process,
but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail.
It's best to be on the safe side before large operations like this.
.panel-row
.button-group
.button.info{"ng-click" => "downloadBackup(true)"}
.label Download Encrypted Backup
.button.info{"ng-click" => "downloadBackup(false)"}
.label Download Decrypted Backup
%div{"ng-if" => "step == 2"}
%p.panel-row
As a result of this process, your encryption keys will change.
Any devices on which you use Standard Notes will need to end their session. After this process completes, you'll be asked to sign back in.
%p.bold.panel-row Please sign out of all applications (excluding this one), including desktop, web, and mobile (iOS and Android).
%p.panel-row Press Continue only when you have completed signing out of all your devices.
%div{"ng-if" => "step == 3"}
%div{"ng-if" => "changePassword"}
%div{"ng-if" => "securityUpdate"}
%p.panel-row Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys.
.panel-row
%form
%input.form-control{:type => 'password', "ng-model" => "formData.currentPassword", "placeholder" => "Current Password", "sn-autofocus" => "true", "should-focus" => "true"}
%input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPassword", "placeholder" => "New Password"}
%input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPasswordConfirmation", "placeholder" => "Confirm New Password"}
%div{"ng-if" => "step == 4"}
%p.panel-row
Your data is being re-encrypted with your new keys and synced to your account.
%p.panel-row.danger
Do not close this window until this process completes.
.footer
%a.right{"ng-click" => "continue()", "ng-class" => "{'disabled' : lockContinue}"}
.spinner.small.inline.info{"ng-if" => "showSpinner", "style" => "margin-right: 5px;"}
{{continueTitle}}

6
package-lock.json generated
View File

@@ -5785,9 +5785,9 @@
"dev": true
},
"sn-stylekit": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/sn-stylekit/-/sn-stylekit-1.0.12.tgz",
"integrity": "sha512-CkCGj6pS2FB9tF5uVbqMlmKpQ1wJFCcXAri2B4nCq8aAv2D5frUhhPFJ1oCnFgBssB9LxnbW2+C2CQojVCL9ig==",
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/sn-stylekit/-/sn-stylekit-1.0.14.tgz",
"integrity": "sha512-0jx2hJOw8Qer/aqcyXSses5g1m+nNDtDkwFkonolyVheTyhJXZrCw4kIIV9tbeOspwqsVjR12pk7L+R2mnl61Q==",
"dev": true
},
"snake-case": {

View File

@@ -33,7 +33,7 @@
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.2",
"sn-stylekit": "1.0.12",
"sn-stylekit": "1.0.14",
"standard-file-js": "file:~/Desktop/sf/sfjs"
},
"license": "GPL-3.0"