keeweb/app/scripts/comp/app/chal-resp-calculator.js

176 lines
5.7 KiB
JavaScript

import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { YubiKey } from 'comp/app/yubikey';
import { UsbListener } from 'comp/app/usb-listener';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { Timeouts } from 'const/timeouts';
import { Launcher } from 'comp/launcher';
const logger = new Logger('chal-resp');
const ChalRespCalculator = {
cache: {},
getCacheKey(params) {
return `${params.vid}:${params.pid}:${params.serial}:${params.slot}`;
},
build(params) {
if (!params) {
return null;
}
return (challenge) => {
return new Promise((resolve, reject) => {
challenge = Buffer.from(challenge);
const hexChallenge = challenge.toString('hex');
const cacheKey = this.getCacheKey(params);
const respFromCache = this.cache[cacheKey]?.[hexChallenge];
if (respFromCache) {
logger.debug('Found ChalResp in cache');
return resolve(Buffer.from(respFromCache, 'hex'));
}
logger.debug('Calculating ChalResp using a YubiKey', params);
this._calc(params, challenge, (err, response) => {
if (err) {
return reject(err);
}
resolve(response);
});
});
};
},
_calc(params, challenge, callback) {
let touchAlert = null;
let userCanceled = false;
YubiKey.calculateChalResp(params, challenge, (err, response) => {
if (userCanceled) {
userCanceled = false;
return;
}
if (touchAlert) {
touchAlert.closeWithoutResult();
touchAlert = null;
}
if (err) {
if (err.noKey) {
logger.info('YubiKey ChalResp: no key');
this._showNoKeyAlert(params.serial, (err) => {
if (err) {
return callback(err);
}
this._calc(params, challenge, callback);
});
return;
} else if (err.touchRequested) {
logger.info('YubiKey ChalResp: touch requested');
touchAlert = this._showTouchAlert(params.serial, (err) => {
touchAlert = null;
userCanceled = true;
logger.info('YubiKey ChalResp canceled');
YubiKey.cancelChalResp();
callback(err);
});
return;
} else {
logger.error('YubiKey ChalResp error', err);
}
return callback(err);
}
const cacheKey = this.getCacheKey(params);
if (!this.cache[cacheKey]) {
this.cache[cacheKey] = {};
}
const hexChallenge = challenge.toString('hex');
this.cache[cacheKey][hexChallenge] = response.toString('hex');
logger.info('Calculated YubiKey ChalResp');
callback(null, response);
});
},
_showNoKeyAlert(serial, callback) {
Launcher.showMainWindow();
let noKeyAlert = null;
let deviceEnumerationTimer;
const onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys.length === 0) {
return;
}
deviceEnumerationTimer = setTimeout(() => {
YubiKey.list((err, list) => {
if (err) {
logger.error('YubiKey list error', err);
return;
}
const isAttached = list.some((yk) => yk.serial === serial);
logger.info(isAttached ? 'YubiKey found' : 'YubiKey not found');
if (isAttached) {
Events.off('usb-devices-changed', onUsbDevicesChanged);
if (noKeyAlert) {
noKeyAlert.closeWithoutResult();
}
callback();
}
});
}, Timeouts.ExternalDeviceAfterReconnect);
};
Events.on('usb-devices-changed', onUsbDevicesChanged);
noKeyAlert = Alerts.alert({
header: Locale.yubiKeyNoKeyHeader,
body: Locale.yubiKeyNoKeyBody.replace('{}', serial),
buttons: [Alerts.buttons.cancel],
iconSvg: 'usb-token',
cancel: () => {
logger.info('No key alert closed');
clearTimeout(deviceEnumerationTimer);
Events.off('usb-devices-changed', onUsbDevicesChanged);
const err = new Error('User canceled the YubiKey no key prompt');
err.userCanceled = true;
return callback(err);
}
});
},
_showTouchAlert(serial, callback) {
Launcher.showMainWindow();
return Alerts.alert({
header: Locale.yubiKeyTouchRequestedHeader,
body: Locale.yubiKeyTouchRequestedBody.replace('{}', serial),
buttons: [Alerts.buttons.cancel],
iconSvg: 'usb-token',
cancel: () => {
logger.info('Touch alert closed');
const err = new Error('User canceled the YubiKey touch prompt');
err.userCanceled = true;
return callback(err);
}
});
},
clearCache(params) {
delete this.cache[this.getCacheKey(params)];
}
};
export { ChalRespCalculator };