mirror of
https://github.com/keeweb/keeweb.git
synced 2024-06-25 07:37:46 +02:00
parent
4cdc80e530
commit
08d49ddc54
|
@ -312,6 +312,17 @@ const Launcher = {
|
||||||
},
|
},
|
||||||
setGlobalShortcuts(appSettings) {
|
setGlobalShortcuts(appSettings) {
|
||||||
this.remoteApp().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() {
|
startUsbListener() {
|
||||||
this.call('startUsbListener');
|
this.call('startUsbListener');
|
||||||
this.usbListenerRunning = true;
|
this.usbListenerRunning = true;
|
||||||
|
@ -202,16 +172,18 @@ if (Launcher) {
|
||||||
|
|
||||||
hardwareEncrypt: async (value) => {
|
hardwareEncrypt: async (value) => {
|
||||||
const { ipcRenderer } = Launcher.electron();
|
const { ipcRenderer } = Launcher.electron();
|
||||||
value = NativeModules.makeXoredValue(value);
|
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||||
const encrypted = await ipcRenderer.invoke('hardwareEncrypt', value);
|
return new kdbxweb.ProtectedValue(data, salt);
|
||||||
return NativeModules.readXoredValue(encrypted);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hardwareDecrypt: async (value, touchIdPrompt) => {
|
hardwareDecrypt: async (value, touchIdPrompt) => {
|
||||||
const { ipcRenderer } = Launcher.electron();
|
const { ipcRenderer } = Launcher.electron();
|
||||||
value = NativeModules.makeXoredValue(value);
|
const { data, salt } = await ipcRenderer.invoke(
|
||||||
const decrypted = await ipcRenderer.invoke('hardwareDecrypt', value, touchIdPrompt);
|
'hardwareDecrypt',
|
||||||
return NativeModules.readXoredValue(decrypted);
|
value.dataAndSalt(),
|
||||||
|
touchIdPrompt
|
||||||
|
);
|
||||||
|
return new kdbxweb.ProtectedValue(data, salt);
|
||||||
},
|
},
|
||||||
|
|
||||||
kbdGetActiveWindow(options) {
|
kbdGetActiveWindow(options) {
|
||||||
|
|
|
@ -43,7 +43,7 @@ const DefaultAppSettings = {
|
||||||
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||||
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
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)
|
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
|
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||||
|
|
||||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||||
|
|
|
@ -460,8 +460,8 @@
|
||||||
"setGenUseLegacyAutoType": "Use legacy auto-type (if you have issues)",
|
"setGenUseLegacyAutoType": "Use legacy auto-type (if you have issues)",
|
||||||
"setGenTouchId": "Touch ID",
|
"setGenTouchId": "Touch ID",
|
||||||
"setGenTouchIdDisabled": "Don't use Touch ID",
|
"setGenTouchIdDisabled": "Don't use Touch ID",
|
||||||
"setGenTouchIdUnlock": "Unlock with Touch ID only after automatic locking",
|
"setGenTouchIdMemory": "Unlock with Touch ID only while KeeWeb is running",
|
||||||
"setGenTouchIdCredentials": "Use Touch ID instead of master password",
|
"setGenTouchIdFile": "Always use Touch ID instead of master password",
|
||||||
"setGenTouchIdPass": "Require master password after",
|
"setGenTouchIdPass": "Require master password after",
|
||||||
"setGenAudit": "Audit",
|
"setGenAudit": "Audit",
|
||||||
"setGenAuditPasswords": "Show warnings about password strength",
|
"setGenAuditPasswords": "Show warnings about password strength",
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { SearchResultCollection } from 'collections/search-result-collection';
|
||||||
import { FileCollection } from 'collections/file-collection';
|
import { FileCollection } from 'collections/file-collection';
|
||||||
import { FileInfoCollection } from 'collections/file-info-collection';
|
import { FileInfoCollection } from 'collections/file-info-collection';
|
||||||
import { RuntimeInfo } from 'const/runtime-info';
|
import { RuntimeInfo } from 'const/runtime-info';
|
||||||
import { Launcher } from 'comp/launcher';
|
|
||||||
import { UsbListener } from 'comp/app/usb-listener';
|
import { UsbListener } from 'comp/app/usb-listener';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import { Timeouts } from 'const/timeouts';
|
import { Timeouts } from 'const/timeouts';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
import { EntryModel } from 'models/entry-model';
|
import { EntryModel } from 'models/entry-model';
|
||||||
|
@ -37,6 +37,7 @@ class AppModel {
|
||||||
isBeta = RuntimeInfo.beta;
|
isBeta = RuntimeInfo.beta;
|
||||||
advancedSearch = null;
|
advancedSearch = null;
|
||||||
attachedYubiKeysCount = 0;
|
attachedYubiKeysCount = 0;
|
||||||
|
memoryPasswordStorage = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
Events.on('refresh', this.refresh.bind(this));
|
Events.on('refresh', this.refresh.bind(this));
|
||||||
|
@ -663,9 +664,13 @@ class AppModel {
|
||||||
path: params.path,
|
path: params.path,
|
||||||
keyFileName: params.keyFileName,
|
keyFileName: params.keyFileName,
|
||||||
keyFilePath: params.keyFilePath,
|
keyFilePath: params.keyFilePath,
|
||||||
backup: (fileInfo && fileInfo.backup) || null,
|
backup: fileInfo?.backup || null,
|
||||||
chalResp: params.chalResp
|
chalResp: params.chalResp
|
||||||
});
|
});
|
||||||
|
if (params.encryptedPassword) {
|
||||||
|
file.encryptedPassword = fileInfo.encryptedPassword;
|
||||||
|
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||||
|
}
|
||||||
const openComplete = (err) => {
|
const openComplete = (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
@ -769,6 +774,14 @@ class AppModel {
|
||||||
keyFilePath: file.keyFilePath || null
|
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.remove(file.id);
|
||||||
this.fileInfos.unshift(fileInfo);
|
this.fileInfos.unshift(fileInfo);
|
||||||
this.fileInfos.save();
|
this.fileInfos.save();
|
||||||
|
@ -811,6 +824,9 @@ class AppModel {
|
||||||
this.tryOpenOtpDeviceInBackground();
|
this.tryOpenOtpDeviceInBackground();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.settings.deviceOwnerAuth) {
|
||||||
|
this.saveEncryptedPassword(file, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileClosed(file) {
|
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 };
|
export { AppModel };
|
||||||
|
|
|
@ -17,7 +17,9 @@ const DefaultProperties = {
|
||||||
opts: null,
|
opts: null,
|
||||||
backup: null,
|
backup: null,
|
||||||
fingerprint: null, // obsolete
|
fingerprint: null, // obsolete
|
||||||
chalResp: null
|
chalResp: null,
|
||||||
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileInfoModel extends Model {
|
class FileInfoModel extends Model {
|
||||||
|
|
|
@ -487,6 +487,13 @@ class FileModel extends Model {
|
||||||
syncError: error
|
syncError: error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!error && this.passwordChanged && this.encryptedPassword) {
|
||||||
|
this.set({
|
||||||
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -757,7 +764,9 @@ FileModel.defineModelProperties({
|
||||||
fingerprint: null, // obsolete
|
fingerprint: null, // obsolete
|
||||||
oldPasswordHash: null,
|
oldPasswordHash: null,
|
||||||
oldKeyFileHash: null,
|
oldKeyFileHash: null,
|
||||||
oldKeyChangeDate: null
|
oldKeyChangeDate: null,
|
||||||
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export { FileModel };
|
export { FileModel };
|
||||||
|
|
|
@ -34,8 +34,8 @@ const KdbxwebInit = {
|
||||||
hash(args) {
|
hash(args) {
|
||||||
const ts = logger.ts();
|
const ts = logger.ts();
|
||||||
|
|
||||||
const password = NativeModules.makeXoredValue(args.password);
|
const password = kdbxweb.ProtectedValue.fromBinary(args.password).dataAndSalt();
|
||||||
const salt = NativeModules.makeXoredValue(args.salt);
|
const salt = kdbxweb.ProtectedValue.fromBinary(args.salt).dataAndSalt();
|
||||||
|
|
||||||
return NativeModules.argon2(password, salt, {
|
return NativeModules.argon2(password, salt, {
|
||||||
type: args.type,
|
type: args.type,
|
||||||
|
@ -52,7 +52,8 @@ const KdbxwebInit = {
|
||||||
|
|
||||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
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) => {
|
.catch((err) => {
|
||||||
password.data.fill(0);
|
password.data.fill(0);
|
||||||
|
|
|
@ -190,3 +190,22 @@ kdbxweb.ProtectedValue.prototype.saltedValue = function () {
|
||||||
}
|
}
|
||||||
return salted;
|
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 { OpenChalRespView } from 'views/open-chal-resp-view';
|
||||||
import { omit } from 'util/fn';
|
import { omit } from 'util/fn';
|
||||||
import { GeneratorView } from 'views/generator-view';
|
import { GeneratorView } from 'views/generator-view';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import template from 'templates/open.hbs';
|
import template from 'templates/open.hbs';
|
||||||
|
|
||||||
const logger = new Logger('open-view');
|
const logger = new Logger('open-view');
|
||||||
|
@ -57,12 +58,10 @@ class OpenView extends View {
|
||||||
};
|
};
|
||||||
|
|
||||||
params = null;
|
params = null;
|
||||||
|
|
||||||
passwordInput = null;
|
passwordInput = null;
|
||||||
|
|
||||||
busy = false;
|
busy = false;
|
||||||
|
|
||||||
currentSelectedIndex = -1;
|
currentSelectedIndex = -1;
|
||||||
|
encryptedPassword = null;
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
super(model);
|
super(model);
|
||||||
|
@ -152,6 +151,7 @@ class OpenView extends View {
|
||||||
|
|
||||||
windowFocused() {
|
windowFocused() {
|
||||||
this.inputEl.focus();
|
this.inputEl.focus();
|
||||||
|
this.checkIfEncryptedPasswordDateIsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput(focusOnMobile) {
|
focusInput(focusOnMobile) {
|
||||||
|
@ -237,8 +237,10 @@ class OpenView extends View {
|
||||||
if (!this.params.keyFileData) {
|
if (!this.params.keyFileData) {
|
||||||
this.params.keyFileName = null;
|
this.params.keyFileName = null;
|
||||||
}
|
}
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
this.displayOpenKeyFile();
|
this.displayOpenKeyFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
case 'xml':
|
case 'xml':
|
||||||
|
@ -248,7 +250,9 @@ class OpenView extends View {
|
||||||
this.params.path = null;
|
this.params.path = null;
|
||||||
this.params.storage = null;
|
this.params.storage = null;
|
||||||
this.params.rev = null;
|
this.params.rev = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.importDbWithXml();
|
this.importDbWithXml();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
case 'kdb':
|
case 'kdb':
|
||||||
|
@ -341,6 +345,15 @@ class OpenView extends View {
|
||||||
.toggleClass('open__settings-yubikey--active', !!this.params.chalResp);
|
.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) {
|
setFile(file, keyFile, fileReadyCallback) {
|
||||||
this.reading = 'fileData';
|
this.reading = 'fileData';
|
||||||
this.processFile(file, (success) => {
|
this.processFile(file, (success) => {
|
||||||
|
@ -479,6 +492,10 @@ class OpenView extends View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputInput() {
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
|
}
|
||||||
|
|
||||||
toggleCapsLockWarning(on) {
|
toggleCapsLockWarning(on) {
|
||||||
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
||||||
}
|
}
|
||||||
|
@ -587,9 +604,12 @@ class OpenView extends View {
|
||||||
this.params.keyFileData = null;
|
this.params.keyFileData = null;
|
||||||
this.params.opts = fileInfo.opts;
|
this.params.opts = fileInfo.opts;
|
||||||
this.params.chalResp = fileInfo.chalResp;
|
this.params.chalResp = fileInfo.chalResp;
|
||||||
|
this.setEncryptedPassword(fileInfo);
|
||||||
|
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
this.displayOpenKeyFile();
|
this.displayOpenKeyFile();
|
||||||
this.displayOpenChalResp();
|
this.displayOpenChalResp();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
|
|
||||||
if (fileWasClicked) {
|
if (fileWasClicked) {
|
||||||
this.focusInput(true);
|
this.focusInput(true);
|
||||||
|
@ -606,7 +626,9 @@ class OpenView extends View {
|
||||||
this.params.name = path.match(/[^/\\]*$/)[0];
|
this.params.name = path.match(/[^/\\]*$/)[0];
|
||||||
this.params.rev = null;
|
this.params.rev = null;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
if (keyFilePath) {
|
if (keyFilePath) {
|
||||||
const parsed = Launcher.parsePath(keyFilePath);
|
const parsed = Launcher.parsePath(keyFilePath);
|
||||||
this.params.keyFileName = parsed.file;
|
this.params.keyFileName = parsed.file;
|
||||||
|
@ -646,15 +668,37 @@ class OpenView extends View {
|
||||||
this.inputEl.attr('disabled', 'disabled');
|
this.inputEl.attr('disabled', 'disabled');
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.params.password = this.passwordInput.value;
|
this.params.password = this.passwordInput.value;
|
||||||
this.afterPaint(() => {
|
if (this.encryptedPassword && !this.params.password.length) {
|
||||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
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) {
|
openDbComplete(err) {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.$el.toggleClass('open--opening', 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) {
|
if (err) {
|
||||||
logger.error('Error opening file', err);
|
logger.error('Error opening file', err);
|
||||||
this.focusInput(true);
|
this.focusInput(true);
|
||||||
|
@ -806,7 +850,9 @@ class OpenView extends View {
|
||||||
this.params.name = UrlFormat.getDataFileName(file.name);
|
this.params.name = UrlFormat.getDataFileName(file.name);
|
||||||
this.params.rev = file.rev;
|
this.params.rev = file.rev;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
showConfig(storage) {
|
showConfig(storage) {
|
||||||
|
@ -902,7 +948,9 @@ class OpenView extends View {
|
||||||
this.params.name = UrlFormat.getDataFileName(req.path);
|
this.params.name = UrlFormat.getDataFileName(req.path);
|
||||||
this.params.rev = stat.rev;
|
this.params.rev = stat.rev;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1062,6 +1110,37 @@ class OpenView extends View {
|
||||||
}
|
}
|
||||||
return undefined;
|
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 };
|
export { OpenView };
|
||||||
|
|
|
@ -137,7 +137,7 @@ class SettingsGeneralView extends View {
|
||||||
titlebarStyle: AppSettingsModel.titlebarStyle,
|
titlebarStyle: AppSettingsModel.titlebarStyle,
|
||||||
storageProviders,
|
storageProviders,
|
||||||
showReloadApp: Features.isStandalone,
|
showReloadApp: Features.isStandalone,
|
||||||
hasDeviceOwnerAuth: Launcher && Features.isMac,
|
hasDeviceOwnerAuth: Launcher && Launcher.hasTouchId(),
|
||||||
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
|
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
|
||||||
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes
|
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes
|
||||||
});
|
});
|
||||||
|
@ -436,13 +436,15 @@ class SettingsGeneralView extends View {
|
||||||
|
|
||||||
let deviceOwnerAuthTimeoutMinutes = AppSettingsModel.deviceOwnerAuthTimeoutMinutes | 0;
|
let deviceOwnerAuthTimeoutMinutes = AppSettingsModel.deviceOwnerAuthTimeoutMinutes | 0;
|
||||||
if (deviceOwnerAuth) {
|
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];
|
const [tMin, tMax] = timeouts[deviceOwnerAuth] || [0, 0];
|
||||||
deviceOwnerAuthTimeoutMinutes = minmax(deviceOwnerAuthTimeoutMinutes, tMin, tMax);
|
deviceOwnerAuthTimeoutMinutes = minmax(deviceOwnerAuthTimeoutMinutes, tMin, tMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppSettingsModel.set({ deviceOwnerAuth, deviceOwnerAuthTimeoutMinutes });
|
AppSettingsModel.set({ deviceOwnerAuth, deviceOwnerAuthTimeoutMinutes });
|
||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
|
this.appModel.checkEncryptedPasswordsStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDeviceOwnerAuthTimeout(e) {
|
changeDeviceOwnerAuthTimeout(e) {
|
||||||
|
|
|
@ -108,6 +108,18 @@
|
||||||
.open--opening & {
|
.open--opening & {
|
||||||
display: none;
|
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 {
|
&-opening-icon {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -195,3 +195,4 @@ $fa-var-paint-brush: next-fa-glyph();
|
||||||
$fa-var-at: next-fa-glyph();
|
$fa-var-at: next-fa-glyph();
|
||||||
$fa-var-usb-token: next-fa-glyph();
|
$fa-var-usb-token: next-fa-glyph();
|
||||||
$fa-var-bell: 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">
|
<div class="open__pass-field-wrap">
|
||||||
<input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024"
|
<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" />
|
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 class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="open__settings">
|
<div class="open__settings">
|
||||||
|
|
|
@ -181,8 +181,8 @@
|
||||||
<label for="settings__general-device-owner-auth">{{res 'setGenTouchId'}}:</label>
|
<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">
|
<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="" {{#unless deviceOwnerAuth}}selected{{/unless}}>{{res 'setGenTouchIdDisabled'}}</option>
|
||||||
<option value="unlock" {{#ifeq deviceOwnerAuth 'unlock'}}selected{{/ifeq}}>{{res 'setGenTouchIdUnlock'}}</option>
|
<option value="memory" {{#ifeq deviceOwnerAuth 'memory'}}selected{{/ifeq}}>{{res 'setGenTouchIdMemory'}}</option>
|
||||||
<option value="credentials" {{#ifeq deviceOwnerAuth 'credentials'}}selected{{/ifeq}}>{{res 'setGenTouchIdCredentials'}}</option>
|
<option value="file" {{#ifeq deviceOwnerAuth 'file'}}selected{{/ifeq}}>{{res 'setGenTouchIdFile'}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{{#if deviceOwnerAuth}}
|
{{#if deviceOwnerAuth}}
|
||||||
|
@ -196,7 +196,7 @@
|
||||||
<option value="480" {{#ifeq deviceOwnerAuthTimeout 480}}selected{{/ifeq}}>{{#Res 'hours'}}8{{/Res}}</option>
|
<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="1440" {{#ifeq deviceOwnerAuthTimeout 1440}}selected{{/ifeq}}>{{Res 'oneDay'}}</option>
|
||||||
<option value="10080" {{#ifeq deviceOwnerAuthTimeout 10080}}selected{{/ifeq}}>{{Res 'oneWeek'}}</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="43200" {{#ifeq deviceOwnerAuthTimeout 43200}}selected{{/ifeq}}>{{Res 'oneMonth'}}</option>
|
||||||
<option value="525600" {{#ifeq deviceOwnerAuthTimeout 525600}}selected{{/ifeq}}>{{Res 'oneYear'}}</option>
|
<option value="525600" {{#ifeq deviceOwnerAuthTimeout 525600}}selected{{/ifeq}}>{{Res 'oneYear'}}</option>
|
||||||
{{/ifeq}}
|
{{/ifeq}}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const { readXoredValue, makeXoredValue } = require('../util/byte-utils');
|
const { readXoredValue, makeXoredValue } = require('../util/byte-utils');
|
||||||
const { reqNative } = require('../util/req-native');
|
const { reqNative } = require('../util/req-native');
|
||||||
|
|
||||||
|
let testCipherParams;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
hardwareEncrypt,
|
hardwareEncrypt,
|
||||||
hardwareDecrypt
|
hardwareDecrypt
|
||||||
|
@ -30,11 +32,36 @@ async function hardwareCrypto(value, encrypt, touchIdPrompt) {
|
||||||
const data = readXoredValue(value);
|
const data = readXoredValue(value);
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
if (encrypt) {
|
const isDev = !__dirname.includes('.asar');
|
||||||
await checkKey();
|
if (isDev && process.env.KEEWEB_EMULATE_HARDWARE_ENCRYPTION) {
|
||||||
res = await secureEnclave.encrypt({ keyTag, data });
|
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 {
|
} 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);
|
data.fill(0);
|
||||||
|
|
|
@ -3,35 +3,35 @@ const crypto = require('crypto');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
readXoredValue: function readXoredValue(val) {
|
readXoredValue: function readXoredValue(val) {
|
||||||
const data = Buffer.from(val.data);
|
const data = Buffer.from(val.data);
|
||||||
const random = Buffer.from(val.random);
|
const salt = Buffer.from(val.salt);
|
||||||
|
|
||||||
val.data.fill(0);
|
val.data.fill(0);
|
||||||
val.random.fill(0);
|
val.salt.fill(0);
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
data[i] ^= random[i];
|
data[i] ^= salt[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
random.fill(0);
|
salt.fill(0);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
makeXoredValue: function makeXoredValue(val) {
|
makeXoredValue: function makeXoredValue(val) {
|
||||||
const data = Buffer.from(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++) {
|
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);
|
data.fill(0);
|
||||||
random.fill(0);
|
salt.fill(0);
|
||||||
|
|
||||||
val.fill(0);
|
val.fill(0);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
result.data.fill(0);
|
result.data.fill(0);
|
||||||
result.random.fill(0);
|
result.salt.fill(0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
"start": "grunt",
|
"start": "grunt",
|
||||||
"test": "grunt test",
|
"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",
|
"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": "grunt dev",
|
||||||
"dev-desktop-macos": "grunt dev-desktop-darwin --skip-sign",
|
"dev-desktop-macos": "grunt dev-desktop-darwin --skip-sign",
|
||||||
"dev-desktop-macos-signed": "grunt dev-desktop-darwin-signed",
|
"dev-desktop-macos-signed": "grunt dev-desktop-darwin-signed",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user