Merge privileges

This commit is contained in:
Mo Bitar
2018-12-11 12:52:15 -06:00
32 changed files with 1115 additions and 210 deletions

View File

@@ -22,7 +22,8 @@ angular.module('app')
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager,
syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory, privilegesManager) {
this.spellcheck = true;
this.componentManager = componentManager;
@@ -386,16 +387,28 @@ angular.module('app')
}
}
this.deleteNote = function() {
if(this.note.locked) {
alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
return;
this.deleteNote = async function() {
let run = () => {
$timeout(() => {
if(this.note.locked) {
alert("This note is locked. If you'd like to delete it, unlock it, and try again.");
return;
}
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
if(confirm(`Are you sure you want to delete ${title}?`)) {
this.remove()(this.note);
this.showMenu = false;
}
});
}
let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note";
if(confirm(`Are you sure you want to delete ${title}?`)) {
this.remove()(this.note);
this.showMenu = false;
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
run();
});
} else {
run();
}
}

View File

@@ -23,7 +23,8 @@ angular.module('app')
}
})
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) {
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager,
privilegesManager) {
authManager.checkForSecurityUpdate().then((available) => {
this.securityUpdateAvailable = available;
@@ -172,7 +173,31 @@ angular.module('app')
}
}
this.selectRoom = function(room) {
room.showRoom = !room.showRoom;
this.selectRoom = async function(room) {
let run = () => {
$timeout(() => {
room.showRoom = !room.showRoom;
})
}
if(!room.showRoom) {
// About to show, check if has privileges
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => {
run();
});
} else {
run();
}
} else {
run();
}
}
this.clickOutsideAccountMenu = function() {
if(privilegesManager.authenticationInProgress()) {
return;
}
this.showAccountMenu = false;
}
});

View File

@@ -1,6 +1,7 @@
angular.module('app')
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) {
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager,
privilegesManager) {
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
@@ -83,14 +84,14 @@ angular.module('app')
syncManager.loadLocalItems().then(() => {
$timeout(() => {
$scope.allTag.didLoad = true;
$rootScope.$broadcast("initial-data-loaded");
$rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly.
syncManager.sync();
// refresh every 30s
setInterval(function () {
syncManager.sync();
}, 30000);
})
syncManager.sync();
// refresh every 30s
setInterval(function () {
syncManager.sync();
}, 30000);
});
authManager.addEventHandler((event) => {

View File

@@ -41,7 +41,6 @@ class LockScreen {
})
}
}
}
angular.module('app').directive('lockScreen', () => new LockScreen);

View File

@@ -31,7 +31,8 @@ angular.module('app')
}
}
})
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager, desktopManager) {
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager,
storageManager, desktopManager, privilegesManager) {
this.panelController = {};
@@ -198,19 +199,31 @@ angular.module('app')
}
}
this.selectNote = function(note, viaClick = false) {
this.selectNote = async function(note, viaClick = false) {
if(!note) {
this.createNewNote();
return;
}
this.selectedNote = note;
note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
let run = () => {
$timeout(() => {
this.selectedNote = note;
note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text);
if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text);
}
})
}
if(note.locked && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewLockedNotes)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewLockedNotes, () => {
run();
});
} else {
run();
}
}

View File

@@ -2,9 +2,6 @@ angular.module('app').directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
// elem.css('overflow-x', 'hidden');
// elem.css('height', 'inherit');
var offset = parseInt(attrs.threshold) || 0;
var e = elem[0]

View File

@@ -0,0 +1,15 @@
angular
.module('app')
.directive('snEnter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.snEnter, {'event': event});
});
event.preventDefault();
}
});
};
});

View File

