extension connection dialog

This commit is contained in:
antelle 2021-04-25 13:46:59 +02:00
parent b2e828e7ef
commit 15936fcef0
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
13 changed files with 192 additions and 32 deletions

View File

@ -7,7 +7,7 @@ import { Features } from 'util/features';
const WebConnectionInfo = {
connectionId: 1,
extensionName: 'keeweb-connect',
extensionName: 'KeeWeb Connect',
supportsNotifications: true
};

View File

@ -8,6 +8,8 @@ import { Alerts } from 'comp/ui/alerts';
import { Locale } from 'util/locale';
import { RuntimeInfo } from 'const/runtime-info';
import { KnownAppVersions } from 'const/known-app-versions';
import { ExtensionConnectView } from 'views/extension/extension-connect-view';
import { RuntimeDataModel } from 'models/runtime-data-model';
const KeeWebAssociationId = 'KeeWeb';
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
@ -136,7 +138,7 @@ function checkContentRequestPermissions(request) {
ensureAtLeastOneFileIsOpen();
const client = getClient(request);
if (client.authorized) {
if (client.permissions) {
return;
}
@ -147,31 +149,41 @@ function checkContentRequestPermissions(request) {
focusKeeWeb();
// TODO: make a proper dialog here instead of a simple question
if (Launcher) {
Alerts.yesno({
header: 'Extension connection',
body: 'Allow this extension to connect?',
success: () => {
resolve();
},
cancel: () => reject(makeError(Errors.userRejected))
});
} else {
// it's 'confirm' here because other browser extensions can't interact with browser alerts
// while they can easily press a button on our alert
// eslint-disable-next-line no-alert
const allowed = confirm('Allow this extension to connect?');
if (allowed) {
resolve();
} else {
reject(makeError(Errors.userRejected));
const config = RuntimeDataModel.extensionConnectConfig;
const files = appModel.files.map((f) => ({
id: f.id,
name: f.name,
checked: !config || config.allFiles || config.files.includes(f.id)
}));
if (!files.some((f) => f.checked)) {
for (const f of files) {
f.checked = true;
}
}
const extensionConnectView = new ExtensionConnectView({
extensionName: `${client.connection.extensionName} (${client.connection.appName})`,
identityVerified: !Launcher,
files,
allFiles: config?.allFiles ?? true,
askGet: config?.askGet || 'multiple'
});
Alerts.alert({
header: Locale.extensionConnectHeader,
icon: 'exchange-alt',
view: extensionConnectView,
wide: true,
opaque: true,
buttons: [Alerts.buttons.allow, Alerts.buttons.deny],
success: () => {
RuntimeDataModel.extensionConnectConfig = extensionConnectView.config;
client.permissions = extensionConnectView.config;
resolve();
},
cancel: () => reject(makeError(Errors.userRejected))
});
})
.then(() => {
client.authorized = true;
Launcher.hideApp();
})
.catch((e) => {
@ -186,7 +198,7 @@ function getVersion(request) {
}
function isKeeWebConnect(request) {
return getClient(request).connection.extensionName === 'keeweb-connect';
return getClient(request).connection.extensionName === 'KeeWeb Connect';
}
function focusKeeWeb() {

View File

@ -17,6 +17,12 @@ const Alerts = {
return Locale.alertYes;
}
},
allow: {
result: 'yes',
get title() {
return Locale.alertAllow;
}
},
no: {
result: '',
get title() {
@ -28,6 +34,12 @@ const Alerts = {
get title() {
return Locale.alertCancel;
}
},
deny: {
result: '',
get title() {
return Locale.alertDeny;
}
}
},

View File

@ -86,6 +86,8 @@
"alertCopy": "Copy",
"alertClose": "Close",
"alertDoNotAsk": "Don't ask me anymore",
"alertAllow": "Allow",
"alertDeny": "Deny",
"appBeta": "WARNING: beta version, only for preview",
@ -755,5 +757,14 @@
"extensionErrorNoOpenFiles": "No open files",
"extensionErrorUserRejected": "The request was denied",
"extensionErrorAlertDisplayed": "Cannot ask user a question now, please try again"
"extensionErrorAlertDisplayed": "Cannot ask user a question now, please try again",
"extensionConnectHeader": "Extension data exchange",
"extensionConnectIntro": "A browser extension that identifies itself as {} tries to exchange data with KeeWeb.",
"extensionConnectUnknownActivity": "KeeWeb doesn't verify that the connected application is what it pretends to be. Approve the request only if you recognize this activity.",
"extensionConnectFiles": "In this session, allow access to files:",
"extensionConnectAllFiles": "All other files",
"extensionConnectAskGet": "Ask before returning passwords to the extension:",
"extensionConnectAskGetMultiple": "if there's more than one match",
"extensionConnectAskGetAlways": "always",
"extensionConnectSettingsAreForSession": "Settings you select here are valid only for the active session. You can view and manage sessions in KeeWeb settings."
}

View File

@ -0,0 +1,62 @@
import { View } from 'framework/views/view';
import template from 'templates/extension/extension-connect.hbs';
class ExtensionConnectView extends View {
template = template;
events = {
'change #extension-connect__ask-get': 'askGetChanged',
'change .extension-connect__file-check': 'fileChecked'
};
constructor(model) {
super(model);
this.config = {
askGet: this.model.askGet,
allFiles: this.model.allFiles,
files: this.model.files.filter((f) => f.checked).map((f) => f.id)
};
}
render() {
super.render({
...this.model,
...this.config,
files: this.model.files.map((f) => ({
id: f.id,
name: f.name,
checked: this.config.files.includes(f.id)
}))
});
}
fileChecked(e) {
const fileId = e.target.dataset.file;
const checked = e.target.checked;
if (fileId === 'all') {
this.config.allFiles = checked;
this.config.files = this.model.files.map((f) => f.id);
} else {
if (checked) {
this.config.files.push(fileId);
} else {
this.config.files = this.config.files.filter((f) => f !== fileId);
this.config.allFiles = false;
}
}
this.render();
const atLeastOneFileSelected = this.config.files.length > 0 || this.config.allFiles;
const allowButton = document.querySelector('.modal button[data-result=yes]');
allowButton.classList.toggle('hide', !atLeastOneFileSelected);
}
askGetChanged(e) {
this.config.askGet = e.target.value;
}
}
export { ExtensionConnectView };

View File

@ -0,0 +1,10 @@
.extension-connect {
&__files {
display: flex;
flex-wrap: wrap;
}
&__file {
margin: $base-padding;
}
}

View File

@ -284,3 +284,7 @@ input[type='range'] {
.input-base {
@extend .input-size-base;
}
select.input-base {
height: 2em;
}

View File

@ -202,3 +202,4 @@ $fa-var-titlebar-minimize: next-fa-glyph();
$fa-var-titlebar-restore: next-fa-glyph();
$fa-var-window-maximize: next-fa-glyph();
$fa-var-download: next-fa-glyph();
$fa-var-exchange-alt: next-fa-glyph();

View File

@ -51,6 +51,9 @@
&__buttons {
align-self: center;
width: 40%;
.modal--wide & {
width: 80%;
}
@include tablet {
width: 90%;
}

View File

@ -29,6 +29,7 @@ $fa-font-path: '~font-awesome/fonts';
@import 'areas/app';
@import 'areas/auto-type';
@import 'areas/details';
@import 'areas/extension';
@import 'areas/footer';
@import 'areas/grp';
@import 'areas/tag';

View File

@ -0,0 +1,39 @@
<div class="extension-connect">
<p>{{#res 'extensionConnectIntro'}}{{extensionName}}{{/res}}</p>
{{#unless identityVerified}}
<p class="muted-color">{{res 'extensionConnectUnknownActivity'}}</p>
{{/unless}}
<div><label>{{res 'extensionConnectFiles'}}</label></div>
<div class="extension-connect__files">
{{#each files as |file|}}
<div class="extension-connect__file">
<input type="checkbox" class="input-base extension-connect__file-check"
data-file="{{file.id}}"
id="extension-connect__file-check--name-{{file.id}}"
{{#if file.checked}}checked{{/if}}
/>
<label for="extension-connect__file-check--name-{{file.id}}">{{file.name}}</label>
</div>
{{/each}}
<div class="extension-connect__file">
<input type="checkbox" class="input-base extension-connect__file-check"
data-file="all"
id="extension-connect__file-check--all"
{{#if allFiles}}checked{{/if}}
/>
<label for="extension-connect__file-check--all">{{res 'extensionConnectAllFiles'}}</label>
</div>
</div>
<div>
<div><label for="extension-connect__ask-get">{{res 'extensionConnectAskGet'}}</label></div>
<select id="extension-connect__ask-get" class="input-base">
<option value="multiple" {{#ifeq askGet 'multiple'}}selected{{/ifeq}}>
{{res 'extensionConnectAskGetMultiple'}}
</option>
<option value="always" {{#ifeq askGet 'always'}}selected{{/ifeq}}>
{{res 'extensionConnectAskGetAlways'}}
</option>
</select>
</div>
<p>{{res 'extensionConnectSettingsAreForSession'}}</p>
</div>

View File

@ -1,4 +1,4 @@
<div class="modal modal--hidden {{#if opaque}}modal--opaque{{/if}}">
<div class="modal modal--hidden {{#if opaque}}modal--opaque{{/if}} {{#if wide}}modal--wide{{/if}}">
<div class="modal__content">
{{#if icon}}
<i class="modal__icon fa fa-{{icon}}"></i>

View File

@ -15,14 +15,19 @@ ipcMain.handle('browserExtensionConnectorSocketEvent', browserExtensionConnector
const logger = new Logger('browser-extension-connector');
const BrowserExtensionNames = {
KWC: 'KeeWeb Connect',
KPXC: 'KeePassXC-Browser'
};
const MaxIncomingDataLength = 10_000;
const ExtensionOrigins = {
'safari-keeweb-connect': 'keeweb-connect',
'keeweb-connect@keeweb.info': 'keeweb-connect',
'chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/': 'keeweb-connect',
'keepassxc-browser@keepassxc.org': 'keepassxc-browser',
'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/': 'keepassxc-browser',
'chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/': 'keepassxc-browser'
'safari-keeweb-connect': BrowserExtensionNames.KWC,
'keeweb-connect@keeweb.info': BrowserExtensionNames.KWC,
'chrome-extension://aphablpbogbpmocgkpeeadeljldnphon/': BrowserExtensionNames.KWC,
'keepassxc-browser@keepassxc.org': BrowserExtensionNames.KPXC,
'chrome-extension://oboonakemofpalcgghocfoadofidjkkk/': BrowserExtensionNames.KPXC,
'chrome-extension://pdffhmdngciaglkoonimfcmckehcpafo/': BrowserExtensionNames.KPXC
};
const AppNames = {