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 = {}; this.panelController = {};
@@ -198,19 +199,31 @@ angular.module('app')
} }
} }
this.selectNote = function(note, viaClick = false) { this.selectNote = async function(note, viaClick = false) {
if(!note) { if(!note) {
this.createNewNote(); this.createNewNote();
return; return;
} }
this.selectedNote = note; let run = () => {
note.conflict_of = null; // clear conflict $timeout(() => {
this.selectionMade()(note); this.selectedNote = note;
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
if(viaClick && this.isFiltering()) { if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text); 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.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus; $scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() { $scope.submitMfaForm = function() {
@@ -375,6 +374,24 @@ class AccountMenu {
Passcode Lock 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() { $scope.hasPasscode = function() {
return passcodeManager.hasPasscode(); return passcodeManager.hasPasscode();
} }

View File

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

View File

@@ -1,112 +1,183 @@
angular.module('app') class PasscodeManager {
.provider('passcodeManager', function () {
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { constructor(authManager, storageManager) {
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, 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._locked = this._hasPasscode;
this.isLocked = function() { const MillisecondsPerSecond = 1000;
return this._locked; PasscodeManager.AutoLockIntervalNone = 0;
} PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
this.hasPasscode = function() { PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
return this._hasPasscode; }
}
this.keys = function() { getAutoLockIntervalOptions() {
return this._keys; return [
} {
value: PasscodeManager.AutoLockIntervalNone,
this.passcodeAuthParams = function() { label: "None"
var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed)); },
if(authParams && !authParams.version) { {
var keys = this.keys(); value: PasscodeManager.AutoLockIntervalOneMinute,
if(keys && keys.ak) { label: "1 Min"
// 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 { value: PasscodeManager.AutoLockIntervalFiveMinutes,
authParams.version = "001"; label: "5 Min"
} },
{
value: PasscodeManager.AutoLockIntervalOneHour,
label: "1 Hr"
} }
return authParams; ]
} }
this.verifyPasscode = async function(passcode) { documentVisibilityChanged(visbility) {
return new Promise(async (resolve, reject) => { let visible = document.visibilityState == "visible";
var params = this.passcodeAuthParams(); if(!visible) {
let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); this.beginAutoLockTimer();
if(keys.pw !== params.hash) { } else {
resolve(false); this.cancelAutoLockTimer();
} 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();
} }
} }
});
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.ActionManageExtensions = "ActionManageExtensions";
PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup";
PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes";
PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
this.availableActions = [ this.availableActions = [
PrivilegesManager.ActionManageExtensions, PrivilegesManager.ActionManageExtensions,
PrivilegesManager.ActionDownloadBackup PrivilegesManager.ActionDownloadBackup,
PrivilegesManager.ActionViewLockedNotes,
PrivilegesManager.ActionManagePrivileges
] ]
this.availableCredentials = [ this.availableCredentials = [
@@ -119,10 +123,19 @@ class PrivilegesManager {
metadata[PrivilegesManager.ActionManageExtensions] = { metadata[PrivilegesManager.ActionManageExtensions] = {
label: "Manage Extensions" label: "Manage Extensions"
} }
metadata[PrivilegesManager.ActionDownloadBackup] = { metadata[PrivilegesManager.ActionDownloadBackup] = {
label: "Download Backups" label: "Download Backups"
}; };
metadata[PrivilegesManager.ActionViewLockedNotes] = {
label: "View Locked Notes"
};
metadata[PrivilegesManager.ActionManagePrivileges] = {
label: "Manage Privileges"
}
return metadata[action]; return metadata[action];
} }

View File

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

View File

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

View File

@@ -130,7 +130,12 @@
.horizontal-group .horizontal-group
%a.info{"ng-click" => "changePasscodePressed()"} Change Passcode %a.info{"ng-click" => "changePasscodePressed()"} Change Passcode
%a.danger{"ng-click" => "removePasscodePressed()"} Remove 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"} .panel-section{"ng-if" => "!importData.loading"}

View File

@@ -20,3 +20,14 @@
%p {{displayInfoForAction(action)}} %p {{displayInfoForAction(action)}}
%th{"ng-repeat" => "credential in availableCredentials"} %th{"ng-repeat" => "credential in availableCredentials"}
%input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} %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()"} .item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
.label Refresh .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 .label
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"} %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()}} .faded {{note.savedTagsString || note.tagsString()}}
.name{"ng-if" => "note.title"} .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.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"} .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}} .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}} .default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}}