Autolock wip
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
|
||||
super($timeout);
|
||||
|
||||
this.setJWTRequestHandler(async () => {
|
||||
return storageManager.getItem("jwt");;
|
||||
return storageManager.getItem("jwt");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#privileges-modal {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
#password-wizard {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()"}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
Reference in New Issue
Block a user