keeweb/app/scripts/models/external/yubikey-otp-model.js

276 lines
8.7 KiB
JavaScript
Raw Normal View History

2020-05-05 21:26:41 +02:00
import { Events } from 'framework/events';
2020-04-15 16:50:01 +02:00
import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model';
import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model';
import { Launcher } from 'comp/launcher';
2020-05-06 18:37:37 +02:00
import { Logger } from 'util/logger';
2020-05-05 21:26:41 +02:00
import { UsbListener } from 'comp/app/usb-listener';
2020-05-06 18:37:37 +02:00
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
2020-04-15 16:50:01 +02:00
2020-04-19 20:55:38 +02:00
let ykmanStatus;
2020-04-19 20:42:55 +02:00
2020-05-06 18:37:37 +02:00
const logger = new Logger('yubikey');
2020-04-19 20:55:38 +02:00
class YubiKeyOtpModel extends ExternalOtpDeviceModel {
2020-04-15 16:50:01 +02:00
constructor(props) {
super({
2020-05-05 20:03:38 +02:00
id: 'yubikey',
name: 'YubiKey',
2020-04-15 16:50:01 +02:00
shortName: 'YubiKey',
2020-05-05 20:50:11 +02:00
deviceClassName: 'YubiKey',
2020-04-15 16:50:01 +02:00
...props
});
}
2020-04-19 20:55:38 +02:00
static get ykmanStatus() {
return ykmanStatus;
}
2020-05-05 21:26:41 +02:00
onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys.length === 0) {
this.emit('ejected');
}
};
2020-04-15 16:50:01 +02:00
open(callback) {
2020-05-07 18:58:17 +02:00
this._open(callback, true);
}
_open(callback, canRetry) {
2020-05-06 19:30:30 +02:00
logger.info('Listing YubiKeys');
2020-05-07 18:58:17 +02:00
2020-05-06 19:30:30 +02:00
if (UsbListener.attachedYubiKeys.length === 0) {
return callback('No YubiKeys');
}
this.openProcess = Launcher.spawn({
cmd: 'ykman',
2020-05-07 18:58:17 +02:00
args: ['list'],
2020-05-06 19:30:30 +02:00
noStdOutLogging: true,
complete: (err, stdout) => {
2020-05-07 23:10:23 +02:00
this.openProcess = null;
2020-05-06 19:30:30 +02:00
if (this.openAborted) {
return callback('Open aborted');
}
if (err) {
return callback(err);
}
2020-05-07 18:58:17 +02:00
const yubiKeysIncludingEmpty = stdout
.trim()
.split(/\n/g)
2020-05-07 21:22:34 +02:00
.map(line => (line.match(/\d{5,}$/g) || [])[0]);
2020-05-07 18:58:17 +02:00
const yubiKeys = yubiKeysIncludingEmpty.filter(s => s);
if (
yubiKeysIncludingEmpty.length === 1 &&
yubiKeys.length === 0 &&
stdout.startsWith('YubiKey') &&
stdout.includes('CCID') &&
!stdout.includes('Serial')
) {
logger.info('The YubiKey is probably stuck');
if (!AppSettingsModel.yubiKeyOathWorkaround) {
return callback(Locale.yubiKeyStuckError);
}
if (canRetry) {
2020-05-07 18:58:17 +02:00
this._repairStuckYubiKey(callback);
return;
}
}
if (!yubiKeys || !yubiKeys.length) {
2020-05-06 19:30:30 +02:00
return callback('No YubiKeys returned by "ykman list"');
}
let openSuccess = 0;
const openErrors = [];
const openNextYubiKey = () => {
const yubiKey = yubiKeys.shift();
this._addYubiKey(yubiKey, err => {
if (this.openAborted) {
return callback('Open aborted');
}
if (err) {
openErrors.push(err);
} else {
openSuccess++;
}
2020-05-07 18:58:17 +02:00
if (yubiKeys && yubiKeys.length) {
2020-05-06 19:30:30 +02:00
openNextYubiKey();
} else {
if (openSuccess) {
this._openComplete();
}
2020-05-06 19:30:30 +02:00
callback(openSuccess ? null : openErrors[0]);
}
});
};
openNextYubiKey();
}
});
}
2020-05-07 18:58:17 +02:00
_addYubiKey(serial, callback) {
logger.info('Adding YubiKey', serial);
2020-04-15 16:50:01 +02:00
this.openProcess = Launcher.spawn({
cmd: 'ykman',
2020-05-06 19:30:30 +02:00
args: ['-d', serial, 'oath', 'code'],
2020-04-15 16:50:01 +02:00
noStdOutLogging: true,
2020-05-06 19:30:30 +02:00
throwOnStdErr: true,
2020-05-07 19:00:18 +02:00
complete: (err, stdout) => {
2020-04-15 16:50:01 +02:00
this.openProcess = null;
2020-05-05 21:26:41 +02:00
if (this.openAborted) {
return callback('Open aborted');
}
2020-04-15 16:50:01 +02:00
if (err) {
return callback(err);
}
for (const line of stdout.split('\n')) {
const match = line.match(/^(.*?):(.*?)\s+(.*)$/);
if (!match) {
continue;
}
const [, title, user, code] = match;
const needsTouch = !code.match(/^\d+$/);
this.entries.push(
new ExternalOtpEntryModel({
id: this.entryId(title, user),
2020-04-15 16:50:01 +02:00
device: this,
2020-05-06 19:30:30 +02:00
deviceSubId: serial,
2020-04-15 16:50:01 +02:00
icon: 'clock-o',
title,
user,
needsTouch
})
);
}
callback();
}
});
}
2020-05-07 18:58:17 +02:00
_repairStuckYubiKey(callback) {
2020-05-06 23:12:49 +02:00
logger.info('Repairing a stuck YubiKey');
let openTimeout;
const countYubiKeys = UsbListener.attachedYubiKeys.length;
const onDevicesChangedDuringRepair = () => {
if (UsbListener.attachedYubiKeys.length === countYubiKeys) {
logger.info('YubiKey was reconnected');
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
clearTimeout(openTimeout);
this.openAborted = false;
2020-05-07 21:22:34 +02:00
setTimeout(() => {
this._open(callback, false);
}, Timeouts.ExternalDeviceAfterReconnect);
2020-05-06 23:12:49 +02:00
}
};
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
Launcher.spawn({
cmd: 'ykman',
2020-05-07 18:58:17 +02:00
args: ['config', 'usb', '-e', 'oath', '-f'],
2020-05-06 23:12:49 +02:00
noStdOutLogging: true,
complete: err => {
logger.info('Repair complete', err ? 'with error' : 'OK');
if (err) {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
return callback(err);
}
openTimeout = setTimeout(() => {
Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
}, Timeouts.ExternalDeviceReconnect);
}
});
}
_openComplete() {
this.active = true;
this._buildEntryMap();
Events.on('usb-devices-changed', this.onUsbDevicesChanged);
}
2020-04-15 16:50:01 +02:00
cancelOpen() {
2020-05-06 18:37:37 +02:00
logger.info('Cancel open');
2020-05-05 21:26:41 +02:00
Events.off('usb-devices-changed', this.onUsbDevicesChanged);
2020-04-15 16:50:01 +02:00
this.openAborted = true;
if (this.openProcess) {
2020-05-06 19:30:30 +02:00
logger.info('Killing the process');
try {
this.openProcess.kill();
} catch {}
2020-04-15 16:50:01 +02:00
}
}
getOtp(entry, callback) {
const msPeriod = 30000;
const timeLeft = msPeriod - (Date.now() % msPeriod) + 500;
return Launcher.spawn({
cmd: 'ykman',
2020-05-06 19:30:30 +02:00
args: [
'-d',
entry.deviceSubId,
'oath',
'code',
'--single',
`${entry.title}:${entry.user}`
],
2020-04-15 16:50:01 +02:00
noStdOutLogging: true,
complete: (err, stdout) => {
if (err) {
return callback(err, null, timeLeft);
}
const otp = stdout.trim();
callback(null, otp, timeLeft);
}
});
}
cancelGetOtp(entry, ps) {
if (ps) {
ps.kill();
}
}
2020-04-19 20:42:55 +02:00
2020-05-05 20:47:15 +02:00
close(callback) {
2020-05-05 21:26:41 +02:00
Events.off('usb-devices-changed', this.onUsbDevicesChanged);
2020-05-05 20:47:15 +02:00
this.set({
active: false
});
}
2020-04-19 20:42:55 +02:00
static checkToolStatus() {
2020-04-19 20:55:38 +02:00
if (ykmanStatus === 'ok') {
return Promise.resolve();
}
2020-04-19 20:42:55 +02:00
return new Promise(resolve => {
2020-04-19 20:55:38 +02:00
ykmanStatus = 'checking';
2020-04-19 20:42:55 +02:00
Launcher.spawn({
cmd: 'ykman',
args: ['-v'],
noStdOutLogging: true,
complete: (err, stdout, code) => {
if (err || code !== 0) {
2020-04-19 20:55:38 +02:00
ykmanStatus = 'error';
2020-04-19 20:42:55 +02:00
} else {
2020-04-19 20:55:38 +02:00
ykmanStatus = 'ok';
2020-04-19 20:42:55 +02:00
}
resolve();
}
});
});
}
2020-04-15 16:50:01 +02:00
}
YubiKeyOtpModel.defineModelProperties({
2020-05-05 21:26:41 +02:00
onUsbDevicesChanged: null,
2020-04-15 16:50:01 +02:00
openProcess: null,
openAborted: false
});
export { YubiKeyOtpModel };