mirror of https://github.com/keeweb/keeweb.git
276 lines
8.7 KiB
JavaScript
276 lines
8.7 KiB
JavaScript
import { Events } from 'framework/events';
|
|
import { ExternalOtpDeviceModel } from 'models/external/external-otp-device-model';
|
|
import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model';
|
|
import { Launcher } from 'comp/launcher';
|
|
import { Logger } from 'util/logger';
|
|
import { UsbListener } from 'comp/app/usb-listener';
|
|
import { AppSettingsModel } from 'models/app-settings-model';
|
|
import { Timeouts } from 'const/timeouts';
|
|
import { Locale } from 'util/locale';
|
|
|
|
let ykmanStatus;
|
|
|
|
const logger = new Logger('yubikey');
|
|
|
|
class YubiKeyOtpModel extends ExternalOtpDeviceModel {
|
|
constructor(props) {
|
|
super({
|
|
id: 'yubikey',
|
|
name: 'YubiKey',
|
|
shortName: 'YubiKey',
|
|
deviceClassName: 'YubiKey',
|
|
...props
|
|
});
|
|
}
|
|
|
|
static get ykmanStatus() {
|
|
return ykmanStatus;
|
|
}
|
|
|
|
onUsbDevicesChanged = () => {
|
|
if (UsbListener.attachedYubiKeys.length === 0) {
|
|
this.emit('ejected');
|
|
}
|
|
};
|
|
|
|
open(callback) {
|
|
this._open(callback, true);
|
|
}
|
|
|
|
_open(callback, canRetry) {
|
|
logger.info('Listing YubiKeys');
|
|
|
|
if (UsbListener.attachedYubiKeys.length === 0) {
|
|
return callback('No YubiKeys');
|
|
}
|
|
this.openProcess = Launcher.spawn({
|
|
cmd: 'ykman',
|
|
args: ['list'],
|
|
noStdOutLogging: true,
|
|
complete: (err, stdout) => {
|
|
this.openProcess = null;
|
|
if (this.openAborted) {
|
|
return callback('Open aborted');
|
|
}
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
const yubiKeysIncludingEmpty = stdout
|
|
.trim()
|
|
.split(/\n/g)
|
|
.map(line => (line.match(/\d{5,}$/g) || [])[0]);
|
|
|
|
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) {
|
|
this._repairStuckYubiKey(callback);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!yubiKeys || !yubiKeys.length) {
|
|
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++;
|
|
}
|
|
if (yubiKeys && yubiKeys.length) {
|
|
openNextYubiKey();
|
|
} else {
|
|
if (openSuccess) {
|
|
this._openComplete();
|
|
}
|
|
callback(openSuccess ? null : openErrors[0]);
|
|
}
|
|
});
|
|
};
|
|
openNextYubiKey();
|
|
}
|
|
});
|
|
}
|
|
|
|
_addYubiKey(serial, callback) {
|
|
logger.info('Adding YubiKey', serial);
|
|
|
|
this.openProcess = Launcher.spawn({
|
|
cmd: 'ykman',
|
|
args: ['-d', serial, 'oath', 'code'],
|
|
noStdOutLogging: true,
|
|
throwOnStdErr: true,
|
|
complete: (err, stdout) => {
|
|
this.openProcess = null;
|
|
if (this.openAborted) {
|
|
return callback('Open aborted');
|
|
}
|
|
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),
|
|
device: this,
|
|
deviceSubId: serial,
|
|
icon: 'clock-o',
|
|
title,
|
|
user,
|
|
needsTouch
|
|
})
|
|
);
|
|
}
|
|
callback();
|
|
}
|
|
});
|
|
}
|
|
|
|
_repairStuckYubiKey(callback) {
|
|
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;
|
|
setTimeout(() => {
|
|
this._open(callback, false);
|
|
}, Timeouts.ExternalDeviceAfterReconnect);
|
|
}
|
|
};
|
|
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
|
|
|
|
Launcher.spawn({
|
|
cmd: 'ykman',
|
|
args: ['config', 'usb', '-e', 'oath', '-f'],
|
|
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);
|
|
}
|
|
|
|
cancelOpen() {
|
|
logger.info('Cancel open');
|
|
Events.off('usb-devices-changed', this.onUsbDevicesChanged);
|
|
this.openAborted = true;
|
|
if (this.openProcess) {
|
|
logger.info('Killing the process');
|
|
try {
|
|
this.openProcess.kill();
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
getOtp(entry, callback) {
|
|
const msPeriod = 30000;
|
|
const timeLeft = msPeriod - (Date.now() % msPeriod) + 500;
|
|
return Launcher.spawn({
|
|
cmd: 'ykman',
|
|
args: [
|
|
'-d',
|
|
entry.deviceSubId,
|
|
'oath',
|
|
'code',
|
|
'--single',
|
|
`${entry.title}:${entry.user}`
|
|
],
|
|
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();
|
|
}
|
|
}
|
|
|
|
close(callback) {
|
|
Events.off('usb-devices-changed', this.onUsbDevicesChanged);
|
|
this.set({
|
|
active: false
|
|
});
|
|
}
|
|
|
|
static checkToolStatus() {
|
|
if (ykmanStatus === 'ok') {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise(resolve => {
|
|
ykmanStatus = 'checking';
|
|
Launcher.spawn({
|
|
cmd: 'ykman',
|
|
args: ['-v'],
|
|
noStdOutLogging: true,
|
|
complete: (err, stdout, code) => {
|
|
if (err || code !== 0) {
|
|
ykmanStatus = 'error';
|
|
} else {
|
|
ykmanStatus = 'ok';
|
|
}
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
YubiKeyOtpModel.defineModelProperties({
|
|
onUsbDevicesChanged: null,
|
|
openProcess: null,
|
|
openAborted: false
|
|
});
|
|
|
|
export { YubiKeyOtpModel };
|