moved native modules to a native module host

This commit is contained in:
antelle 2020-06-06 13:30:35 +02:00
parent c1d354a6fb
commit 082c6bee93
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
13 changed files with 560 additions and 199 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,8 @@ const Timeouts = {
DefaultHttpRequest: 60000,
ExternalDeviceReconnect: 3000,
ExternalDeviceAfterReconnect: 1000,
FieldLabelDoubleClick: 300
FieldLabelDoubleClick: 300,
NativeModuleHostRestartTime: 3000
};
export { Timeouts };

View File

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

View File

@ -19,7 +19,7 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
}
onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys.length === 0) {
if (UsbListener.attachedYubiKeys === 0) {
this.emit('ejected');
}
};

View File

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

View File

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

View File

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

View File

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

View File

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