From 81d4c5c403b16c238260ad5ec44c31bd627fd5db Mon Sep 17 00:00:00 2001
From: antelle
Date: Sun, 24 May 2020 18:39:56 +0200
Subject: [PATCH] extracted YubiKey module
---
app/scripts/comp/app/yubikey.js | 206 +++++++++++++++
app/scripts/const/default-app-settings.js | 2 +-
app/scripts/locales/base.json | 2 +-
.../models/external/yubikey-otp-model.js | 249 ++++--------------
app/scripts/views/open-view.js | 31 ++-
.../views/settings/settings-devices-view.js | 16 +-
app/templates/settings/settings-devices.hbs | 12 +-
7 files changed, 293 insertions(+), 225 deletions(-)
create mode 100644 app/scripts/comp/app/yubikey.js
diff --git a/app/scripts/comp/app/yubikey.js b/app/scripts/comp/app/yubikey.js
new file mode 100644
index 00000000..622c39ce
--- /dev/null
+++ b/app/scripts/comp/app/yubikey.js
@@ -0,0 +1,206 @@
+import { Events } from 'framework/events';
+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';
+
+const logger = new Logger('yubikey');
+
+const YubiKey = {
+ ykmanStatus: undefined,
+ process: null,
+ aborted: false,
+
+ checkToolStatus() {
+ if (this.ykmanStatus === 'ok') {
+ return Promise.resolve(this.ykmanStatus);
+ }
+ return new Promise(resolve => {
+ this.ykmanStatus = 'checking';
+ Launcher.spawn({
+ cmd: 'ykman',
+ args: ['-v'],
+ noStdOutLogging: true,
+ complete: (err, stdout, code) => {
+ if (err || code !== 0) {
+ this.ykmanStatus = 'error';
+ } else {
+ this.ykmanStatus = 'ok';
+ }
+ resolve(this.ykmanStatus);
+ }
+ });
+ });
+ },
+
+ abort() {
+ logger.info('Aborting');
+ if (this.process) {
+ logger.info('Killing the process');
+ try {
+ this.process.kill();
+ } catch {}
+ }
+ this.aborted = true;
+ this.process = null;
+ },
+
+ list(callback) {
+ this._list(callback, true);
+ },
+
+ _list(callback, canRetry) {
+ if (this.process) {
+ return callback('Already in progress');
+ }
+ this.aborted = false;
+
+ logger.info('Listing YubiKeys');
+
+ if (UsbListener.attachedYubiKeys.length === 0) {
+ return callback(null, []);
+ }
+
+ this.process = Launcher.spawn({
+ cmd: 'ykman',
+ args: ['list'],
+ noStdOutLogging: true,
+ complete: (err, stdout) => {
+ this.process = null;
+
+ if (this.aborted) {
+ return callback('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.yubiKeyStuckWorkaround) {
+ return callback(Locale.yubiKeyStuckError);
+ }
+ if (canRetry) {
+ return this._repairStuckYubiKey(callback);
+ }
+ }
+
+ if (!yubiKeys.length) {
+ return callback('No YubiKeys returned by "ykman list"');
+ }
+
+ callback(null, yubiKeys);
+ }
+ });
+ },
+
+ _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.aborted = false;
+ setTimeout(() => {
+ this._list(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(`YubiKey repair error: ${err}`);
+ }
+ openTimeout = setTimeout(() => {
+ Events.off('usb-devices-changed', onDevicesChangedDuringRepair);
+ }, Timeouts.ExternalDeviceReconnect);
+ }
+ });
+ },
+
+ getOtpCodes(serial, callback) {
+ if (this.process) {
+ return callback('Already in progress');
+ }
+ this.aborted = false;
+
+ this.process = Launcher.spawn({
+ cmd: 'ykman',
+ args: ['-d', serial, 'oath', 'code'],
+ noStdOutLogging: true,
+ throwOnStdErr: true,
+ complete: (err, stdout) => {
+ this.process = null;
+
+ if (this.aborted) {
+ return callback('Aborted');
+ }
+ if (err) {
+ return callback(err);
+ }
+
+ const codes = [];
+
+ 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+$/);
+
+ codes.push({
+ title,
+ user,
+ needsTouch
+ });
+ }
+
+ callback(null, codes);
+ }
+ });
+ },
+
+ getOtp(serial, entry, callback) {
+ return Launcher.spawn({
+ cmd: 'ykman',
+ args: ['-d', serial, 'oath', 'code', '--single', entry],
+ noStdOutLogging: true,
+ complete: (err, stdout) => {
+ if (err) {
+ return callback(err);
+ }
+ const otp = stdout.trim();
+ callback(null, otp);
+ }
+ });
+ }
+};
+
+export { YubiKey };
diff --git a/app/scripts/const/default-app-settings.js b/app/scripts/const/default-app-settings.js
index 5508267b..b03f1aa2 100644
--- a/app/scripts/const/default-app-settings.js
+++ b/app/scripts/const/default-app-settings.js
@@ -41,7 +41,7 @@ const DefaultAppSettings = {
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
yubiKeyMatchEntries: true, // show matching one-time codes in entries
yubiKeyShowChalResp: true, // show YubiKey challenge-response option
- yubiKeyOathWorkaround: false, // enable the workaround for YubiKey OATH issues
+ yubiKeyStuckWorkaround: false, // enable the workaround for stuck YubiKeys
canOpen: true, // can select and open new files
canOpenDemo: true, // can open a demo file
diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json
index abf4c767..e3d7f597 100644
--- a/app/scripts/locales/base.json
+++ b/app/scripts/locales/base.json
@@ -604,7 +604,7 @@
"setDevicesYubiKeyChalRespTitle": "Challenge-Response",
"setDevicesYubiKeyChalRespDesc": "It's also possible to use a YubiKey in challenge-response mode, so that a piece of private key used to encrypt files resides on a YubiKey.",
"setDevicesYubiKeyChalRespShow": "Show the option to use a YubiKey when opening files",
- "setDevicesYubiKeyOathWorkaround": "Reconnect the YubiKey if it hangs when loading one-time codes",
+ "setDevicesYubiKeyStuckWorkaround": "Reconnect the YubiKey if it seems to be stuck during loading",
"setAboutTitle": "About",
"setAboutBuilt": "This app is built with these awesome tools",
diff --git a/app/scripts/models/external/yubikey-otp-model.js b/app/scripts/models/external/yubikey-otp-model.js
index 69d59bcd..c8b0ff1b 100644
--- a/app/scripts/models/external/yubikey-otp-model.js
+++ b/app/scripts/models/external/yubikey-otp-model.js
@@ -4,11 +4,7 @@ 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;
+import { YubiKey } from 'comp/app/yubikey';
const logger = new Logger('yubikey');
@@ -23,10 +19,6 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
});
}
- static get ykmanStatus() {
- return ykmanStatus;
- }
-
onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys.length === 0) {
this.emit('ejected');
@@ -34,157 +26,61 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
};
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();
+ YubiKey.list((err, yubiKeys) => {
+ if (err) {
+ return callback(err);
}
+
+ let openSuccess = 0;
+ const openErrors = [];
+ const openNextYubiKey = () => {
+ const yubiKey = yubiKeys.shift();
+ this._addYubiKey(yubiKey, err => {
+ if (YubiKey.aborted) {
+ return callback('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();
+ YubiKey.getOtpCodes(serial, (err, codes) => {
+ if (err) {
+ return callback(err);
}
- });
- }
- _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);
+ for (const code of codes) {
+ this.entries.push(
+ new ExternalOtpEntryModel({
+ id: this.entryId(code.title, code.user),
+ device: this,
+ deviceSubId: serial,
+ icon: 'clock-o',
+ title: code.title,
+ user: code.user,
+ needsTouch: code.needsTouch
+ })
+ );
}
- };
- 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);
- }
+ callback();
});
}
@@ -195,38 +91,15 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
}
cancelOpen() {
- logger.info('Cancel open');
+ YubiKey.abort();
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);
- }
+ YubiKey.getOtp(entry.deviceSubId, `${entry.title}:${entry.user}`, (err, otp) => {
+ callback(err, otp, timeLeft);
});
}
@@ -242,34 +115,10 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
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
+ onUsbDevicesChanged: null
});
export { YubiKeyOtpModel };
diff --git a/app/scripts/views/open-view.js b/app/scripts/views/open-view.js
index 4f977dc6..aea34b20 100644
--- a/app/scripts/views/open-view.js
+++ b/app/scripts/views/open-view.js
@@ -9,7 +9,7 @@ import { SecureInput } from 'comp/browser/secure-input';
import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts';
import { UsbListener } from 'comp/app/usb-listener';
-import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
+import { YubiKey } from 'comp/app/yubikey';
import { Keys } from 'const/keys';
import { Comparators } from 'util/data/comparators';
import { Features } from 'util/features';
@@ -47,7 +47,7 @@ class OpenView extends View {
'keypress .open__pass-input': 'inputKeypress',
'click .open__pass-enter-btn': 'openDb',
'click .open__settings-key-file': 'openKeyFile',
- 'click .open__settings-yubikey': 'selectYubiKey',
+ 'click .open__settings-yubikey': 'selectYubiKeyChalResp',
'click .open__last-item': 'openLast',
'click .open__icon-generate': 'toggleGenerator',
dragover: 'dragover',
@@ -445,10 +445,6 @@ class OpenView extends View {
this.showOpenFileInfo(fileInfo, true);
}
- selectYubiKey() {
- Alerts.notImplemented();
- }
-
removeFile(id) {
this.model.removeFileInfo(id);
this.$el.find('.open__last-item[data-id="' + id + '"]').remove();
@@ -1019,15 +1015,15 @@ class OpenView extends View {
const icon = this.$el.find('.open__icon-yubikey');
icon.toggleClass('flip3d', true);
- YubiKeyOtpModel.checkToolStatus().then(() => {
- if (YubiKeyOtpModel.ykmanStatus !== 'ok') {
+ YubiKey.checkToolStatus().then(status => {
+ if (status !== 'ok') {
icon.toggleClass('flip3d', false);
this.inputEl.removeAttr('disabled');
this.busy = false;
return Events.emit('toggle-settings', 'devices');
}
this.otpDevice = this.model.openOtpDevice(err => {
- if (err && !this.otpDevice.openAborted) {
+ if (err && !YubiKey.aborted) {
Alerts.error({
header: Locale.openError,
body: Locale.openErrorDescription,
@@ -1042,6 +1038,23 @@ class OpenView extends View {
});
}
}
+
+ selectYubiKeyChalResp() {
+ if (this.busy) {
+ return;
+ }
+ this.busy = true;
+ YubiKey.checkToolStatus().then(status => {
+ if (status !== 'ok') {
+ this.busy = false;
+ return Events.emit('toggle-settings', 'devices');
+ }
+
+ this.busy = false;
+
+ Alerts.notImplemented();
+ });
+ }
}
export { OpenView };
diff --git a/app/scripts/views/settings/settings-devices-view.js b/app/scripts/views/settings/settings-devices-view.js
index 02219437..0a745e99 100644
--- a/app/scripts/views/settings/settings-devices-view.js
+++ b/app/scripts/views/settings/settings-devices-view.js
@@ -1,7 +1,7 @@
import { Events } from 'framework/events';
import { View } from 'framework/views/view';
import { AppSettingsModel } from 'models/app-settings-model';
-import { YubiKeyOtpModel } from 'models/external/yubikey-otp-model';
+import { YubiKey } from 'comp/app/yubikey';
import { Links } from 'const/links';
import { UsbListener } from 'comp/app/usb-listener';
import template from 'templates/settings/settings-devices.hbs';
@@ -15,13 +15,13 @@ class SettingsDevicesView extends View {
'change .settings__yubikey-auto-open': 'changeYubiKeyAutoOpen',
'change .settings__yubikey-match-entries': 'changeYubiKeyMatchEntries',
'change .settings__yubikey-chalresp-show': 'changeYubiKeyShowChalResp',
- 'change .settings__yubikey-oath-workaround': 'changeYubiKeyOathWorkaround'
+ 'change .settings__yubikey-stuck-workaround': 'changeYubiKeyStuckWorkaround'
};
constructor(...args) {
super(...args);
- if (!['ok', 'checking'].includes(YubiKeyOtpModel.ykmanStatus)) {
- this.toolCheckPromise = YubiKeyOtpModel.checkToolStatus();
+ if (!['ok', 'checking'].includes(YubiKey.ykmanStatus)) {
+ this.toolCheckPromise = YubiKey.checkToolStatus();
}
}
@@ -33,12 +33,12 @@ class SettingsDevicesView extends View {
super.render({
supported: UsbListener.supported,
enableUsb: UsbListener.supported && AppSettingsModel.enableUsb,
- ykmanStatus: YubiKeyOtpModel.ykmanStatus,
+ ykmanStatus: YubiKey.ykmanStatus,
yubiKeyShowIcon: AppSettingsModel.yubiKeyShowIcon,
yubiKeyAutoOpen: AppSettingsModel.yubiKeyAutoOpen,
yubiKeyMatchEntries: AppSettingsModel.yubiKeyMatchEntries,
yubiKeyShowChalResp: AppSettingsModel.yubiKeyShowChalResp,
- yubiKeyOathWorkaround: AppSettingsModel.yubiKeyOathWorkaround,
+ yubiKeyStuckWorkaround: AppSettingsModel.yubiKeyStuckWorkaround,
yubiKeyManualLink: Links.YubiKeyManual,
ykmanInstallLink: Links.YubiKeyManagerInstall
});
@@ -70,8 +70,8 @@ class SettingsDevicesView extends View {
this.render();
}
- changeYubiKeyOathWorkaround(e) {
- AppSettingsModel.yubiKeyOathWorkaround = e.target.checked;
+ changeYubiKeyStuckWorkaround(e) {
+ AppSettingsModel.yubiKeyStuckWorkaround = e.target.checked;
this.render();
}
}
diff --git a/app/templates/settings/settings-devices.hbs b/app/templates/settings/settings-devices.hbs
index 694c64af..d1a77376 100644
--- a/app/templates/settings/settings-devices.hbs
+++ b/app/templates/settings/settings-devices.hbs
@@ -14,10 +14,15 @@
{{#res 'setDevicesYubiKeyToolsDesc'}}ykman
{{/res}}
{{#res 'setDevicesYubiKeyToolsDesc2'}}{{res 'setDevicesYubiKeyToolsDescLink'}}{{/res}}
-
+
{{#ifeq ykmanStatus 'checking'}}{{#res 'setDevicesYubiKeyToolsStatusChecking'}}ykman
{{/res}}...{{/ifeq}}
{{#ifeq ykmanStatus 'ok'}}{{#res 'setDevicesYubiKeyToolsStatusOk'}}ykman
{{/res}}{{/ifeq}}
{{#ifeq ykmanStatus 'error'}}{{#res 'setDevicesYubiKeyToolsStatusError'}}ykman
{{/res}}{{/ifeq}}
+
+
+
+
{{res 'setDevicesYubiKeyOtpTitle'}}
{{res 'setDevicesYubiKeyOtpDesc'}}
@@ -36,11 +41,6 @@
{{#if yubiKeyAutoOpen}}checked{{/if}} />
-
-
-
-
{{res 'setDevicesYubiKeyChalRespTitle'}}
{{res 'setDevicesYubiKeyChalRespDesc'}}