fix #679, fix #1383: Touch ID on macOS

This commit is contained in:
antelle 2021-02-03 20:30:59 +01:00
parent 4cdc80e530
commit 08d49ddc54
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
18 changed files with 318 additions and 73 deletions

View File

@ -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;
}
};

View File

@ -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) {

View File

@ -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

View File

@ -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",

View File

@ -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 };

View File

@ -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 {

View File

@ -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 };

View File

@ -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);

View File

@ -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);
};

View File

@ -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 };

View File

@ -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) {

View File

@ -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;

View File

@ -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();

View File

@ -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">

View File

@ -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}}

View File

@ -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);

View File

@ -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;

View File

@ -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",