keeweb/desktop/native-module-host.js

313 lines
8.2 KiB
JavaScript

const { readXoredValue, makeXoredValue } = require('./scripts/util/byte-utils');
const { reqNative } = require('./scripts/util/req-native');
const YubiKeyVendorIds = [0x1050];
const attachedYubiKeys = [];
let usbListenerRunning = false;
let usbListenerInitialized = false;
let autoType;
let callback;
const messageHandlers = {
start() {},
startUsbListener() {
if (usbListenerRunning) {
return;
}
if (!usbListenerInitialized) {
const usbDetection = reqNative('usb-detection');
usbDetection.registerAdded(usbDeviceAttached);
usbDetection.registerRemoved(usbDeviceDetached);
usbDetection.startMonitoring();
usbListenerInitialized = true;
}
fillAttachedYubiKeys();
usbListenerRunning = true;
},
stopUsbListener() {
if (!usbListenerRunning) {
return;
}
usbListenerRunning = false;
attachedYubiKeys.length = 0;
},
getYubiKeys(config) {
return new Promise((resolve, reject) => {
const ykChapResp = reqNative('yubikey-chalresp');
ykChapResp.getYubiKeys(config, (err, yubiKeys) => {
if (err) {
reject(err);
} else {
resolve(yubiKeys);
}
});
});
},
yubiKeyChallengeResponse(yubiKey, challenge, slot, callbackId) {
const ykChalResp = reqNative('yubikey-chalresp');
challenge = Buffer.from(challenge);
ykChalResp.challengeResponse(yubiKey, challenge, slot, (error, result) => {
if (error) {
error = errorToTransport(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('yubiKeyChallengeResponseResult', { callbackId, error, result });
});
},
yubiKeyCancelChallengeResponse() {
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 {
resolve(makeXoredValue(res));
}
});
} catch (e) {
reject(e);
}
});
},
kbdGetActiveWindow(options) {
return getAutoType().activeWindow(options);
},
kbdGetActivePid() {
return getAutoType().activePid();
},
kbdShowWindow(win) {
return getAutoType().showWindow(win);
},
kbdText(str) {
return getAutoType().text(str);
},
kbdTextAsKeys(str, mods) {
return kbdTextAsKeys(str, mods);
},
kbdKeyPress(code, modifiers) {
return getAutoType().keyPress(kbdKeyCode(code), kbdModifier(modifiers));
},
kbdShortcut(code) {
return getAutoType().shortcut(kbdKeyCode(code));
},
kbdKeyMoveWithModifier(down, modifiers) {
return getAutoType().keyMoveWithModifier(down, kbdModifier(modifiers));
},
kbdKeyPressWithCharacter(character, code, modifiers) {
const typer = getAutoType();
typer.keyMoveWithCharacter(true, character, code, kbdModifier(modifiers));
typer.keyMoveWithCharacter(false, character, code, kbdModifier(modifiers));
},
kbdEnsureModifierNotPressed() {
return getAutoType().ensureModifierNotPressed();
}
};
function isYubiKey(device) {
return YubiKeyVendorIds.includes(device.vendorId);
}
function usbDeviceAttached(device) {
if (!usbListenerRunning) {
return;
}
if (isYubiKey(device)) {
attachedYubiKeys.push(device);
reportYubiKeys();
}
}
function usbDeviceDetached(device) {
if (!usbListenerRunning) {
return;
}
if (isYubiKey(device)) {
const index = attachedYubiKeys.findIndex((yk) => yk.deviceAddress === device.deviceAddress);
if (index >= 0) {
attachedYubiKeys.splice(index, 1);
}
reportYubiKeys();
}
}
function fillAttachedYubiKeys() {
const usbDetection = reqNative('usb-detection');
usbDetection.find((ignoredError, devices) => {
if (devices) {
attachedYubiKeys.push(...devices.filter(isYubiKey));
reportYubiKeys();
}
return undefined;
});
}
function reportYubiKeys() {
callback('yubikeys', attachedYubiKeys.length);
}
function getAutoType() {
if (!autoType) {
const keyboardAutoType = reqNative('keyboard-auto-type');
autoType = new keyboardAutoType.AutoType();
autoType.setCheckPressedModifiers(false);
}
return autoType;
}
function kbdKeyCode(code) {
const { KeyCode } = reqNative('keyboard-auto-type');
const kbdCode = KeyCode[code];
if (!kbdCode) {
throw new Error(`Bad code: ${code}`);
}
return kbdCode;
}
function kbdModifier(modifiers) {
const { Modifier } = reqNative('keyboard-auto-type');
let modifier = Modifier.None;
if (modifiers) {
for (const mod of modifiers) {
if (!Modifier[mod]) {
throw new Error(`Bad modifier: ${mod}`);
}
modifier |= Modifier[mod];
}
}
return modifier;
}
function kbdTextAsKeys(str, modifiers) {
const modifier = kbdModifier(modifiers);
let ix = 0;
const typer = getAutoType();
const tx = typer.beginBatchTextEntry();
try {
for (const kc of typer.osKeyCodesForChars(str)) {
const char = str[ix++];
let effectiveModifier = modifier;
if (kc?.modifier) {
typer.keyMoveWithModifier(true, kc.modifier);
effectiveModifier |= kc.modifier;
}
if (kc) {
typer.keyMoveWithCharacter(true, null, kc.code, effectiveModifier);
typer.keyMoveWithCharacter(false, null, kc.code, effectiveModifier);
} else {
typer.keyMoveWithCharacter(true, char, null, effectiveModifier);
typer.keyMoveWithCharacter(false, char, null, effectiveModifier);
}
if (kc?.modifier) {
typer.keyMoveWithModifier(false, kc.modifier);
}
}
} finally {
tx.done();
}
}
function handleMessage({ 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 = errorToTransport(error);
callback('result', { callId, error });
});
}
function errorToTransport(error) {
const obj = {
name: error.name,
message: error.message,
stack: error.stack,
code: error.code
};
for (const [key, val] of Object.entries(error)) {
obj[key] = val;
}
return obj;
}
function startInOwnProcess() {
callback = (cmd, ...args) => {
try {
process.send({ cmd, args });
} catch {}
};
process.on('message', handleMessage);
process.on('disconnect', () => {
process.exit(0);
});
}
function startInMain(channel) {
channel.on('send', handleMessage);
callback = (cmd, ...args) => {
channel.emit('message', { cmd, args });
};
const { app } = require('electron');
app.on('will-quit', () => {
if (usbListenerInitialized) {
reqNative('usb-detection').stopMonitoring();
}
});
}
module.exports = { startInOwnProcess, startInMain };