returning otp codes

This commit is contained in:
antelle 2021-04-29 12:16:53 +02:00
parent cd8c76b3eb
commit b7e53185b0
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
7 changed files with 253 additions and 65 deletions

View File

@ -15,6 +15,7 @@ import { RuntimeDataModel } from 'models/runtime-data-model';
import { AppSettingsModel } from 'models/app-settings-model';
import { Timeouts } from 'const/timeouts';
import { SelectEntryView } from 'views/select/select-entry-view';
import { SelectEntryFieldView } from 'views/select/select-entry-field-view';
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
const KeeWebAssociationId = 'KeeWeb';
@ -289,6 +290,80 @@ function focusKeeWeb() {
}
}
async function findEntry(request, filterOptions) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.url) {
throw new Error('Empty url');
}
const files = getAvailableFiles(request);
const client = getClient(request);
const filter = new SelectEntryFilter(
{ url: payload.url, title: payload.title },
appModel,
files,
filterOptions
);
filter.subdomains = false;
let entries = filter.getEntries();
filter.subdomains = true;
let entry;
if (entries.length) {
if (entries.length === 1 && client.permissions.askGet === 'multiple') {
entry = entries[0];
}
} else {
entries = filter.getEntries();
if (!entries.length) {
if (AppSettingsModel.extensionFocusIfEmpty) {
filter.useUrl = false;
if (filter.title) {
filter.useTitle = true;
entries = filter.getEntries();
if (!entries.length) {
filter.useTitle = false;
}
}
} else {
throw makeError(Errors.noMatches);
}
}
}
if (!entry) {
const extName = getHumanReadableExtensionName(client);
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
const selectEntryView = new SelectEntryView({ filter, topMessage });
focusKeeWeb();
const inactivityTimer = setTimeout(() => {
selectEntryView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const result = await selectEntryView.showAndGetResult();
clearTimeout(inactivityTimer);
entry = result?.entry;
if (!entry) {
throw makeError(Errors.userRejected);
}
}
client.stats.passwordsRead++;
return entry;
}
const ProtocolHandlers = {
'ping'({ data }) {
return { data };
@ -406,69 +481,7 @@ const ProtocolHandlers = {
},
async 'get-logins'(request) {
const payload = decryptRequest(request);
await checkContentRequestPermissions(request);
if (!payload.url) {
throw new Error('Empty url');
}
const files = getAvailableFiles(request);
const client = getClient(request);
const filter = new SelectEntryFilter({ url: payload.url }, appModel, files);
filter.subdomains = false;
let canReturnFirstEntry = false;
let entries = filter.getEntries();
filter.subdomains = true;
if (!entries.length) {
canReturnFirstEntry = false;
entries = filter.getEntries();
if (!entries.length) {
if (AppSettingsModel.extensionFocusIfEmpty) {
filter.useUrl = false;
} else {
throw makeError(Errors.noMatches);
}
}
}
let entry;
if (
canReturnFirstEntry &&
entries.length === 1 &&
client.permissions.askGet === 'multiple'
) {
entry = entries[0];
} else {
const extName = getHumanReadableExtensionName(client);
const topMessage = Locale.extensionSelectPasswordFor.replace('{}', extName);
const selectEntryView = new SelectEntryView({ filter, topMessage });
focusKeeWeb();
const inactivityTimer = setTimeout(() => {
selectEntryView.emit('result', undefined);
}, Timeouts.KeeWebConnectRequest);
const result = await selectEntryView.showAndGetResult();
clearTimeout(inactivityTimer);
entry = result?.entry;
if (!entry) {
throw makeError(Errors.userRejected);
}
}
client.stats.passwordsRead++;
const entry = await findEntry(request);
return encryptResponse(request, {
success: 'true',
@ -490,6 +503,51 @@ const ProtocolHandlers = {
});
},
async 'get-totp-by-url'(request) {
const entry = await findEntry(request, { otp: true });
entry.initOtpGenerator();
if (!entry.otpGenerator) {
throw makeError(Errors.noMatches);
}
let selectEntryFieldView;
if (entry.needsTouch) {
selectEntryFieldView = new SelectEntryFieldView({
needsTouch: true,
deviceShortName: entry.device.shortName
});
selectEntryFieldView.render();
}
const otpPromise = new Promise((resolve, reject) => {
selectEntryFieldView.on('result', () => reject(makeError(Errors.userRejected)));
entry.otpGenerator.next((err, otp) => {
if (otp) {
resolve(otp);
} else {
reject(err || makeError(Errors.userRejected));
}
});
});
let totp;
try {
totp = await otpPromise;
} finally {
if (selectEntryFieldView) {
selectEntryFieldView.remove();
}
}
return encryptResponse(request, {
success: 'true',
version: getVersion(request),
totp
});
},
async 'get-totp'(request) {
decryptRequest(request);
await checkContentRequestPermissions(request);

View File

@ -806,5 +806,7 @@
"selectEntryEnterHint": "use the highlighted entry",
"selectEntryTypingHint": "Start typing to filter",
"selectEntryContains": "Contains text",
"selectEntrySubdomains": "Subdomains"
"selectEntrySubdomains": "Subdomains",
"selectEntryFieldHeader": "Select field",
"selectEntryFieldTouch": "Press a button on your device to generate a one-time code."
}

View File

@ -35,6 +35,10 @@ class OtpDeviceEntryModel extends Model {
return this.fields;
}
getAllUrls() {
return [];
}
getFieldValue(field) {
return this.fields[field];
}

View File

@ -48,6 +48,15 @@ class EntrySearch {
return false;
}
}
if (filter.otp) {
if (
!this.model.fields.otp &&
!this.model.fields['TOTP Seed'] &&
this.model.backend !== 'otp-device'
) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,81 @@
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { Keys } from 'const/keys';
import { Scrollable } from 'framework/views/scrollable';
import template from 'templates/select/select-entry-field.hbs';
class SelectEntryFieldView extends View {
parent = 'body';
modal = 'select-entry-field';
template = template;
events = {
'click .select-entry-field__item': 'itemClicked',
'click .select-entry-field__cancel-btn': 'cancelClicked'
};
result = null;
constructor(model) {
super(model);
this.initScroll();
this.listenTo(Events, 'main-window-blur', this.mainWindowBlur);
this.setupKeys();
}
setupKeys() {
this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, false, 'select-entry-field');
this.onKey(Keys.DOM_VK_RETURN, this.enterPressed, false, 'select-entry-field');
}
render() {
super.render(this.model);
document.activeElement.blur();
const scrollRoot = this.el.querySelector('.select-entry-field__items');
if (scrollRoot) {
this.createScroll({
root: scrollRoot,
scroller: this.el.querySelector('.scroller'),
bar: this.el.querySelector('.scroller__bar')
});
}
}
cancelAndClose() {
this.result = null;
this.emit('result', this.result);
}
escPressed() {
this.cancelAndClose();
}
enterPressed() {
this.closeWithResult();
}
mainWindowBlur() {
this.emit('result', undefined);
}
showAndGetResult() {
this.render();
return new Promise((resolve) => {
this.once('result', (result) => {
this.remove();
resolve(result);
});
});
}
cancelClicked() {
this.cancelAndClose();
}
}
Object.assign(SelectEntryFieldView.prototype, Scrollable);
export { SelectEntryFieldView };

View File

@ -1,4 +1,5 @@
.select-entry {
.select-entry,
.select-entry-field {
@include position(absolute, 0 null null 0);
@include size(100%);
background-color: var(--background-color);
@ -64,6 +65,16 @@
justify-content: center;
}
}
&__large-text {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
&__large-icon {
font-size: $modal-icon-size;
}
&__table {
flex: 1;
width: 100%;

View File

@ -0,0 +1,23 @@
<div class="select-entry-field">
{{#if needsTouch}}
<div class="select-entry__header">
<h1 class="select-entry__header-text">{{#res 'detOtpTouch'}}{{deviceShortName}}{{/res}}</h1>
</div>
<div class="select-entry-field__large-text">
<i class="fa fa-usb-token select-entry-field__large-icon"></i>
<p>{{res 'selectEntryFieldTouch'}}</p>
</div>
{{else}}
<div class="select-entry__header">
<h1 class="select-entry__header-text">{{res 'selectEntryFieldHeader'}}</h1>
</div>
<div class="select-entry-field__items">
<div class="scroller">
</div>
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
</div>
{{/if}}
<div class="select-entry-field__buttons">
<button class="btn btn-error select-entry-field__cancel-btn">{{res 'alertCancel'}} ({{res 'keyEsc'}})</button>
</div>
</div>