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);