mirror of https://github.com/keeweb/keeweb.git
returning otp codes
This commit is contained in:
parent
cd8c76b3eb
commit
b7e53185b0
|
@ -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);
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ class OtpDeviceEntryModel extends Model {
|
|||
return this.fields;
|
||||
}
|
||||
|
||||
getAllUrls() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getFieldValue(field) {
|
||||
return this.fields[field];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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%;
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue