mirror of https://github.com/keeweb/keeweb
parent
4cdc80e530
commit
08d49ddc54
|
@ -312,6 +312,17 @@ const Launcher = {
|
|||
},
|
||||
setGlobalShortcuts(appSettings) {
|
||||
this.remoteApp().setGlobalShortcuts(appSettings);
|
||||
},
|
||||
hasTouchId() {
|
||||
if (this.hasTouchId.value === undefined) {
|
||||
if (RuntimeInfo.devMode) {
|
||||
this.hasTouchId.value = !!process.env.KEEWEB_EMULATE_HARDWARE_ENCRYPTION;
|
||||
} else {
|
||||
const { systemPreferences } = this.electron().remote;
|
||||
this.hasTouchId.value = !!systemPreferences.canPromptTouchID();
|
||||
}
|
||||
}
|
||||
return this.hasTouchId.value;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -139,36 +139,6 @@ if (Launcher) {
|
|||
});
|
||||
},
|
||||
|
||||
makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = Buffer.from(kdbxweb.Random.getBytes(data.length));
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
const result = { data: [...data], random: [...random] };
|
||||
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('startUsbListener');
|
||||
this.usbListenerRunning = true;
|
||||
|
@ -202,16 +172,18 @@ if (Launcher) {
|
|||
|
||||
hardwareEncrypt: async (value) => {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
value = NativeModules.makeXoredValue(value);
|
||||
const encrypted = await ipcRenderer.invoke('hardwareEncrypt', value);
|
||||
return NativeModules.readXoredValue(encrypted);
|
||||
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
hardwareDecrypt: async (value, touchIdPrompt) => {
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
value = NativeModules.makeXoredValue(value);
|
||||
const decrypted = await ipcRenderer.invoke('hardwareDecrypt', value, touchIdPrompt);
|
||||
return NativeModules.readXoredValue(decrypted);
|
||||
const { data, salt } = await ipcRenderer.invoke(
|
||||
'hardwareDecrypt',
|
||||
value.dataAndSalt(),
|
||||
touchIdPrompt
|
||||
);
|
||||
return new kdbxweb.ProtectedValue(data, salt);
|
||||
},
|
||||
|
||||
kbdGetActiveWindow(options) {
|
||||
|
|
|
@ -43,7 +43,7 @@ const DefaultAppSettings = {
|
|||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||
useLegacyAutoType: false, // use legacy auto-type engine (will be removed in future versions)
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'unlock' / 'credentials'
|
||||
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||
|
||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||
|
|
|
@ -460,8 +460,8 @@
|
|||
"setGenUseLegacyAutoType": "Use legacy auto-type (if you have issues)",
|
||||
"setGenTouchId": "Touch ID",
|
||||
"setGenTouchIdDisabled": "Don't use Touch ID",
|
||||
"setGenTouchIdUnlock": "Unlock with Touch ID only after automatic locking",
|
||||
"setGenTouchIdCredentials": "Use Touch ID instead of master password",
|
||||
"setGenTouchIdMemory": "Unlock with Touch ID only while KeeWeb is running",
|
||||
"setGenTouchIdFile": "Always use Touch ID instead of master password",
|
||||
"setGenTouchIdPass": "Require master password after",
|
||||
"setGenAudit": "Audit",
|
||||
"setGenAuditPasswords": "Show warnings about password strength",
|
||||
|
|
|
@ -4,8 +4,8 @@ import { SearchResultCollection } from 'collections/search-result-collection';
|
|||
import { FileCollection } from 'collections/file-collection';
|
||||
import { FileInfoCollection } from 'collections/file-info-collection';
|
||||
import { RuntimeInfo } from 'const/runtime-info';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
|
@ -37,6 +37,7 @@ class AppModel {
|
|||
isBeta = RuntimeInfo.beta;
|
||||
advancedSearch = null;
|
||||
attachedYubiKeysCount = 0;
|
||||
memoryPasswordStorage = {};
|
||||
|
||||
constructor() {
|
||||
Events.on('refresh', this.refresh.bind(this));
|
||||
|
@ -663,9 +664,13 @@ class AppModel {
|
|||
path: params.path,
|
||||
keyFileName: params.keyFileName,
|
||||
keyFilePath: params.keyFilePath,
|
||||
backup: (fileInfo && fileInfo.backup) || null,
|
||||
backup: fileInfo?.backup || null,
|
||||
chalResp: params.chalResp
|
||||
});
|
||||
if (params.encryptedPassword) {
|
||||
file.encryptedPassword = fileInfo.encryptedPassword;
|
||||
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||
}
|
||||
const openComplete = (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -769,6 +774,14 @@ class AppModel {
|
|||
keyFilePath: file.keyFilePath || null
|
||||
});
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth === 'file' && file.encryptedPassword) {
|
||||
const maxDate = new Date(file.encryptedPasswordDate);
|
||||
maxDate.setMinutes(maxDate.getMinutes() + this.settings.deviceOwnerAuthTimeoutMinutes);
|
||||
if (maxDate > new Date()) {
|
||||
fileInfo.encryptedPassword = file.encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = file.encryptedPasswordDate;
|
||||
}
|
||||
}
|
||||
this.fileInfos.remove(file.id);
|
||||
this.fileInfos.unshift(fileInfo);
|
||||
this.fileInfos.save();
|
||||
|
@ -811,6 +824,9 @@ class AppModel {
|
|||
this.tryOpenOtpDeviceInBackground();
|
||||
}
|
||||
}
|
||||
if (this.settings.deviceOwnerAuth) {
|
||||
this.saveEncryptedPassword(file, params);
|
||||
}
|
||||
}
|
||||
|
||||
fileClosed(file) {
|
||||
|
@ -1269,6 +1285,97 @@ class AppModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveEncryptedPassword(file, params) {
|
||||
if (!this.settings.deviceOwnerAuth || params.encryptedPassword) {
|
||||
return;
|
||||
}
|
||||
NativeModules.hardwareEncrypt(params.password)
|
||||
.then((encryptedPassword) => {
|
||||
encryptedPassword = encryptedPassword.toBase64();
|
||||
const fileInfo = this.fileInfos.get(file.id);
|
||||
const encryptedPasswordDate = new Date();
|
||||
file.encryptedPassword = encryptedPassword;
|
||||
file.encryptedPasswordDate = encryptedPasswordDate;
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
fileInfo.encryptedPassword = encryptedPassword;
|
||||
fileInfo.encryptedPasswordDate = encryptedPasswordDate;
|
||||
this.fileInfos.save();
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
this.memoryPasswordStorage[file.id] = {
|
||||
value: encryptedPassword,
|
||||
date: encryptedPasswordDate
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
delete this.memoryPasswordStorage[file.id];
|
||||
this.appLogger.error('Error encrypting password', e);
|
||||
});
|
||||
}
|
||||
|
||||
getMemoryPassword(fileId) {
|
||||
return this.memoryPasswordStorage[fileId];
|
||||
}
|
||||
|
||||
checkEncryptedPasswordsStorage() {
|
||||
if (this.settings.deviceOwnerAuth === 'file') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (this.memoryPasswordStorage[fileInfo.id]) {
|
||||
fileInfo.encryptedPassword = this.memoryPasswordStorage[fileInfo.id].value;
|
||||
fileInfo.encryptedPasswordDate = this.memoryPasswordStorage[fileInfo.id].date;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (this.memoryPasswordStorage[file.id]) {
|
||||
file.encryptedPassword = this.memoryPasswordStorage[file.id].value;
|
||||
file.encryptedPasswordDate = this.memoryPasswordStorage[file.id].date;
|
||||
}
|
||||
}
|
||||
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
this.memoryPasswordStorage[fileInfo.id] = {
|
||||
value: fileInfo.encryptedPassword,
|
||||
date: fileInfo.encryptedPasswordDate
|
||||
};
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
} else {
|
||||
let changed = false;
|
||||
for (const fileInfo of this.fileInfos) {
|
||||
if (fileInfo.encryptedPassword) {
|
||||
fileInfo.encryptedPassword = null;
|
||||
fileInfo.encryptedPasswordDate = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
this.fileInfos.save();
|
||||
}
|
||||
for (const file of this.files) {
|
||||
if (file.encryptedPassword) {
|
||||
file.encryptedPassword = null;
|
||||
file.encryptedPasswordDate = null;
|
||||
}
|
||||
}
|
||||
this.memoryPasswordStorage = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { AppModel };
|
||||
|
|
|
@ -17,7 +17,9 @@ const DefaultProperties = {
|
|||
opts: null,
|
||||
backup: null,
|
||||
fingerprint: null, // obsolete
|
||||
chalResp: null
|
||||
chalResp: null,
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
};
|
||||
|
||||
class FileInfoModel extends Model {
|
||||
|
|
|
@ -487,6 +487,13 @@ class FileModel extends Model {
|
|||
syncError: error
|
||||
});
|
||||
|
||||
if (!error && this.passwordChanged && this.encryptedPassword) {
|
||||
this.set({
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
@ -757,7 +764,9 @@ FileModel.defineModelProperties({
|
|||
fingerprint: null, // obsolete
|
||||
oldPasswordHash: null,
|
||||
oldKeyFileHash: null,
|
||||
oldKeyChangeDate: null
|
||||
oldKeyChangeDate: null,
|
||||
encryptedPassword: null,
|
||||
encryptedPasswordDate: null
|
||||
});
|
||||
|
||||
export { FileModel };
|
||||
|
|
|
@ -34,8 +34,8 @@ const KdbxwebInit = {
|
|||
hash(args) {
|
||||
const ts = logger.ts();
|
||||
|
||||
const password = NativeModules.makeXoredValue(args.password);
|
||||
const salt = NativeModules.makeXoredValue(args.salt);
|
||||
const password = kdbxweb.ProtectedValue.fromBinary(args.password).dataAndSalt();
|
||||
const salt = kdbxweb.ProtectedValue.fromBinary(args.salt).dataAndSalt();
|
||||
|
||||
return NativeModules.argon2(password, salt, {
|
||||
type: args.type,
|
||||
|
@ -52,7 +52,8 @@ const KdbxwebInit = {
|
|||
|
||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||
|
||||
return NativeModules.readXoredValue(res);
|
||||
res = new kdbxweb.ProtectedValue(res.data, res.salt);
|
||||
return res.getBinary();
|
||||
})
|
||||
.catch((err) => {
|
||||
password.data.fill(0);
|
||||
|
|
|
@ -190,3 +190,22 @@ kdbxweb.ProtectedValue.prototype.saltedValue = function () {
|
|||
}
|
||||
return salted;
|
||||
};
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.dataAndSalt = function () {
|
||||
return {
|
||||
data: [...this._value],
|
||||
salt: [...this._salt]
|
||||
};
|
||||
};
|
||||
|
||||
kdbxweb.ProtectedValue.prototype.toBase64 = function () {
|
||||
const binary = this.getBinary();
|
||||
const base64 = kdbxweb.ByteUtils.bytesToBase64(binary);
|
||||
kdbxweb.ByteUtils.zeroBuffer(binary);
|
||||
return base64;
|
||||
};
|
||||
|
||||
kdbxweb.ProtectedValue.fromBase64 = function (base64) {
|
||||
const bytes = kdbxweb.ByteUtils.base64ToBytes(base64);
|
||||
return kdbxweb.ProtectedValue.fromBinary(bytes);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import { StorageFileListView } from 'views/storage-file-list-view';
|
|||
import { OpenChalRespView } from 'views/open-chal-resp-view';
|
||||
import { omit } from 'util/fn';
|
||||
import { GeneratorView } from 'views/generator-view';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import template from 'templates/open.hbs';
|
||||
|
||||
const logger = new Logger('open-view');
|
||||
|
@ -57,12 +58,10 @@ class OpenView extends View {
|
|||
};
|
||||
|
||||
params = null;
|
||||
|
||||
passwordInput = null;
|
||||
|
||||
busy = false;
|
||||
|
||||
currentSelectedIndex = -1;
|
||||
encryptedPassword = null;
|
||||
|
||||
constructor(model) {
|
||||
super(model);
|
||||
|
@ -152,6 +151,7 @@ class OpenView extends View {
|
|||
|
||||
windowFocused() {
|
||||
this.inputEl.focus();
|
||||
this.checkIfEncryptedPasswordDateIsValid();
|
||||
}
|
||||
|
||||
focusInput(focusOnMobile) {
|
||||
|
@ -237,8 +237,10 @@ class OpenView extends View {
|
|||
if (!this.params.keyFileData) {
|
||||
this.params.keyFileName = null;
|
||||
}
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenKeyFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
success = true;
|
||||
break;
|
||||
case 'xml':
|
||||
|
@ -248,7 +250,9 @@ class OpenView extends View {
|
|||
this.params.path = null;
|
||||
this.params.storage = null;
|
||||
this.params.rev = null;
|
||||
this.encryptedPassword = null;
|
||||
this.importDbWithXml();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
success = true;
|
||||
break;
|
||||
case 'kdb':
|
||||
|
@ -341,6 +345,15 @@ class OpenView extends View {
|
|||
.toggleClass('open__settings-yubikey--active', !!this.params.chalResp);
|
||||
}
|
||||
|
||||
displayOpenDeviceOwnerAuth() {
|
||||
const available = !!this.encryptedPassword;
|
||||
const passEmpty = !this.passwordInput.length;
|
||||
const canUseEncryptedPassword = available && passEmpty;
|
||||
this.el
|
||||
.querySelector('.open__pass-enter-btn')
|
||||
.classList.toggle('open__pass-enter-btn--touch-id', canUseEncryptedPassword);
|
||||
}
|
||||
|
||||
setFile(file, keyFile, fileReadyCallback) {
|
||||
this.reading = 'fileData';
|
||||
this.processFile(file, (success) => {
|
||||
|
@ -479,6 +492,10 @@ class OpenView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
inputInput() {
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
}
|
||||
|
||||
toggleCapsLockWarning(on) {
|
||||
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
||||
}
|
||||
|
@ -587,9 +604,12 @@ class OpenView extends View {
|
|||
this.params.keyFileData = null;
|
||||
this.params.opts = fileInfo.opts;
|
||||
this.params.chalResp = fileInfo.chalResp;
|
||||
this.setEncryptedPassword(fileInfo);
|
||||
|
||||
this.displayOpenFile();
|
||||
this.displayOpenKeyFile();
|
||||
this.displayOpenChalResp();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
|
||||
if (fileWasClicked) {
|
||||
this.focusInput(true);
|
||||
|
@ -606,7 +626,9 @@ class OpenView extends View {
|
|||
this.params.name = path.match(/[^/\\]*$/)[0];
|
||||
this.params.rev = null;
|
||||
this.params.fileData = null;
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
if (keyFilePath) {
|
||||
const parsed = Launcher.parsePath(keyFilePath);
|
||||
this.params.keyFileName = parsed.file;
|
||||
|
@ -646,15 +668,37 @@ class OpenView extends View {
|
|||
this.inputEl.attr('disabled', 'disabled');
|
||||
this.busy = true;
|
||||
this.params.password = this.passwordInput.value;
|
||||
this.afterPaint(() => {
|
||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||
});
|
||||
if (this.encryptedPassword && !this.params.password.length) {
|
||||
logger.debug('Encrypting password using hardware decryption');
|
||||
const touchIdPrompt = Locale.bioOpenAuthPrompt.replace('{}', this.params.name);
|
||||
const encryptedPassword = kdbxweb.ProtectedValue.fromBase64(
|
||||
this.encryptedPassword.value
|
||||
);
|
||||
NativeModules.hardwareDecrypt(encryptedPassword, touchIdPrompt)
|
||||
.then((password) => {
|
||||
this.params.password = password;
|
||||
this.params.encryptedPassword = this.encryptedPassword;
|
||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.message.includes('User refused')) {
|
||||
err.userCanceled = true;
|
||||
}
|
||||
this.openDbComplete(err);
|
||||
});
|
||||
} else {
|
||||
this.params.encryptedPassword = null;
|
||||
this.afterPaint(() => {
|
||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openDbComplete(err) {
|
||||
this.busy = false;
|
||||
this.$el.toggleClass('open--opening', false);
|
||||
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err);
|
||||
const showInputError = err && !err.userCanceled;
|
||||
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!showInputError);
|
||||
if (err) {
|
||||
logger.error('Error opening file', err);
|
||||
this.focusInput(true);
|
||||
|
@ -806,7 +850,9 @@ class OpenView extends View {
|
|||
this.params.name = UrlFormat.getDataFileName(file.name);
|
||||
this.params.rev = file.rev;
|
||||
this.params.fileData = null;
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
}
|
||||
|
||||
showConfig(storage) {
|
||||
|
@ -902,7 +948,9 @@ class OpenView extends View {
|
|||
this.params.name = UrlFormat.getDataFileName(req.path);
|
||||
this.params.rev = stat.rev;
|
||||
this.params.fileData = null;
|
||||
this.encryptedPassword = null;
|
||||
this.displayOpenFile();
|
||||
this.displayOpenDeviceOwnerAuth();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1062,6 +1110,37 @@ class OpenView extends View {
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setEncryptedPassword(fileInfo) {
|
||||
this.encryptedPassword = null;
|
||||
if (!fileInfo.id) {
|
||||
return;
|
||||
}
|
||||
switch (this.model.settings.deviceOwnerAuth) {
|
||||
case 'memory':
|
||||
this.encryptedPassword = this.model.getMemoryPassword(fileInfo.id);
|
||||
break;
|
||||
case 'file':
|
||||
this.encryptedPassword = {
|
||||
value: fileInfo.encryptedPassword,
|
||||
date: fileInfo.encryptedPasswordDate
|
||||
};
|
||||
break;
|
||||
}
|
||||
this.checkIfEncryptedPasswordDateIsValid();
|
||||
}
|
||||
|
||||
checkIfEncryptedPasswordDateIsValid() {
|
||||
if (this.encryptedPassword) {
|
||||
const maxDate = new Date(this.encryptedPassword.date);
|
||||
maxDate.setMinutes(
|
||||
maxDate.getMinutes() + this.model.settings.deviceOwnerAuthTimeoutMinutes
|
||||
);
|
||||
if (maxDate < new Date()) {
|
||||
this.encryptedPassword = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { OpenView };
|
||||
|
|
|
@ -137,7 +137,7 @@ class SettingsGeneralView extends View {
|
|||
titlebarStyle: AppSettingsModel.titlebarStyle,
|
||||
storageProviders,
|
||||
showReloadApp: Features.isStandalone,
|
||||
hasDeviceOwnerAuth: Launcher && Features.isMac,
|
||||
hasDeviceOwnerAuth: Launcher && Launcher.hasTouchId(),
|
||||
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
|
||||
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes
|
||||
});
|
||||
|
@ -436,13 +436,15 @@ class SettingsGeneralView extends View {
|
|||
|
||||
let deviceOwnerAuthTimeoutMinutes = AppSettingsModel.deviceOwnerAuthTimeoutMinutes | 0;
|
||||
if (deviceOwnerAuth) {
|
||||
const timeouts = { unlock: [30, 10080], credentials: [30, 525600] };
|
||||
const timeouts = { memory: [30, 10080], file: [30, 525600] };
|
||||
const [tMin, tMax] = timeouts[deviceOwnerAuth] || [0, 0];
|
||||
deviceOwnerAuthTimeoutMinutes = minmax(deviceOwnerAuthTimeoutMinutes, tMin, tMax);
|
||||
}
|
||||
|
||||
AppSettingsModel.set({ deviceOwnerAuth, deviceOwnerAuthTimeoutMinutes });
|
||||
this.render();
|
||||
|
||||
this.appModel.checkEncryptedPasswordsStorage();
|
||||
}
|
||||
|
||||
changeDeviceOwnerAuthTimeout(e) {
|
||||
|
|
|
@ -108,6 +108,18 @@
|
|||
.open--opening & {
|
||||
display: none;
|
||||
}
|
||||
&-icon-enter {
|
||||
display: block;
|
||||
.open__pass-enter-btn--touch-id & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&-icon-touch-id {
|
||||
display: none;
|
||||
.open__pass-enter-btn--touch-id & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-opening-icon {
|
||||
display: none;
|
||||
|
|
|
@ -195,3 +195,4 @@ $fa-var-paint-brush: next-fa-glyph();
|
|||
$fa-var-at: next-fa-glyph();
|
||||
$fa-var-usb-token: next-fa-glyph();
|
||||
$fa-var-bell: next-fa-glyph();
|
||||
$fa-var-fingerprint: next-fa-glyph();
|
||||
|
|
|
@ -80,7 +80,10 @@
|
|||
<div class="open__pass-field-wrap">
|
||||
<input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024"
|
||||
placeholder="{{#if canOpen}}{{res 'openClickToOpen'}}{{/if}}" readonly tabindex="23" />
|
||||
<div class="open__pass-enter-btn" tabindex="24"><i class="fa fa-level-down-alt rotate-90"></i></div>
|
||||
<div class="open__pass-enter-btn" tabindex="24">
|
||||
<i class="fa fa-level-down-alt rotate-90 open__pass-enter-btn-icon-enter"></i>
|
||||
<i class="fa fa-fingerprint open__pass-enter-btn-icon-touch-id"></i>
|
||||
</div>
|
||||
<div class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div>
|
||||
</div>
|
||||
<div class="open__settings">
|
||||
|
|
|
@ -181,8 +181,8 @@
|
|||
<label for="settings__general-device-owner-auth">{{res 'setGenTouchId'}}:</label>
|
||||
<select class="settings__general-device-owner-auth settings__select input-base" id="settings__general-device-owner-auth">
|
||||
<option value="" {{#unless deviceOwnerAuth}}selected{{/unless}}>{{res 'setGenTouchIdDisabled'}}</option>
|
||||
<option value="unlock" {{#ifeq deviceOwnerAuth 'unlock'}}selected{{/ifeq}}>{{res 'setGenTouchIdUnlock'}}</option>
|
||||
<option value="credentials" {{#ifeq deviceOwnerAuth 'credentials'}}selected{{/ifeq}}>{{res 'setGenTouchIdCredentials'}}</option>
|
||||
<option value="memory" {{#ifeq deviceOwnerAuth 'memory'}}selected{{/ifeq}}>{{res 'setGenTouchIdMemory'}}</option>
|
||||
<option value="file" {{#ifeq deviceOwnerAuth 'file'}}selected{{/ifeq}}>{{res 'setGenTouchIdFile'}}</option>
|
||||
</select>
|
||||
</div>
|
||||
{{#if deviceOwnerAuth}}
|
||||
|
@ -196,7 +196,7 @@
|
|||
<option value="480" {{#ifeq deviceOwnerAuthTimeout 480}}selected{{/ifeq}}>{{#Res 'hours'}}8{{/Res}}</option>
|
||||
<option value="1440" {{#ifeq deviceOwnerAuthTimeout 1440}}selected{{/ifeq}}>{{Res 'oneDay'}}</option>
|
||||
<option value="10080" {{#ifeq deviceOwnerAuthTimeout 10080}}selected{{/ifeq}}>{{Res 'oneWeek'}}</option>
|
||||
{{#ifeq deviceOwnerAuth 'credentials'}}
|
||||
{{#ifeq deviceOwnerAuth 'file'}}
|
||||
<option value="43200" {{#ifeq deviceOwnerAuthTimeout 43200}}selected{{/ifeq}}>{{Res 'oneMonth'}}</option>
|
||||
<option value="525600" {{#ifeq deviceOwnerAuthTimeout 525600}}selected{{/ifeq}}>{{Res 'oneYear'}}</option>
|
||||
{{/ifeq}}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const { readXoredValue, makeXoredValue } = require('../util/byte-utils');
|
||||
const { reqNative } = require('../util/req-native');
|
||||
|
||||
let testCipherParams;
|
||||
|
||||
module.exports = {
|
||||
hardwareEncrypt,
|
||||
hardwareDecrypt
|
||||
|
@ -30,11 +32,36 @@ async function hardwareCrypto(value, encrypt, touchIdPrompt) {
|
|||
const data = readXoredValue(value);
|
||||
|
||||
let res;
|
||||
if (encrypt) {
|
||||
await checkKey();
|
||||
res = await secureEnclave.encrypt({ keyTag, data });
|
||||
const isDev = !__dirname.includes('.asar');
|
||||
if (isDev && process.env.KEEWEB_EMULATE_HARDWARE_ENCRYPTION) {
|
||||
const crypto = require('crypto');
|
||||
if (!testCipherParams) {
|
||||
let key, iv;
|
||||
if (process.env.KEEWEB_EMULATE_HARDWARE_ENCRYPTION === 'persistent') {
|
||||
key = Buffer.alloc(32, 0);
|
||||
iv = Buffer.alloc(16, 0);
|
||||
} else {
|
||||
key = crypto.randomBytes(32);
|
||||
iv = crypto.randomBytes(16);
|
||||
}
|
||||
testCipherParams = { key, iv };
|
||||
}
|
||||
const { key, iv } = testCipherParams;
|
||||
const algo = 'aes-256-cbc';
|
||||
let cipher;
|
||||
if (encrypt) {
|
||||
cipher = crypto.createCipheriv(algo, key, iv);
|
||||
} else {
|
||||
cipher = crypto.createDecipheriv(algo, key, iv);
|
||||
}
|
||||
res = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
} else {
|
||||
res = await secureEnclave.decrypt({ keyTag, data, touchIdPrompt });
|
||||
if (encrypt) {
|
||||
await checkKey();
|
||||
res = await secureEnclave.encrypt({ keyTag, data });
|
||||
} else {
|
||||
res = await secureEnclave.decrypt({ keyTag, data, touchIdPrompt });
|
||||
}
|
||||
}
|
||||
|
||||
data.fill(0);
|
||||
|
|
|
@ -3,35 +3,35 @@ const crypto = require('crypto');
|
|||
module.exports = {
|
||||
readXoredValue: function readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
const salt = Buffer.from(val.salt);
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
val.salt.fill(0);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
data[i] ^= salt[i];
|
||||
}
|
||||
|
||||
random.fill(0);
|
||||
salt.fill(0);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
makeXoredValue: function makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = crypto.randomBytes(data.length);
|
||||
const salt = crypto.randomBytes(data.length);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
data[i] ^= salt[i];
|
||||
}
|
||||
const result = { data: [...data], random: [...random] };
|
||||
const result = { data: [...data], salt: [...salt] };
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
salt.fill(0);
|
||||
|
||||
val.fill(0);
|
||||
|
||||
setTimeout(() => {
|
||||
result.data.fill(0);
|
||||
result.random.fill(0);
|
||||
result.salt.fill(0);
|
||||
}, 0);
|
||||
|
||||
return result;
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
"start": "grunt",
|
||||
"test": "grunt test",
|
||||
"build-beta": "grunt --beta && cp dist/index.html ../keeweb-beta/index.html && cd ../keeweb-beta && git add index.html && git commit -a -m 'beta' && git push origin master",
|
||||
"electron": "cross-env KEEWEB_IS_PORTABLE=0 ELECTRON_DISABLE_SECURITY_WARNINGS=1 KEEWEB_HTML_PATH=http://localhost:8085 electron desktop --no-sandbox",
|
||||
"electron": "cross-env KEEWEB_IS_PORTABLE=0 ELECTRON_DISABLE_SECURITY_WARNINGS=1 KEEWEB_EMULATE_HARDWARE_ENCRYPTION=persistent KEEWEB_HTML_PATH=http://localhost:8085 electron desktop --no-sandbox",
|
||||
"dev": "grunt dev",
|
||||
"dev-desktop-macos": "grunt dev-desktop-darwin --skip-sign",
|
||||
"dev-desktop-macos-signed": "grunt dev-desktop-darwin-signed",
|
||||
|
|
Loading…
Reference in New Issue