mirror of https://github.com/keeweb/keeweb
yubikey ui alerts
parent
f5bb29e61a
commit
e6826b176e
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
@ -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 {}"
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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|}}
|
||||
|
|
Loading…
Reference in New Issue