mirror of https://github.com/keeweb/keeweb
moved native modules to a native module host
parent
c1d354a6fb
commit
082c6bee93
|
@ -41,6 +41,7 @@ const ChalRespCalculator = {
|
|||
complete() {
|
||||
const err = new Error(Locale.yubiKeyDisabledErrorHeader);
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
reject(err);
|
||||
}
|
||||
|
@ -158,6 +159,7 @@ const ChalRespCalculator = {
|
|||
|
||||
const err = new Error('User canceled the YubiKey no key prompt');
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -177,6 +179,7 @@ const ChalRespCalculator = {
|
|||
|
||||
const err = new Error('User canceled the YubiKey touch prompt');
|
||||
err.userCanceled = true;
|
||||
err.ykError = true;
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { YubiKeyVendorId } from 'const/hardware';
|
||||
import { Features } from 'util/features';
|
||||
|
||||
const logger = new Logger('usb-listener');
|
||||
|
||||
const UsbListener = {
|
||||
supported: Features.isDesktop,
|
||||
attachedYubiKeys: [],
|
||||
attachedYubiKeys: 0,
|
||||
|
||||
init() {
|
||||
if (!this.supported) {
|
||||
return;
|
||||
}
|
||||
|
||||
Events.on('native-modules-yubikeys', (e) => {
|
||||
if (e.numYubiKeys !== this.attachedYubiKeys) {
|
||||
logger.debug(`YubiKeys changed ${this.attachedYubiKeys} => ${e.numYubiKeys}`);
|
||||
this.attachedYubiKeys = e.numYubiKeys;
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
});
|
||||
|
||||
AppSettingsModel.on('change:enableUsb', (model, enabled) => {
|
||||
if (enabled) {
|
||||
this.start();
|
||||
|
@ -37,73 +44,25 @@ const UsbListener = {
|
|||
}
|
||||
|
||||
try {
|
||||
const ts = logger.ts();
|
||||
|
||||
this.usb = Launcher.reqNative('usb');
|
||||
|
||||
this.listen();
|
||||
|
||||
this.attachedYubiKeys = this.usb
|
||||
.getDeviceList()
|
||||
.filter(this.isYubiKey)
|
||||
.map((device) => ({ device }));
|
||||
|
||||
if (this.attachedYubiKeys.length > 0) {
|
||||
logger.info(`${this.attachedYubiKeys.length} YubiKey(s) found`, logger.ts(ts));
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
NativeModules.startUsbListener();
|
||||
} catch (e) {
|
||||
logger.error('Error loading USB module', e);
|
||||
logger.error('Error starting USB listener', e);
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
logger.info('Stopping USB listener');
|
||||
|
||||
if (this.usb) {
|
||||
if (this.attachedYubiKeys.length) {
|
||||
this.attachedYubiKeys = [];
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
|
||||
this.usb.off('attach', UsbListener.deviceAttached);
|
||||
this.usb.off('detach', UsbListener.deviceDetached);
|
||||
|
||||
this.usb = null;
|
||||
try {
|
||||
NativeModules.stopUsbListener();
|
||||
} catch (e) {
|
||||
logger.error('Error stopping USB listener', e);
|
||||
}
|
||||
},
|
||||
|
||||
listen() {
|
||||
this.usb.on('attach', UsbListener.deviceAttached);
|
||||
this.usb.on('detach', UsbListener.deviceDetached);
|
||||
},
|
||||
|
||||
deviceAttached(device) {
|
||||
if (UsbListener.isYubiKey(device)) {
|
||||
UsbListener.attachedYubiKeys.push({ device });
|
||||
logger.info(`YubiKey attached, total: ${UsbListener.attachedYubiKeys.length}`, device);
|
||||
if (this.attachedYubiKeys) {
|
||||
this.attachedYubiKeys = 0;
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
},
|
||||
|
||||
deviceDetached(device) {
|
||||
if (UsbListener.isYubiKey(device)) {
|
||||
const index = UsbListener.attachedYubiKeys.findIndex(
|
||||
(yk) => yk.device.deviceAddress === device.deviceAddress
|
||||
);
|
||||
if (index >= 0) {
|
||||
UsbListener.attachedYubiKeys.splice(index, 1);
|
||||
logger.info(
|
||||
`YubiKey detached, total: ${UsbListener.attachedYubiKeys.length}`,
|
||||
device
|
||||
);
|
||||
Events.emit('usb-devices-changed');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isYubiKey(device) {
|
||||
return device.deviceDescriptor.idVendor === YubiKeyVendorId;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Events } from 'framework/events';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
import { Logger } from 'util/logger';
|
||||
import { UsbListener } from 'comp/app/usb-listener';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
|
@ -14,13 +15,6 @@ const YubiKey = {
|
|||
process: null,
|
||||
aborted: false,
|
||||
|
||||
get ykChalResp() {
|
||||
if (!this._ykChalResp) {
|
||||
this._ykChalResp = Launcher.reqNative('yubikey-chalresp');
|
||||
}
|
||||
return this._ykChalResp;
|
||||
},
|
||||
|
||||
cmd() {
|
||||
if (this._cmd) {
|
||||
return this._cmd;
|
||||
|
@ -70,21 +64,20 @@ const YubiKey = {
|
|||
},
|
||||
|
||||
list(callback) {
|
||||
this.ykChalResp.getYubiKeys({}, (err, yubiKeys) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
|
||||
return {
|
||||
vid,
|
||||
pid,
|
||||
serial,
|
||||
slots,
|
||||
fullName: this.getKeyFullName(pid, version, serial)
|
||||
};
|
||||
});
|
||||
callback(null, yubiKeys);
|
||||
});
|
||||
NativeModules.getYubiKeys({})
|
||||
.then((yubiKeys) => {
|
||||
yubiKeys = yubiKeys.map(({ serial, vid, pid, version, slots }) => {
|
||||
return {
|
||||
vid,
|
||||
pid,
|
||||
serial,
|
||||
slots,
|
||||
fullName: this.getKeyFullName(pid, version, serial)
|
||||
};
|
||||
});
|
||||
callback(null, yubiKeys);
|
||||
})
|
||||
.catch(callback);
|
||||
},
|
||||
|
||||
getKeyFullName(pid, version, serial) {
|
||||
|
@ -113,7 +106,7 @@ const YubiKey = {
|
|||
|
||||
logger.info('Listing YubiKeys');
|
||||
|
||||
if (UsbListener.attachedYubiKeys.length === 0) {
|
||||
if (!UsbListener.attachedYubiKeys) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
|
@ -171,9 +164,9 @@ const YubiKey = {
|
|||
logger.info('Repairing a stuck YubiKey');
|
||||
|
||||
let openTimeout;
|
||||
const countYubiKeys = UsbListener.attachedYubiKeys.length;
|
||||
const countYubiKeys = UsbListener.attachedYubiKeys;
|
||||
const onDevicesChangedDuringRepair = () => {
|
||||
if (UsbListener.attachedYubiKeys.length === countYubiKeys) {
|
||||
if (UsbListener.attachedYubiKeys === countYubiKeys) {
|
||||
logger.info('YubiKey was reconnected');
|
||||
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
|
||||
clearTimeout(openTimeout);
|
||||
|
@ -274,22 +267,24 @@ const YubiKey = {
|
|||
const paddedChallenge = Buffer.alloc(YubiKeyChallengeSize, padLen);
|
||||
challenge.copy(paddedChallenge);
|
||||
|
||||
this.ykChalResp.challengeResponse(yubiKey, paddedChallenge, slot, (err, response) => {
|
||||
if (err) {
|
||||
if (err.code === this.ykChalResp.YK_ENOKEY) {
|
||||
err.noKey = true;
|
||||
NativeModules.yubiKeyChallengeResponse(
|
||||
yubiKey,
|
||||
[...paddedChallenge],
|
||||
slot,
|
||||
(err, result) => {
|
||||
if (result) {
|
||||
result = Buffer.from(result);
|
||||
}
|
||||
if (err.code === this.ykChalResp.YK_ETIMEOUT) {
|
||||
err.timeout = true;
|
||||
if (err) {
|
||||
err.ykError = true;
|
||||
}
|
||||
return callback(err);
|
||||
return callback(err, result);
|
||||
}
|
||||
callback(null, response);
|
||||
});
|
||||
);
|
||||
},
|
||||
|
||||
cancelChalResp() {
|
||||
this.ykChalResp.cancelChallengeResponse();
|
||||
NativeModules.yubiKeyCancelChallengeResponse();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -26,9 +26,6 @@ const Launcher = {
|
|||
remReq(mod) {
|
||||
return this.electron().remote.require(mod);
|
||||
},
|
||||
reqNative(mod) {
|
||||
return this.electron().remote.app.reqNative(mod);
|
||||
},
|
||||
openLink(href) {
|
||||
if (/^(http|https|ftp|sftp|mailto):/i.test(href)) {
|
||||
this.electron().shell.openExternal(href);
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
|
||||
let NativeModules;
|
||||
|
||||
if (Launcher) {
|
||||
const logger = new Logger('native-module-connector');
|
||||
|
||||
let host;
|
||||
let callId = 0;
|
||||
let promises = {};
|
||||
let ykChalRespCallbacks = {};
|
||||
|
||||
const handlers = {
|
||||
yubikeys(numYubiKeys) {
|
||||
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
||||
},
|
||||
|
||||
log(...args) {
|
||||
logger.info('Message from host', ...args);
|
||||
},
|
||||
|
||||
result({ callId, result, error }) {
|
||||
const promise = promises[callId];
|
||||
if (promise) {
|
||||
delete promises[callId];
|
||||
if (error) {
|
||||
logger.error('Received an error', promise.cmd, error);
|
||||
promise.reject(error);
|
||||
} else {
|
||||
promise.resolve(result);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'yk-chal-resp-result'({ callbackId, error, result }) {
|
||||
const callback = ykChalRespCallbacks[callbackId];
|
||||
if (callback) {
|
||||
const willBeCalledAgain = error && error.touchRequested;
|
||||
if (!willBeCalledAgain) {
|
||||
delete ykChalRespCallbacks[callbackId];
|
||||
}
|
||||
callback(error, result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NativeModules = {
|
||||
startHost() {
|
||||
if (host) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Starting native module host');
|
||||
|
||||
const path = Launcher.req('path');
|
||||
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
|
||||
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
|
||||
|
||||
const { fork } = Launcher.req('child_process');
|
||||
|
||||
host = fork(mainModulePath);
|
||||
|
||||
host.on('message', (message) => this.hostCallback(message));
|
||||
|
||||
host.on('error', (e) => this.hostError(e));
|
||||
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
||||
|
||||
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
||||
|
||||
if (this.usbListenerRunning) {
|
||||
this.call('start-usb');
|
||||
}
|
||||
},
|
||||
|
||||
hostError(e) {
|
||||
logger.error('Host error', e);
|
||||
},
|
||||
|
||||
hostExit(code, sig) {
|
||||
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
||||
host = null;
|
||||
|
||||
const err = new Error('Native module host crashed');
|
||||
|
||||
for (const promise of Object.values(promises)) {
|
||||
promise.reject(err);
|
||||
}
|
||||
promises = {};
|
||||
|
||||
for (const callback of Object.values(ykChalRespCallbacks)) {
|
||||
callback(err);
|
||||
}
|
||||
ykChalRespCallbacks = {};
|
||||
|
||||
if (code !== 0) {
|
||||
this.autoRestartHost();
|
||||
}
|
||||
},
|
||||
|
||||
hostCallback(message) {
|
||||
const { cmd, args } = message;
|
||||
// logger.debug('Callback', cmd, args);
|
||||
if (handlers[cmd]) {
|
||||
handlers[cmd](...args);
|
||||
} else {
|
||||
logger.error('No callback', cmd);
|
||||
}
|
||||
},
|
||||
|
||||
autoRestartHost() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.startHost();
|
||||
} catch (e) {
|
||||
logger.error('Native module host failed to auto-restart', e);
|
||||
}
|
||||
}, Timeouts.NativeModuleHostRestartTime);
|
||||
},
|
||||
|
||||
call(cmd, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!host) {
|
||||
try {
|
||||
this.startHost();
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
callId++;
|
||||
if (callId === Number.MAX_SAFE_INTEGER) {
|
||||
callId = 1;
|
||||
}
|
||||
// logger.debug('Call', cmd, args, callId);
|
||||
promises[callId] = { cmd, resolve, reject };
|
||||
host.send({ cmd, args, callId });
|
||||
});
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('start-usb');
|
||||
this.usbListenerRunning = true;
|
||||
},
|
||||
|
||||
stopUsbListener() {
|
||||
this.usbListenerRunning = false;
|
||||
if (host) {
|
||||
this.call('stop-usb');
|
||||
}
|
||||
},
|
||||
|
||||
getYubiKeys(config) {
|
||||
return this.call('get-yubikeys', config);
|
||||
},
|
||||
|
||||
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||
ykChalRespCallbacks[callId] = callback;
|
||||
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
|
||||
},
|
||||
|
||||
yubiKeyCancelChallengeResponse() {
|
||||
if (host) {
|
||||
this.call('yk-cancel-chal-resp');
|
||||
}
|
||||
},
|
||||
|
||||
argon2(password, salt, options) {
|
||||
return this.call('argon2', password, salt, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { NativeModules };
|
|
@ -15,7 +15,8 @@ const Timeouts = {
|
|||
DefaultHttpRequest: 60000,
|
||||
ExternalDeviceReconnect: 3000,
|
||||
ExternalDeviceAfterReconnect: 1000,
|
||||
FieldLabelDoubleClick: 300
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000
|
||||
};
|
||||
|
||||
export { Timeouts };
|
||||
|
|
|
@ -532,7 +532,7 @@ class AppModel {
|
|||
params,
|
||||
(err, file) => {
|
||||
if (err) {
|
||||
if (err.name === 'KdbxError' || err.userCanceled) {
|
||||
if (err.name === 'KdbxError' || err.ykError) {
|
||||
return callback(err);
|
||||
}
|
||||
logger.info(
|
||||
|
@ -558,7 +558,7 @@ class AppModel {
|
|||
setTimeout(() => this.syncFile(file), 0);
|
||||
callback(err);
|
||||
} else {
|
||||
if (err.name === 'KdbxError' || err.userCanceled) {
|
||||
if (err.name === 'KdbxError' || err.ykError) {
|
||||
return callback(err);
|
||||
}
|
||||
logger.info(
|
||||
|
@ -1239,13 +1239,13 @@ class AppModel {
|
|||
usbDevicesChanged() {
|
||||
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
||||
|
||||
this.attachedYubiKeysCount = UsbListener.attachedYubiKeys.length;
|
||||
this.attachedYubiKeysCount = UsbListener.attachedYubiKeys;
|
||||
|
||||
if (!this.settings.yubiKeyAutoOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNewYubiKey = UsbListener.attachedYubiKeys.length > attachedYubiKeysCount;
|
||||
const isNewYubiKey = UsbListener.attachedYubiKeys > attachedYubiKeysCount;
|
||||
const hasOpenFiles = this.files.some((file) => file.active && !file.external);
|
||||
|
||||
if (isNewYubiKey && hasOpenFiles && !this.openingOtpDevice) {
|
||||
|
|
|
@ -19,7 +19,7 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
|
|||
}
|
||||
|
||||
onUsbDevicesChanged = () => {
|
||||
if (UsbListener.attachedYubiKeys.length === 0) {
|
||||
if (UsbListener.attachedYubiKeys === 0) {
|
||||
this.emit('ejected');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import kdbxweb from 'kdbxweb';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Features } from 'util/features';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { NativeModules } from 'comp/launcher/native-modules';
|
||||
|
||||
const logger = new Logger('argon2');
|
||||
|
||||
|
@ -29,36 +29,69 @@ const KdbxwebInit = {
|
|||
if (!global.WebAssembly) {
|
||||
return Promise.reject('WebAssembly is not supported');
|
||||
}
|
||||
if (Launcher && AppSettingsModel.nativeArgon2) {
|
||||
const ts = logger.ts();
|
||||
const argon2 = Launcher.reqNative('argon2');
|
||||
logger.debug('Native argon2 runtime loaded (main thread)', logger.ts(ts));
|
||||
if (Features.isDesktop && AppSettingsModel.nativeArgon2) {
|
||||
logger.debug('Using native argon2');
|
||||
this.runtimeModule = {
|
||||
hash(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ts = logger.ts();
|
||||
argon2.hash(
|
||||
Buffer.from(args.password),
|
||||
Buffer.from(args.salt),
|
||||
{
|
||||
type: args.type,
|
||||
version: args.version,
|
||||
hashLength: args.length,
|
||||
saltLength: args.salt.length,
|
||||
timeCost: args.iterations,
|
||||
parallelism: args.parallelism,
|
||||
memoryCost: args.memory
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
logger.error('Argon2 error', err);
|
||||
return reject(err);
|
||||
}
|
||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||
resolve(res);
|
||||
}
|
||||
);
|
||||
});
|
||||
const ts = logger.ts();
|
||||
|
||||
const password = makeXoredValue(args.password);
|
||||
const salt = makeXoredValue(args.salt);
|
||||
|
||||
return NativeModules.argon2(password, salt, {
|
||||
type: args.type,
|
||||
version: args.version,
|
||||
hashLength: args.length,
|
||||
saltLength: args.salt.length,
|
||||
timeCost: args.iterations,
|
||||
parallelism: args.parallelism,
|
||||
memoryCost: args.memory
|
||||
})
|
||||
.then((res) => {
|
||||
password.data.fill(0);
|
||||
salt.data.fill(0);
|
||||
|
||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||
|
||||
return readXoredValue(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
password.data.fill(0);
|
||||
salt.data.fill(0);
|
||||
|
||||
logger.error('Argon2 error', err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
return Promise.resolve(this.runtimeModule);
|
||||
|
|
|
@ -106,7 +106,7 @@ class OpenView extends View {
|
|||
!this.model.settings.canOpen &&
|
||||
!this.model.settings.canCreate &&
|
||||
!(this.model.settings.canOpenDemo && !this.model.settings.demoOpened);
|
||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;
|
||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys;
|
||||
const canOpenYubiKey =
|
||||
hasYubiKeys &&
|
||||
this.model.settings.canOpenOtpDevice &&
|
||||
|
@ -992,7 +992,7 @@ class OpenView extends View {
|
|||
|
||||
usbDevicesChanged() {
|
||||
if (this.model.settings.canOpenOtpDevice) {
|
||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;
|
||||
const hasYubiKeys = !!UsbListener.attachedYubiKeys;
|
||||
|
||||
const showOpenIcon = hasYubiKeys && this.model.settings.yubiKeyShowIcon;
|
||||
this.$el.find('.open__icon-yubikey').toggleClass('hide', !showOpenIcon);
|
||||
|
|
|
@ -724,7 +724,7 @@ class SettingsFileView extends View {
|
|||
if (!Launcher || !AppSettingsModel.enableUsb || !AppSettingsModel.yubiKeyShowChalResp) {
|
||||
return;
|
||||
}
|
||||
if (!UsbListener.attachedYubiKeys.length) {
|
||||
if (!UsbListener.attachedYubiKeys) {
|
||||
if (this.yubiKeys.length) {
|
||||
this.yubiKeys = [];
|
||||
this.render();
|
||||
|
@ -738,7 +738,7 @@ class SettingsFileView extends View {
|
|||
this.render();
|
||||
if (
|
||||
userInitiated &&
|
||||
UsbListener.attachedYubiKeys.length &&
|
||||
UsbListener.attachedYubiKeys &&
|
||||
!yubiKeys.length &&
|
||||
Features.isMac
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const electron = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
let perfTimestamps = global.perfTimestamps;
|
||||
perfTimestamps.push({ name: 'loading app requires', ts: process.hrtime() });
|
||||
|
@ -16,7 +15,6 @@ let restartPending = false;
|
|||
let mainWindowPosition = {};
|
||||
let updateMainWindowPositionTimeout = null;
|
||||
let mainWindowMaximized = false;
|
||||
let usbBinding = null;
|
||||
|
||||
const windowPositionFileName = 'window-position.json';
|
||||
const portableConfigFileName = 'keeweb-portable.json';
|
||||
|
@ -187,10 +185,11 @@ app.setHookBeforeQuitEvent = (hooked) => {
|
|||
app.hookBeforeQuitEvent = !!hooked;
|
||||
};
|
||||
app.setGlobalShortcuts = setGlobalShortcuts;
|
||||
app.reqNative = reqNative;
|
||||
app.showAndFocusMainWindow = showAndFocusMainWindow;
|
||||
app.loadConfig = loadConfig;
|
||||
app.saveConfig = saveConfig;
|
||||
app.getAppMainRoot = getAppMainRoot;
|
||||
app.getAppContentRoot = getAppContentRoot;
|
||||
|
||||
function setSystemAppearance() {
|
||||
if (process.platform === 'darwin') {
|
||||
|
@ -402,7 +401,6 @@ function mainWindowClosing() {
|
|||
}
|
||||
|
||||
function mainWindowClosed() {
|
||||
usbBinding?.removeAllListeners();
|
||||
app.removeAllListeners('remote-app-event');
|
||||
}
|
||||
|
||||
|
@ -567,35 +565,20 @@ function setUserDataPaths() {
|
|||
|
||||
perfTimestamps?.push({ name: 'portable check', ts: process.hrtime() });
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Is portable:', isPortable);
|
||||
|
||||
if (isPortable) {
|
||||
const portableConfigDir = path.dirname(execPath);
|
||||
const portableConfigPath = path.join(portableConfigDir, portableConfigFileName);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Portable config path:', portableConfigPath);
|
||||
|
||||
if (fs.existsSync(portableConfigPath)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Portable config path exists');
|
||||
|
||||
const portableConfig = JSON.parse(fs.readFileSync(portableConfigPath, 'utf8'));
|
||||
const portableUserDataDir = path.resolve(portableConfigDir, portableConfig.userDataDir);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Portable user data dir:', portableUserDataDir);
|
||||
|
||||
if (!fs.existsSync(portableUserDataDir)) {
|
||||
fs.mkdirSync(portableUserDataDir);
|
||||
}
|
||||
|
||||
app.setPath('userData', portableUserDataDir);
|
||||
usingPortableUserDataDir = true;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Portable config path doesn't exist`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -743,55 +726,25 @@ function reportStartProfile() {
|
|||
emitRemoteEvent('start-profile', startProfile);
|
||||
}
|
||||
|
||||
function getAppMainRoot() {
|
||||
if (isDev) {
|
||||
return __dirname;
|
||||
} else {
|
||||
return process.mainModule.path;
|
||||
}
|
||||
}
|
||||
|
||||
function getAppContentRoot() {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
function reqNative(mod) {
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
|
||||
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
||||
let fullPath;
|
||||
const fullPath = path.join(getAppMainRoot(), modulePath);
|
||||
|
||||
if (isDev) {
|
||||
fullPath = path.join(__dirname, modulePath);
|
||||
} else {
|
||||
const mainAsarPath = process.mainModule.path;
|
||||
fullPath = path.join(mainAsarPath, modulePath);
|
||||
|
||||
// Currently native modules can't be updated
|
||||
// const latestAsarPath = __dirname;
|
||||
//
|
||||
// fullPath = path.join(latestAsarPath, modulePath);
|
||||
//
|
||||
// if (!fs.existsSync(fullPath)) {
|
||||
// fullPath = path.join(mainAsarPath, modulePath);
|
||||
// }
|
||||
}
|
||||
|
||||
const binding = require(fullPath);
|
||||
|
||||
if (mod === 'usb') {
|
||||
usbBinding = initUsb(binding);
|
||||
}
|
||||
|
||||
return binding;
|
||||
}
|
||||
|
||||
function initUsb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
|
||||
binding.on('newListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._enableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
binding.on('removeListener', () => {
|
||||
if (binding.listenerCount('attach') === 0 && binding.listenerCount('detach') === 0) {
|
||||
binding._disableHotplugEvents();
|
||||
}
|
||||
});
|
||||
|
||||
return binding;
|
||||
return require(fullPath);
|
||||
}
|
||||
|
||||
function loadSettingsEncryptionKey() {
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
let appMainRoot;
|
||||
const nativeModules = {};
|
||||
|
||||
const YubiKeyVendorIds = [0x1050];
|
||||
const attachedYubiKeys = [];
|
||||
let usbListenerRunning = false;
|
||||
|
||||
startListener();
|
||||
|
||||
const messageHandlers = {
|
||||
init(root) {
|
||||
appMainRoot = root;
|
||||
},
|
||||
|
||||
'start-usb'() {
|
||||
if (usbListenerRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usb = reqNative('usb');
|
||||
|
||||
fillAttachedYubiKeys();
|
||||
|
||||
usb.on('attach', usbDeviceAttached);
|
||||
usb.on('detach', usbDeviceDetached);
|
||||
|
||||
usb._enableHotplugEvents();
|
||||
|
||||
usbListenerRunning = true;
|
||||
},
|
||||
|
||||
'stop-usb'() {
|
||||
if (!usbListenerRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usb = reqNative('usb');
|
||||
|
||||
usb.off('attach', usbDeviceAttached);
|
||||
usb.off('detach', usbDeviceDetached);
|
||||
|
||||
usb._disableHotplugEvents();
|
||||
|
||||
usbListenerRunning = false;
|
||||
attachedYubiKeys.length = 0;
|
||||
},
|
||||
|
||||
'get-yubikeys'(config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ykChapResp = reqNative('yubikey-chalresp');
|
||||
ykChapResp.getYubiKeys(config, (err, yubiKeys) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(yubiKeys);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
'yk-chal-resp'(yubiKey, challenge, slot, callbackId) {
|
||||
const ykChalResp = reqNative('yubikey-chalresp');
|
||||
challenge = Buffer.from(challenge);
|
||||
ykChalResp.challengeResponse(yubiKey, challenge, slot, (error, result) => {
|
||||
if (error) {
|
||||
if (error.code === ykChalResp.YK_ENOKEY) {
|
||||
error.noKey = true;
|
||||
}
|
||||
if (error.code === ykChalResp.YK_ETIMEOUT) {
|
||||
error.timeout = true;
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
result = [...result];
|
||||
}
|
||||
return callback('yk-chal-resp-result', { callbackId, error, result });
|
||||
});
|
||||
},
|
||||
|
||||
'yk-cancel-chal-resp'() {
|
||||
const ykChalResp = reqNative('yubikey-chalresp');
|
||||
ykChalResp.cancelChallengeResponse();
|
||||
},
|
||||
|
||||
argon2(password, salt, options) {
|
||||
const argon2 = reqNative('argon2');
|
||||
|
||||
password = readXoredValue(password);
|
||||
salt = readXoredValue(salt);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
argon2.hash(password, salt, options, (err, res) => {
|
||||
password.fill(0);
|
||||
salt.fill(0);
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const xoredRes = makeXoredValue(res);
|
||||
res.fill(0);
|
||||
|
||||
resolve(xoredRes);
|
||||
|
||||
setTimeout(() => {
|
||||
xoredRes.data.fill(0);
|
||||
xoredRes.random.fill(0);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const moduleInit = {
|
||||
usb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
return binding;
|
||||
}
|
||||
};
|
||||
|
||||
function isYubiKey(device) {
|
||||
return YubiKeyVendorIds.includes(device.deviceDescriptor.idVendor);
|
||||
}
|
||||
|
||||
function usbDeviceAttached(device) {
|
||||
if (isYubiKey(device)) {
|
||||
attachedYubiKeys.push(device);
|
||||
reportYubiKeys();
|
||||
}
|
||||
}
|
||||
|
||||
function usbDeviceDetached(device) {
|
||||
if (isYubiKey(device)) {
|
||||
const index = attachedYubiKeys.findIndex((yk) => yk.deviceAddress === device.deviceAddress);
|
||||
if (index >= 0) {
|
||||
attachedYubiKeys.splice(index, 1);
|
||||
}
|
||||
reportYubiKeys();
|
||||
}
|
||||
}
|
||||
|
||||
function fillAttachedYubiKeys() {
|
||||
const usb = reqNative('usb');
|
||||
attachedYubiKeys.push(...usb.getDeviceList().filter(isYubiKey));
|
||||
reportYubiKeys();
|
||||
}
|
||||
|
||||
function reportYubiKeys() {
|
||||
callback('yubikeys', attachedYubiKeys.length);
|
||||
}
|
||||
|
||||
function reqNative(mod) {
|
||||
if (nativeModules[mod]) {
|
||||
return nativeModules[mod];
|
||||
}
|
||||
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
|
||||
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
||||
const fullPath = path.join(appMainRoot, modulePath);
|
||||
|
||||
let binding = require(fullPath);
|
||||
|
||||
if (moduleInit[mod]) {
|
||||
binding = moduleInit[mod](binding);
|
||||
}
|
||||
|
||||
nativeModules[mod] = binding;
|
||||
return binding;
|
||||
}
|
||||
|
||||
function readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
random.fill(0);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = crypto.randomBytes(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;
|
||||
}
|
||||
|
||||
function startListener() {
|
||||
process.on('message', ({ callId, cmd, args }) => {
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
const handler = messageHandlers[cmd];
|
||||
if (handler) {
|
||||
return handler(...args);
|
||||
} else {
|
||||
throw new Error(`Handler not found: ${cmd}`);
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
callback('result', { callId, result });
|
||||
})
|
||||
.catch((error) => {
|
||||
error = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
};
|
||||
callback('result', { callId, error });
|
||||
});
|
||||
});
|
||||
|
||||
process.on('disconnect', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
function callback(cmd, ...args) {
|
||||
try {
|
||||
process.send({ cmd, args });
|
||||
} catch {}
|
||||
}
|
Loading…
Reference in New Issue