1
0
mirror of https://github.com/keeweb/keeweb.git synced 2024-06-26 07:39:04 +02:00

extracted YubiKey module

This commit is contained in:
antelle 2020-05-24 18:39:56 +02:00
parent bf7dec2437
commit 81d4c5c403
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
7 changed files with 293 additions and 225 deletions

View File

@ -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 };

View File

@ -41,7 +41,7 @@ const DefaultAppSettings = {
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
yubiKeyMatchEntries: true, // show matching one-time codes in entries yubiKeyMatchEntries: true, // show matching one-time codes in entries
yubiKeyShowChalResp: true, // show YubiKey challenge-response option 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 canOpen: true, // can select and open new files
canOpenDemo: true, // can open a demo file canOpenDemo: true, // can open a demo file

View File

@ -604,7 +604,7 @@
"setDevicesYubiKeyChalRespTitle": "Challenge-Response", "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.", "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", "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", "setAboutTitle": "About",
"setAboutBuilt": "This app is built with these awesome tools", "setAboutBuilt": "This app is built with these awesome tools",

View File

@ -4,11 +4,7 @@ import { ExternalOtpEntryModel } from 'models/external/external-otp-entry-model'
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { UsbListener } from 'comp/app/usb-listener'; import { UsbListener } from 'comp/app/usb-listener';
import { AppSettingsModel } from 'models/app-settings-model'; import { YubiKey } from 'comp/app/yubikey';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
let ykmanStatus;
const logger = new Logger('yubikey'); const logger = new Logger('yubikey');
@ -23,10 +19,6 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
}); });
} }
static get ykmanStatus() {
return ykmanStatus;
}
onUsbDevicesChanged = () => { onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys.length === 0) { if (UsbListener.attachedYubiKeys.length === 0) {
this.emit('ejected'); this.emit('ejected');
@ -34,157 +26,61 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
}; };
open(callback) { open(callback) {
this._open(callback, true); YubiKey.list((err, yubiKeys) => {
} if (err) {
return callback(err);
_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();
} }
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) { _addYubiKey(serial, callback) {
logger.info('Adding YubiKey', serial); logger.info('Adding YubiKey', serial);
this.openProcess = Launcher.spawn({ YubiKey.getOtpCodes(serial, (err, codes) => {
cmd: 'ykman', if (err) {
args: ['-d', serial, 'oath', 'code'], return callback(err);
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) { for (const code of codes) {
logger.info('Repairing a stuck YubiKey'); this.entries.push(
new ExternalOtpEntryModel({
let openTimeout; id: this.entryId(code.title, code.user),
const countYubiKeys = UsbListener.attachedYubiKeys.length; device: this,
const onDevicesChangedDuringRepair = () => { deviceSubId: serial,
if (UsbListener.attachedYubiKeys.length === countYubiKeys) { icon: 'clock-o',
logger.info('YubiKey was reconnected'); title: code.title,
Events.off('usb-devices-changed', onDevicesChangedDuringRepair); user: code.user,
clearTimeout(openTimeout); needsTouch: code.needsTouch
this.openAborted = false; })
setTimeout(() => { );
this._open(callback, false);
}, Timeouts.ExternalDeviceAfterReconnect);
} }
};
Events.on('usb-devices-changed', onDevicesChangedDuringRepair);
Launcher.spawn({ callback();
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);
}
}); });
} }
@ -195,38 +91,15 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
} }
cancelOpen() { cancelOpen() {
logger.info('Cancel open'); YubiKey.abort();
Events.off('usb-devices-changed', this.onUsbDevicesChanged); 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) { getOtp(entry, callback) {
const msPeriod = 30000; const msPeriod = 30000;
const timeLeft = msPeriod - (Date.now() % msPeriod) + 500; const timeLeft = msPeriod - (Date.now() % msPeriod) + 500;
return Launcher.spawn({ YubiKey.getOtp(entry.deviceSubId, `${entry.title}:${entry.user}`, (err, otp) => {
cmd: 'ykman', callback(err, otp, timeLeft);
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);
}
}); });
} }
@ -242,34 +115,10 @@ class YubiKeyOtpModel extends ExternalOtpDeviceModel {
active: false 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({ YubiKeyOtpModel.defineModelProperties({
onUsbDevicesChanged: null, onUsbDevicesChanged: null
openProcess: null,
openAborted: false
}); });
export { YubiKeyOtpModel }; export { YubiKeyOtpModel };

View File

@ -9,7 +9,7 @@ import { SecureInput } from 'comp/browser/secure-input';
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { Alerts } from 'comp/ui/alerts'; import { Alerts } from 'comp/ui/alerts';
import { UsbListener } from 'comp/app/usb-listener'; 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 { Keys } from 'const/keys';
import { Comparators } from 'util/data/comparators'; import { Comparators } from 'util/data/comparators';
import { Features } from 'util/features'; import { Features } from 'util/features';
@ -47,7 +47,7 @@ class OpenView extends View {
'keypress .open__pass-input': 'inputKeypress', 'keypress .open__pass-input': 'inputKeypress',
'click .open__pass-enter-btn': 'openDb', 'click .open__pass-enter-btn': 'openDb',
'click .open__settings-key-file': 'openKeyFile', 'click .open__settings-key-file': 'openKeyFile',
'click .open__settings-yubikey': 'selectYubiKey', 'click .open__settings-yubikey': 'selectYubiKeyChalResp',
'click .open__last-item': 'openLast', 'click .open__last-item': 'openLast',
'click .open__icon-generate': 'toggleGenerator', 'click .open__icon-generate': 'toggleGenerator',
dragover: 'dragover', dragover: 'dragover',
@ -445,10 +445,6 @@ class OpenView extends View {
this.showOpenFileInfo(fileInfo, true); this.showOpenFileInfo(fileInfo, true);
} }
selectYubiKey() {
Alerts.notImplemented();
}
removeFile(id) { removeFile(id) {
this.model.removeFileInfo(id); this.model.removeFileInfo(id);
this.$el.find('.open__last-item[data-id="' + id + '"]').remove(); 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'); const icon = this.$el.find('.open__icon-yubikey');
icon.toggleClass('flip3d', true); icon.toggleClass('flip3d', true);
YubiKeyOtpModel.checkToolStatus().then(() => { YubiKey.checkToolStatus().then(status => {
if (YubiKeyOtpModel.ykmanStatus !== 'ok') { if (status !== 'ok') {
icon.toggleClass('flip3d', false); icon.toggleClass('flip3d', false);
this.inputEl.removeAttr('disabled'); this.inputEl.removeAttr('disabled');
this.busy = false; this.busy = false;
return Events.emit('toggle-settings', 'devices'); return Events.emit('toggle-settings', 'devices');
} }
this.otpDevice = this.model.openOtpDevice(err => { this.otpDevice = this.model.openOtpDevice(err => {
if (err && !this.otpDevice.openAborted) { if (err && !YubiKey.aborted) {
Alerts.error({ Alerts.error({
header: Locale.openError, header: Locale.openError,
body: Locale.openErrorDescription, 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 }; export { OpenView };

View File

@ -1,7 +1,7 @@
import { Events } from 'framework/events'; import { Events } from 'framework/events';
import { View } from 'framework/views/view'; import { View } from 'framework/views/view';
import { AppSettingsModel } from 'models/app-settings-model'; 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 { Links } from 'const/links';
import { UsbListener } from 'comp/app/usb-listener'; import { UsbListener } from 'comp/app/usb-listener';
import template from 'templates/settings/settings-devices.hbs'; 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-auto-open': 'changeYubiKeyAutoOpen',
'change .settings__yubikey-match-entries': 'changeYubiKeyMatchEntries', 'change .settings__yubikey-match-entries': 'changeYubiKeyMatchEntries',
'change .settings__yubikey-chalresp-show': 'changeYubiKeyShowChalResp', 'change .settings__yubikey-chalresp-show': 'changeYubiKeyShowChalResp',
'change .settings__yubikey-oath-workaround': 'changeYubiKeyOathWorkaround' 'change .settings__yubikey-stuck-workaround': 'changeYubiKeyStuckWorkaround'
}; };
constructor(...args) { constructor(...args) {
super(...args); super(...args);
if (!['ok', 'checking'].includes(YubiKeyOtpModel.ykmanStatus)) { if (!['ok', 'checking'].includes(YubiKey.ykmanStatus)) {
this.toolCheckPromise = YubiKeyOtpModel.checkToolStatus(); this.toolCheckPromise = YubiKey.checkToolStatus();
} }
} }
@ -33,12 +33,12 @@ class SettingsDevicesView extends View {
super.render({ super.render({
supported: UsbListener.supported, supported: UsbListener.supported,
enableUsb: UsbListener.supported && AppSettingsModel.enableUsb, enableUsb: UsbListener.supported && AppSettingsModel.enableUsb,
ykmanStatus: YubiKeyOtpModel.ykmanStatus, ykmanStatus: YubiKey.ykmanStatus,
yubiKeyShowIcon: AppSettingsModel.yubiKeyShowIcon, yubiKeyShowIcon: AppSettingsModel.yubiKeyShowIcon,
yubiKeyAutoOpen: AppSettingsModel.yubiKeyAutoOpen, yubiKeyAutoOpen: AppSettingsModel.yubiKeyAutoOpen,
yubiKeyMatchEntries: AppSettingsModel.yubiKeyMatchEntries, yubiKeyMatchEntries: AppSettingsModel.yubiKeyMatchEntries,
yubiKeyShowChalResp: AppSettingsModel.yubiKeyShowChalResp, yubiKeyShowChalResp: AppSettingsModel.yubiKeyShowChalResp,
yubiKeyOathWorkaround: AppSettingsModel.yubiKeyOathWorkaround, yubiKeyStuckWorkaround: AppSettingsModel.yubiKeyStuckWorkaround,
yubiKeyManualLink: Links.YubiKeyManual, yubiKeyManualLink: Links.YubiKeyManual,
ykmanInstallLink: Links.YubiKeyManagerInstall ykmanInstallLink: Links.YubiKeyManagerInstall
}); });
@ -70,8 +70,8 @@ class SettingsDevicesView extends View {
this.render(); this.render();
} }
changeYubiKeyOathWorkaround(e) { changeYubiKeyStuckWorkaround(e) {
AppSettingsModel.yubiKeyOathWorkaround = e.target.checked; AppSettingsModel.yubiKeyStuckWorkaround = e.target.checked;
this.render(); this.render();
} }
} }

View File

@ -14,10 +14,15 @@
{{#res 'setDevicesYubiKeyToolsDesc'}}<code>ykman</code>{{/res}} {{#res 'setDevicesYubiKeyToolsDesc'}}<code>ykman</code>{{/res}}
{{#res 'setDevicesYubiKeyToolsDesc2'}}<a href="{{ykmanInstallLink}}" target="_blank">{{res 'setDevicesYubiKeyToolsDescLink'}}</a>{{/res}} {{#res 'setDevicesYubiKeyToolsDesc2'}}<a href="{{ykmanInstallLink}}" target="_blank">{{res 'setDevicesYubiKeyToolsDescLink'}}</a>{{/res}}
</p> </p>
<div> <p>
{{#ifeq ykmanStatus 'checking'}}{{#res 'setDevicesYubiKeyToolsStatusChecking'}}<code>ykman</code>{{/res}}...{{/ifeq}} {{#ifeq ykmanStatus 'checking'}}{{#res 'setDevicesYubiKeyToolsStatusChecking'}}<code>ykman</code>{{/res}}...{{/ifeq}}
{{#ifeq ykmanStatus 'ok'}}{{#res 'setDevicesYubiKeyToolsStatusOk'}}<code>ykman</code>{{/res}}{{/ifeq}} {{#ifeq ykmanStatus 'ok'}}{{#res 'setDevicesYubiKeyToolsStatusOk'}}<code>ykman</code>{{/res}}{{/ifeq}}
{{#ifeq ykmanStatus 'error'}}{{#res 'setDevicesYubiKeyToolsStatusError'}}<code>ykman</code>{{/res}}{{/ifeq}} {{#ifeq ykmanStatus 'error'}}{{#res 'setDevicesYubiKeyToolsStatusError'}}<code>ykman</code>{{/res}}{{/ifeq}}
</p>
<div>
<input type="checkbox" class="settings__input input-base settings__yubikey-stuck-workaround" id="settings__yubikey-stuck-workaround"
{{#if yubiKeyStuckWorkaround}}checked{{/if}} />
<label for="settings__yubikey-stuck-workaround">{{res 'setDevicesYubiKeyStuckWorkaround'}}</label>
</div> </div>
<h3>{{res 'setDevicesYubiKeyOtpTitle'}}</h3> <h3>{{res 'setDevicesYubiKeyOtpTitle'}}</h3>
<p>{{res 'setDevicesYubiKeyOtpDesc'}}</p> <p>{{res 'setDevicesYubiKeyOtpDesc'}}</p>
@ -36,11 +41,6 @@
{{#if yubiKeyAutoOpen}}checked{{/if}} /> {{#if yubiKeyAutoOpen}}checked{{/if}} />
<label for="settings__yubikey-auto-open">{{res 'setDevicesYubiKeyOtpAutoOpen'}}</label> <label for="settings__yubikey-auto-open">{{res 'setDevicesYubiKeyOtpAutoOpen'}}</label>
</div> </div>
<div>
<input type="checkbox" class="settings__input input-base settings__yubikey-oath-workaround" id="settings__yubikey-oath-workaround"
{{#if yubiKeyOathWorkaround}}checked{{/if}} />
<label for="settings__yubikey-oath-workaround">{{res 'setDevicesYubiKeyOathWorkaround'}}</label>
</div>
<h3>{{res 'setDevicesYubiKeyChalRespTitle'}}</h3> <h3>{{res 'setDevicesYubiKeyChalRespTitle'}}</h3>
<p>{{res 'setDevicesYubiKeyChalRespDesc'}}</p> <p>{{res 'setDevicesYubiKeyChalRespDesc'}}</p>
<div> <div>