yubikey ui alerts

This commit is contained in:
antelle 2020-05-30 16:02:02 +02:00
parent f5bb29e61a
commit e6826b176e
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
9 changed files with 156 additions and 27 deletions

View File

@ -261,7 +261,7 @@ const AutoType = {
resetPendingEvent() {
if (this.pendingEvent) {
this.pendingEvent = null;
logger.debug('auto-type event cancelled');
logger.debug('auto-type event canceled');
}
},

View File

@ -1,5 +1,10 @@
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { YubiKey } from 'comp/app/yubikey';
import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { Timeouts } from 'const/timeouts';
import { UsbListener } from './usb-listener';
const logger = new Logger('chal-resp');
@ -16,8 +21,6 @@ const ChalRespCalculator = {
}
return challenge => {
return new Promise((resolve, reject) => {
const ts = logger.ts();
challenge = Buffer.from(challenge);
const hexChallenge = challenge.toString('hex');
@ -30,33 +33,131 @@ const ChalRespCalculator = {
logger.debug('Calculating ChalResp using a YubiKey', params);
YubiKey.calculateChalResp(params, challenge, (err, response) => {
this._calc(params, challenge, (err, response) => {
if (err) {
if (err.noKey) {
logger.debug('No YubiKey');
// TODO
return;
}
if (err.touchRequested) {
logger.debug('YubiKey touch requested');
// TODO
return;
}
return reject(err);
}
if (!this.cache[cacheKey]) {
this.cache[cacheKey] = {};
}
this.cache[cacheKey][hexChallenge] = response.toString('hex');
logger.info('Calculated ChalResp', logger.ts(ts));
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;
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) {
let noKeyAlert = null;
let deviceEnumerationTimer;
const onUsbDevicesChanged = () => {
if (UsbListener.attachedYubiKeys === 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) {
return Alerts.alert({
header: Locale.yubiTouchRequestedHeader,
body: Locale.yubiTouchRequestedBody.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)];
}

View File

@ -253,10 +253,10 @@ const YubiKey = {
this.ykChalResp.challengeResponse(yubiKey, challenge, slot, (err, response) => {
if (err) {
if (err.code === this.ykChalResp.YK_ENOKEY) {
err.ykNoKey = true;
err.noKey = true;
}
if (err.code === this.ykChalResp.YK_ETIMEOUT) {
err.ykTimeout = true;
err.timeout = true;
}
return callback(err);
}

View File

@ -39,7 +39,6 @@ const Alerts = {
const view = new ModalView(config);
view.render();
view.on('result', (res, check) => {
Alerts.alertDisplayed = false;
if (res && config.success) {
config.success(res, check);
}
@ -50,6 +49,9 @@ const Alerts = {
config.complete(res, check);
}
});
view.on('will-close', () => {
Alerts.alertDisplayed = false;
});
return view;
},

View File

@ -671,5 +671,9 @@
"importTo": "Entries will be imported to",
"importNewFile": "New file",
"yubiKeyStuckError": "The YubiKey seems to be stuck, auto-repair can be enabled in app settings."
"yubiKeyStuckError": "The YubiKey seems to be stuck, auto-repair can be enabled in app settings.",
"yubiKeyNoKeyHeader": "YubiKey required",
"yubiKeyNoKeyBody": "Please insert your YubiKey with serial number {}",
"yubiTouchRequestedHeader": "Touch your YubiKey",
"yubiTouchRequestedBody": "Please touch your YubiKey with serial number {}"
}

View File

@ -75,13 +75,24 @@ class ModalView extends View {
const checked = this.model.checkbox
? this.$el.find('#modal__check').is(':checked')
: undefined;
this.emit('will-close');
this.emit('result', result, checked);
this.removeView();
}
closeWithoutResult() {
this.emit('will-close');
this.removeView();
}
removeView() {
this.$el.addClass('modal--hidden');
this.unbindEvents();
setTimeout(() => this.remove(), 100);
}
closeImmediate() {
this.emit('will-close');
this.emit('result', undefined);
this.unbindEvents();
this.remove();

View File

@ -685,6 +685,8 @@ class OpenView extends View {
this.inputEl[0].selectionEnd = this.inputEl.val().length;
if (err.code === 'InvalidKey') {
InputFx.shake(this.inputEl);
} else if (err.userCanceled) {
// nothing to do
} else {
if (err.notFound) {
err = Locale.openErrorFileNotFound;
@ -1074,7 +1076,7 @@ class OpenView extends View {
Alerts.alert({
header: Locale.openChalRespHeader,
icon: 'exchange',
iconSvg: 'usb-token',
buttons: [{ result: '', title: Locale.alertCancel }],
esc: '',
click: '',

View File

@ -30,6 +30,11 @@
&__icon {
font-size: $modal-icon-size;
text-align: center;
&--svg {
fill: var(--text-color);
width: 1.4em;
align-self: center;
}
}
&__header {
user-select: text;

View File

@ -1,6 +1,10 @@
<div class="modal modal--hidden {{#if opaque}}modal--opaque{{/if}}">
<div class="modal__content">
<i class="modal__icon fa fa-{{icon}}"></i>
{{#if icon}}
<i class="modal__icon fa fa-{{icon}}"></i>
{{else if iconSvg}}
{{{svg iconSvg 'modal__icon modal__icon--svg'}}}
{{/if}}
<div class="modal__header">{{header}}</div>
<div class="modal__body">
{{#each body as |item|}}