@@ -10,7 +10,7 @@ class AccountMenu {
}
controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager,
$timeout, $compile, archiveManager) {
$timeout, $compile, archiveManager, privilegesManager) {
'ngInject';
$scope.formData = {mergeLocal: true, ephemeral: false};
@@ -38,7 +38,6 @@ class AccountMenu {
}
$scope.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() {
@@ -167,10 +166,27 @@ class AccountMenu {
$scope.openPasswordWizard = function(type) {
// Close the account menu
$scope.close();
authManager.presentPasswordWizard(type);
}
$scope.openPrivilegesModal = async function() {
$scope.close();
let run = () => {
$timeout(() => {
privilegesManager.presentPrivilegesManagementModal();
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePrivileges)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePrivileges, () => {
run();
});
} else {
run();
}
}
// 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
@@ -229,36 +245,49 @@ class AccountMenu {
})
}
$scope.importFileSelected = function(files) {
$scope.importData = {};
$scope.importFileSelected = async function(files) {
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
try {
var data = JSON.parse(e.target.result);
$timeout(function(){
if(data.auth_params) {
// request password
$scope.importData.requestPassword = true;
$scope.importData.data = data;
let run = () => {
$timeout(() => {
$scope.importData = {};
$timeout(() => {
var element = document.getElementById("import-password-request");
if(element) {
element.scrollIntoView(false);
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
try {
var data = JSON.parse(e.target.result);
$timeout(function(){
if(data.auth_params) {
// request password
$scope.importData.requestPassword = true;
$scope.importData.data = data;
$timeout(() => {
var element = document.getElementById("import-password-request");
if(element) {
element.scrollIntoView(false);
}
})
} else {
$scope.performImport(data, null);
}
})
} else {
$scope.performImport(data, null);
} catch (e) {
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
}
})
} catch (e) {
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
}
}
reader.readAsText(file);
})
}
reader.readAsText(file);
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {
run();
}
}
$scope.importJSONData = function(data, password, callback) {
@@ -316,8 +345,18 @@ class AccountMenu {
Export
*/
$scope.downloadDataArchive = function() {
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
$scope.downloadDataArchive = async function() {
let run = () => {
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
run();
});
} else {
run();
}
}
/*
@@ -362,6 +401,35 @@ class AccountMenu {
Passcode Lock
*/
$scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
$scope.reloadAutoLockInterval = function() {
passcodeManager.getAutoLockInterval().then((interval) => {
$timeout(() => {
$scope.selectedAutoLockInterval = interval;
})
})
}
$scope.reloadAutoLockInterval();
$scope.selectAutoLockInterval = async function(interval) {
let run = async () => {
await passcodeManager.setAutoLockInterval(interval);
$timeout(() => {
$scope.reloadAutoLockInterval();
});
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}
$scope.hasPasscode = function() {
return passcodeManager.hasPasscode();
}
@@ -377,10 +445,10 @@ class AccountMenu {
return;
}
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode : passcodeManager.setPasscode;
let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode.bind(passcodeManager) : passcodeManager.setPasscode.bind(passcodeManager);
fn(passcode, () => {
$timeout(function(){
$timeout(() => {
$scope.formData.showPasscodeForm = false;
var offline = authManager.offline();
@@ -393,27 +461,51 @@ class AccountMenu {
})
}
$scope.changePasscodePressed = function() {
$scope.formData.changingPasscode = true;
$scope.addPasscodeClicked();
$scope.formData.changingPasscode = false;
$scope.changePasscodePressed = async function() {
let run = () => {
$timeout(() => {
$scope.formData.changingPasscode = true;
$scope.addPasscodeClicked();
$scope.formData.changingPasscode = false;
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}
$scope.removePasscodePressed = function() {
var signedIn = !authManager.offline();
var message = "Are you sure you want to remove your local passcode?";
if(!signedIn) {
message += " This will remove encryption from your local data.";
}
if(confirm(message)) {
passcodeManager.clearPasscode();
$scope.removePasscodePressed = async function() {
let run = () => {
$timeout(() => {
var signedIn = !authManager.offline();
var message = "Are you sure you want to remove your local passcode?";
if(!signedIn) {
message += " This will remove encryption from your local data.";
}
if(confirm(message)) {
passcodeManager.clearPasscode();
if(authManager.offline()) {
syncManager.markAllItemsDirtyAndSaveOffline();
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
// we don't want to write unencrypted data to disk.
// $rootScope.$broadcast("major-data-change");
}
if(authManager.offline()) {
syncManager.markAllItemsDirtyAndSaveOffline();
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
// we don't want to write unencrypted data to disk.
// $rootScope.$broadcast("major-data-change");
}
}
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
}
}

View File

@@ -7,6 +7,7 @@ class MenuRow {
this.scope = {
action: "&",
circle: "=",
circleAlign: "=",
label: "=",
subtitle: "=",
hasButton: "=",

View File

@@ -0,0 +1,97 @@
/*
The purpose of the conflict resoltion modal is to present two versions of a conflicted item,
and allow the user to choose which to keep (or to keep both.)
*/
class PrivilegesAuthModal {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/privileges-auth-modal.html";
this.scope = {
action: "=",
onSuccess: "=",
onCancel: "=",
};
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
}
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
'ngInject';
$scope.authenticationParameters = {};
$scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions();
privilegesManager.getSelectedSessionLength().then((length) => {
$timeout(() => {
$scope.selectedSessionLength = length;
})
})
$scope.selectSessionLength = function(length) {
$scope.selectedSessionLength = length;
}
privilegesManager.netCredentialsForAction($scope.action).then((credentials) => {
$timeout(() => {
$scope.requiredCredentials = credentials.sort();
});
});
$scope.promptForCredential = function(credential) {
return privilegesManager.displayInfoForCredential(credential).prompt;
}
$scope.cancel = function() {
$scope.dismiss();
$scope.onCancel && $scope.onCancel();
}
$scope.isCredentialInFailureState = function(credential) {
if(!$scope.failedCredentials) {
return false;
}
return $scope.failedCredentials.find((candidate) => {
return candidate == credential;
}) != null;
}
$scope.validate = function() {
var failed = [];
for(var cred of $scope.requiredCredentials) {
var value = $scope.authenticationParameters[cred];
if(!value || value.length == 0) {
failed.push(cred);
}
}
$scope.failedCredentials = failed;
return failed.length == 0;
}
$scope.submit = function() {
if(!$scope.validate()) {
return;
}
privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => {
$timeout(() => {
if(result.success) {
privilegesManager.setSessionLength($scope.selectedSessionLength);
$scope.onSuccess();
$scope.dismiss();
} else {
$scope.failedCredentials = result.failedCredentials;
}
})
})
}
}
}
angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal);

View File

@@ -0,0 +1,88 @@
class PrivilegesManagementModal {
constructor() {
this.restrict = "E";
this.templateUrl = "directives/privileges-management-modal.html";
this.scope = {
};
}
link($scope, el, attrs) {
$scope.dismiss = function() {
el.remove();
}
}
controller($scope, privilegesManager, passcodeManager, authManager, $timeout) {
'ngInject';
$scope.dummy = {};
$scope.hasPasscode = passcodeManager.hasPasscode();
$scope.hasAccount = !authManager.offline();
$scope.displayInfoForCredential = function(credential) {
let info = privilegesManager.displayInfoForCredential(credential);
if(credential == PrivilegesManager.CredentialLocalPasscode) {
info["availability"] = $scope.hasPasscode;
} else if(credential == PrivilegesManager.CredentialAccountPassword) {
info["availability"] = $scope.hasAccount;
} else {
info["availability"] = true;
}
return info;
}
$scope.displayInfoForAction = function(action) {
return privilegesManager.displayInfoForAction(action).label;
}
$scope.isCredentialRequiredForAction = function(action, credential) {
if(!$scope.privileges) {
return false;
}
return $scope.privileges.isCredentialRequiredForAction(action, credential);
}
$scope.clearSession = function() {
privilegesManager.clearSession().then(() => {
$scope.reloadPrivileges();
})
}
$scope.reloadPrivileges = async function() {
$scope.availableActions = privilegesManager.getAvailableActions();
$scope.availableCredentials = privilegesManager.getAvailableCredentials();
let sessionEndDate = await privilegesManager.getSessionExpirey();
$scope.sessionExpirey = sessionEndDate.toLocaleString();
$scope.sessionExpired = new Date() >= sessionEndDate;
$scope.credentialDisplayInfo = {};
for(let cred of $scope.availableCredentials) {
$scope.credentialDisplayInfo[cred] = $scope.displayInfoForCredential(cred);
}
privilegesManager.getPrivileges().then((privs) => {
$timeout(() => {
$scope.privileges = privs;
})
})
}
$scope.checkboxValueChanged = function(action, credential) {
$scope.privileges.toggleCredentialForAction(action, credential);
privilegesManager.savePrivileges();
}
$scope.reloadPrivileges();
$scope.cancel = function() {
$scope.dismiss();
$scope.onCancel && $scope.onCancel();
}
}
}
angular.module('app').directive('privilegesManagementModal', () => new PrivilegesManagementModal);

View File

@@ -0,0 +1,34 @@
class SNPrivileges extends SFItem {
setCredentialsForAction(action, credentials) {
this.content.desktopPrivileges[action] = credentials;
}
getCredentialsForAction(action) {
return this.content.desktopPrivileges[action] || [];
}
toggleCredentialForAction(action, credential) {
if(this.isCredentialRequiredForAction(action, credential)) {
this.removeCredentialForAction(action, credential);
} else {
this.addCredentialForAction(action, credential);
}
}
removeCredentialForAction(action, credential) {
_.pull(this.content.desktopPrivileges[action], credential);
}
addCredentialForAction(action, credential) {
var credentials = this.getCredentialsForAction(action);
credentials.push(credential);
this.setCredentialsForAction(action, credentials);
}
isCredentialRequiredForAction(action, credential) {
var credentialsRequired = this.getCredentialsForAction(action);
return credentialsRequired.includes(credential);
}
}

View File

@@ -92,6 +92,13 @@ class AuthManager extends SFAuthManager {
}
}
async verifyAccountPassword(password) {
let authParams = await this.getAuthParams();
let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams);
let success = keys.mk === (await this.keys()).mk;
return success;
}
async checkForSecurityUpdate() {
if(this.offline()) {
return false;
@@ -128,6 +135,7 @@ class AuthManager extends SFAuthManager {
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
// console.log("Loaded existing user prefs", resolvedSingleton.uuid);
this.userPreferences = resolvedSingleton;
this.userPreferencesDidChange();
}, (valueCallback) => {

View File

@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
super($timeout);
this.setJWTRequestHandler(async () => {
return storageManager.getItem("jwt");;
return storageManager.getItem("jwt");
})
}
}

View File

@@ -7,7 +7,8 @@ SFModelManager.ContentTypeClassMapping = {
"SN|Theme" : SNTheme,
"SN|Component" : SNComponent,
"SF|Extension" : SNServerExtension,
"SF|MFA" : SNMfa
"SF|MFA" : SNMfa,
"SN|Privileges" : SNPrivileges
};
SFItem.AppDomain = "org.standardnotes.sn";

View File

@@ -1,100 +1,186 @@
angular.module('app')
.provider('passcodeManager', function () {
class PasscodeManager {
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager);
}
constructor(authManager, storageManager) {
document.addEventListener('visibilitychange', (e) => {
this.documentVisibilityChanged(document.visibilityState);
});
function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
this.authManager = authManager;
this.storageManager = storageManager;
this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
this._locked = this._hasPasscode;
this.isLocked = function() {
return this._locked;
}
const MillisecondsPerSecond = 1000;
PasscodeManager.AutoLockIntervalNone = 0;
PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
this.hasPasscode = function() {
return this._hasPasscode;
}
PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
}
this.keys = function() {
return this._keys;
}
this.passcodeAuthParams = function() {
var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed));
if(authParams && !authParams.version) {
var keys = this.keys();
if(keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
authParams.version = "002";
} else {
authParams.version = "001";
}
getAutoLockIntervalOptions() {
return [
{
value: PasscodeManager.AutoLockIntervalNone,
label: "None"
},
{
value: PasscodeManager.AutoLockIntervalOneMinute,
label: "1 Min"
},
{
value: PasscodeManager.AutoLockIntervalFiveMinutes,
label: "5 Min"
},
{
value: PasscodeManager.AutoLockIntervalOneHour,
label: "1 Hr"
}
return authParams;
}
]
}
this.unlock = function(passcode, callback) {
var params = this.passcodeAuthParams();
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
if(keys.pw !== params.hash) {
callback(false);
return;
}
this._keys = keys;
this._authParams = params;
this.decryptLocalStorage(keys, params).then(() => {
this._locked = false;
callback(true);
})
});
}
this.setPasscode = (passcode, callback) => {
var uuid = SFJS.crypto.generateUUIDSync();
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
let keys = results.keys;
let authParams = results.authParams;
authParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;
this._authParams = authParams;
// Encrypting will initially clear localStorage
this.encryptLocalStorage(keys, authParams);
// After it's cleared, it's safe to write to it
storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
callback(true);
});
}
this.changePasscode = (newPasscode, callback) => {
this.setPasscode(newPasscode, callback);
}
this.clearPasscode = function() {
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
storageManager.removeItem("offlineParams", StorageManager.Fixed);
this._keys = null;
this._hasPasscode = false;
}
this.encryptLocalStorage = function(keys, authParams) {
storageManager.setKeys(keys, authParams);
// Switch to Ephemeral storage, wiping Fixed storage
// Last argument is `force`, which we set to true because in the case of changing passcode
storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
}
this.decryptLocalStorage = async function(keys, authParams) {
storageManager.setKeys(keys, authParams);
return storageManager.decryptStorage();
documentVisibilityChanged(visbility) {
let visible = document.visibilityState == "visible";
if(!visible) {
this.beginAutoLockTimer();
} else {
this.cancelAutoLockTimer();
}
}
});
async beginAutoLockTimer() {
var interval = await this.getAutoLockInterval();
if(interval == PasscodeManager.AutoLockIntervalNone) {
return;
}
this.lockTimeout = setTimeout(() => {
this.lockApplication();
}, interval);
}
cancelAutoLockTimer() {
clearTimeout(this.lockTimeout);
}
lockApplication() {
window.location.reload();
this.cancelAutoLockTimer();
}
isLocked() {
return this._locked;
}
hasPasscode() {
return this._hasPasscode;
}
keys() {
return this._keys;
}
async setAutoLockInterval(interval) {
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
}
async getAutoLockInterval() {
let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
if(interval) {
return JSON.parse(interval);
} else {
return PasscodeManager.AutoLockIntervalNone;
}
}
passcodeAuthParams() {
var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed));
if(authParams && !authParams.version) {
var keys = this.keys();
if(keys && keys.ak) {
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams.
authParams.version = "002";
} else {
authParams.version = "001";
}
}
return authParams;
}
async verifyPasscode(passcode) {
return new Promise(async (resolve, reject) => {
var params = this.passcodeAuthParams();
let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params);
if(keys.pw !== params.hash) {
resolve(false);
} else {
resolve(true);
}
})
}
unlock(passcode, callback) {
var params = this.passcodeAuthParams();
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
if(keys.pw !== params.hash) {
callback(false);
return;
}
this._keys = keys;
this._authParams = params;
this.decryptLocalStorage(keys, params).then(() => {
this._locked = false;
callback(true);
})
});
}
setPasscode(passcode, callback) {
var uuid = SFJS.crypto.generateUUIDSync();
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => {
let keys = results.keys;
let authParams = results.authParams;
authParams.hash = keys.pw;
this._keys = keys;
this._hasPasscode = true;
this._authParams = authParams;
// Encrypting will initially clear localStorage
this.encryptLocalStorage(keys, authParams);
// After it's cleared, it's safe to write to it
this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
callback(true);
});
}
changePasscode(newPasscode, callback) {
this.setPasscode(newPasscode, callback);
}
clearPasscode() {
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral
this.storageManager.removeItem("offlineParams", StorageManager.Fixed);
this._keys = null;
this._hasPasscode = false;
}
encryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
// Switch to Ephemeral storage, wiping Fixed storage
// Last argument is `force`, which we set to true because in the case of changing passcode
this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true);
}
async decryptLocalStorage(keys, authParams) {
this.storageManager.setKeys(keys, authParams);
return this.storageManager.decryptStorage();
}
}
angular.module('app').service('passcodeManager', PasscodeManager);

View File

@@ -0,0 +1,307 @@
class PrivilegesManager {
constructor(passcodeManager, authManager, singletonManager, modelManager, storageManager, $rootScope, $compile) {
this.passcodeManager = passcodeManager;
this.authManager = authManager;
this.singletonManager = singletonManager;
this.modelManager = modelManager;
this.storageManager = storageManager;
this.$rootScope = $rootScope;
this.$compile = $compile;
this.loadPrivileges();
PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword";
PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode";
PrivilegesManager.ActionManageExtensions = "ActionManageExtensions";
PrivilegesManager.ActionManageBackups = "ActionManageBackups";
PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes";
PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
PrivilegesManager.ActionManagePasscode = "ActionManagePasscode";
PrivilegesManager.ActionDeleteNote = "ActionDeleteNote";
PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey";
PrivilegesManager.SessionLengthKey = "SessionLengthKey";
PrivilegesManager.SessionLengthNone = 0;
PrivilegesManager.SessionLengthFiveMinutes = 300;
PrivilegesManager.SessionLengthOneHour = 3600;
PrivilegesManager.SessionLengthOneWeek = 604800;
this.availableActions = [
PrivilegesManager.ActionManagePrivileges,
PrivilegesManager.ActionManageExtensions,
PrivilegesManager.ActionManageBackups,
PrivilegesManager.ActionManagePasscode,
PrivilegesManager.ActionViewLockedNotes,
PrivilegesManager.ActionDeleteNote
]
this.availableCredentials = [
PrivilegesManager.CredentialAccountPassword,
PrivilegesManager.CredentialLocalPasscode
];
this.sessionLengths = [
PrivilegesManager.SessionLengthNone,
PrivilegesManager.SessionLengthFiveMinutes,
PrivilegesManager.SessionLengthOneHour,
PrivilegesManager.SessionLengthOneWeek,
PrivilegesManager.SessionLengthIndefinite
]
}
getAvailableActions() {
return this.availableActions;
}
getAvailableCredentials() {
return this.availableCredentials;
}
presentPrivilegesModal(action, onSuccess, onCancel) {
if(this.authenticationInProgress()) {
onCancel && onCancel();
return;
}
let customSuccess = () => {
onSuccess && onSuccess();
this.currentAuthenticationElement = null;
}
let customCancel = () => {
onCancel && onCancel();
this.currentAuthenticationElement = null;
}
var scope = this.$rootScope.$new(true);
scope.action = action;
scope.onSuccess = customSuccess;
scope.onCancel = customCancel;
var el = this.$compile( "<privileges-auth-modal action='action' on-success='onSuccess' on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>" )(scope);
angular.element(document.body).append(el);
this.currentAuthenticationElement = el;
}
async netCredentialsForAction(action) {
let credentials = (await this.getPrivileges()).getCredentialsForAction(action);
let netCredentials = [];
for(var cred of credentials) {
if(cred == PrivilegesManager.CredentialAccountPassword) {
if(!this.authManager.offline()) {
netCredentials.push(cred);
}
} else if(cred == PrivilegesManager.CredentialLocalPasscode) {
if(this.passcodeManager.hasPasscode()) {
netCredentials.push(cred);
}
}
}
return netCredentials;
}
presentPrivilegesManagementModal() {
var scope = this.$rootScope.$new(true);
var el = this.$compile( "<privileges-management-modal class='sk-modal'></privileges-management-modal>")(scope);
angular.element(document.body).append(el);
}
authenticationInProgress() {
return this.currentAuthenticationElement != null;
}
async loadPrivileges() {
return new Promise((resolve, reject) => {
let prefsContentType = "SN|Privileges";
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
this.privileges = resolvedSingleton;
if(!this.privileges.content.desktopPrivileges) {
this.privileges.content.desktopPrivileges = {};
}
console.log("Resolved existing privs", resolvedSingleton.uuid);
resolve(resolvedSingleton);
}, (valueCallback) => {
// Safe to create. Create and return object.
var privs = new SNPrivileges({content_type: prefsContentType});
this.modelManager.addItem(privs);
privs.setDirty(true);
this.$rootScope.sync();
valueCallback(privs);
console.log("Creating new privs", privs.uuid);
resolve(privs);
});
});
}
async getPrivileges() {
if(this.privileges) {
return this.privileges;
} else {
return this.loadPrivileges();
}
}
displayInfoForCredential(credential) {
let metadata = {}
metadata[PrivilegesManager.CredentialAccountPassword] = {
label: "Account Password",
prompt: "Please enter your account password."
}
metadata[PrivilegesManager.CredentialLocalPasscode] = {
label: "Local Passcode",
prompt: "Please enter your local passcode."
}
return metadata[credential];
}
displayInfoForAction(action) {
let metadata = {};
metadata[PrivilegesManager.ActionManageExtensions] = {
label: "Manage Extensions"
};
metadata[PrivilegesManager.ActionManageBackups] = {
label: "Download/Import Backups"
};
metadata[PrivilegesManager.ActionViewLockedNotes] = {
label: "View Locked Notes"
};
metadata[PrivilegesManager.ActionManagePrivileges] = {
label: "Manage Privileges"
};
metadata[PrivilegesManager.ActionManagePasscode] = {
label: "Manage Passcode"
}
metadata[PrivilegesManager.ActionDeleteNote] = {
label: "Delete Notes"
}
return metadata[action];
}
getSessionLengthOptions() {
return [
{
value: PrivilegesManager.SessionLengthNone,
label: "Don't Remember"
},
{
value: PrivilegesManager.SessionLengthFiveMinutes,
label: "5 Minutes"
},
{
value: PrivilegesManager.SessionLengthOneHour,
label: "1 Hour"
},
{
value: PrivilegesManager.SessionLengthOneWeek,
label: "1 Week"
}
]
}
async setSessionLength(length) {
let addToNow = (seconds) => {
let date = new Date();
date.setSeconds(date.getSeconds() + seconds);
return date;
}
let expiresAt = addToNow(length);
return Promise.all([
this.storageManager.setItem(PrivilegesManager.SessionExpiresAtKey, JSON.stringify(expiresAt), StorageManager.FixedEncrypted),
this.storageManager.setItem(PrivilegesManager.SessionLengthKey, JSON.stringify(length), StorageManager.FixedEncrypted),
])
}
async clearSession() {
return this.setSessionLength(PrivilegesManager.SessionLengthNone);
}
async getSelectedSessionLength() {
let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, StorageManager.FixedEncrypted);
if(length) {
return JSON.parse(length);
} else {
return PrivilegesManager.SessionLengthNone;
}
}
async getSessionExpirey() {
let expiresAt = await this.storageManager.getItem(PrivilegesManager.SessionExpiresAtKey, StorageManager.FixedEncrypted);
if(expiresAt) {
return new Date(JSON.parse(expiresAt));
} else {
return new Date();
}
}
async actionRequiresPrivilege(action) {
let expiresAt = await this.getSessionExpirey();
if(expiresAt > new Date()) {
return false;
}
return (await this.netCredentialsForAction(action)).length > 0;
}
async savePrivileges() {
let privs = await this.getPrivileges();
privs.setDirty(true);
this.$rootScope.sync();
}
async authenticateAction(action, credentialAuthMapping) {
var requiredCredentials = (await this.netCredentialsForAction(action));
var successfulCredentials = [], failedCredentials = [];
for(let requiredCredential of requiredCredentials) {
var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]);
if(passesAuth) {
successfulCredentials.push(requiredCredential);
} else {
failedCredentials.push(requiredCredential);
}
}
return {
success: failedCredentials.length == 0,
successfulCredentials: successfulCredentials,
failedCredentials: failedCredentials
}
}
async _verifyAuthenticationParameters(credential, value) {
let verifyAccountPassword = async (password) => {
return this.authManager.verifyAccountPassword(password);
}
let verifyLocalPasscode = async (passcode) => {
return this.passcodeManager.verifyPasscode(passcode);
}
if(credential == PrivilegesManager.CredentialAccountPassword) {
return verifyAccountPassword(value);
} else if(credential == PrivilegesManager.CredentialLocalPasscode) {
return verifyLocalPasscode(value);
}
}
}
angular.module('app').service('privilegesManager', PrivilegesManager);

View File

@@ -108,7 +108,6 @@ class SingletonManager {
var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
}
}
} else {