Autolock wip

This commit is contained in:
Mo Bitar
2018-11-13 15:05:57 -06:00
parent 9835992e16
commit 091f4cff7f
11 changed files with 258 additions and 119 deletions

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

@@ -38,7 +38,6 @@ class AccountMenu {
}
$scope.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() {
@@ -375,6 +374,24 @@ class AccountMenu {
Passcode Lock
*/
$scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
$scope.reloadAutoLockInterval = function() {
passcodeManager.getAutoLockInterval().then((interval) => {
$timeout(() => {
$scope.selectedAutoLockInterval = interval;
console.log("selectedAutoLockInterval", $scope.selectedAutoLockInterval);
})
})
}
$scope.reloadAutoLockInterval();
$scope.selectAutoLockInterval = async function(interval) {
await passcodeManager.setAutoLockInterval(interval);
$scope.reloadAutoLockInterval();
}
$scope.hasPasscode = function() {
return passcodeManager.hasPasscode();
}

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

@@ -1,112 +1,183 @@
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', () => {
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.verifyPasscode = async function(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);
}
})
}
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() {
console.log("beginAutoLockTimer");
var interval = await this.getAutoLockInterval();
this.lockTimeout = setTimeout(() => {
this.lockApplication();
}, interval);
}
cancelAutoLockTimer() {
console.log("cancelAutoLockTimer");
clearTimeout(this.lockTimeout);
}
lockApplication() {
console.log("lockApplication");
window.location.reload();
this.cancelAutoLockTimer();
}
isLocked() {
return this._locked;
}
hasPasscode() {
return this._hasPasscode;
}
keys() {
return this._keys;
}
async setAutoLockInterval(interval) {
console.log("Set autolock interval", interval);
return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.Fixed);
}
async getAutoLockInterval() {
let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.Fixed);
console.log("Got interval", interval);
return interval && JSON.parse(interval);
}
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

@@ -15,10 +15,14 @@ class PrivilegesManager {
PrivilegesManager.ActionManageExtensions = "ActionManageExtensions";
PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup";
PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes";
PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
this.availableActions = [
PrivilegesManager.ActionManageExtensions,
PrivilegesManager.ActionDownloadBackup
PrivilegesManager.ActionDownloadBackup,
PrivilegesManager.ActionViewLockedNotes,
PrivilegesManager.ActionManagePrivileges
]
this.availableCredentials = [
@@ -119,10 +123,19 @@ class PrivilegesManager {
metadata[PrivilegesManager.ActionManageExtensions] = {
label: "Manage Extensions"
}
metadata[PrivilegesManager.ActionDownloadBackup] = {
label: "Download Backups"
};
metadata[PrivilegesManager.ActionViewLockedNotes] = {
label: "View Locked Notes"
};
metadata[PrivilegesManager.ActionManagePrivileges] = {
label: "Manage Privileges"
}
return metadata[action];
}

View File

@@ -1,4 +1,11 @@
$heading-height: 75px;
#editor-column {
.locked {
opacity: 0.8;
}
}
.editor {
flex: 1 50%;
display: flex;
@@ -7,10 +14,6 @@ $heading-height: 75px;
background-color: white;
}
.locked {
opacity: 0.8;
}
#editor-title-bar {
width: 100%;

View File

@@ -12,6 +12,10 @@
}
}
#privileges-modal {
width: 700px;
}
#password-wizard {
font-size: 16px;
}

View File

@@ -130,7 +130,12 @@
.horizontal-group
%a.info{"ng-click" => "changePasscodePressed()"} Change Passcode
%a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode
.panel-row
.horizontal-group
%p Autolock:
%a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)",
"ng-class" => "{'info bold' : option.value == selectedAutoLockInterval}"}
{{option.label}}
.panel-section{"ng-if" => "!importData.loading"}

View File

@@ -20,3 +20,14 @@
%p {{displayInfoForAction(action)}}
%th{"ng-repeat" => "credential in availableCredentials"}
%input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"}
.footer
%h2 About Privileges
%p
Privileges represent interface level authentication for accessing certain items and features.
Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state.
Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state.
%p
Privileges sync across your other devices (not including mobile); however, note that if you require
a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode
requirement will be ignored on that device.

View File

@@ -39,6 +39,6 @@
.item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
.label Refresh
.item#lock-item{"ng-if" => "ctrl.hasPasscode()"}
.item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."}
.label
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}

View File

@@ -59,9 +59,11 @@
.faded {{note.savedTagsString || note.tagsString()}}
.name{"ng-if" => "note.title"}
%span.locked.tinted{"ng-if" => "note.locked", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
%i.icon.ion-locked.medium-text
{{note.title}}
.note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview"}
.note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview && !note.locked"}
.html-preview{"ng-if" => "note.content.preview_html", "ng-bind-html" => "note.content.preview_html"}
.plain-preview{"ng-if" => "!note.content.preview_html && note.content.preview_plain"} {{note.content.preview_plain}}
.default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}}