Merge branch 'develop' into usb

# Conflicts:
#	app/scripts/const/default-app-settings.js
#	app/scripts/views/open-view.js
This commit is contained in:
antelle 2020-04-23 20:26:52 +02:00
commit dd68aa342a
No known key found for this signature in database
GPG Key ID: 094A2F2D6136A4EE
34 changed files with 164 additions and 91 deletions

View File

@ -63,7 +63,8 @@ ready(() => {
.catch(e => {
Alerts.error({
header: Locale.appSettingsError,
body: Locale.appNotSupportedError + '<br/><br/>' + e,
body: Locale.appNotSupportedError,
pre: e,
buttons: [],
esc: false,
enter: false,
@ -147,7 +148,7 @@ ready(() => {
esc: false,
enter: false,
click: false,
body: Locale.appSecWarnBody1 + '<br/><br/>' + Locale.appSecWarnBody2,
body: Locale.appSecWarnBody1 + '\n\n' + Locale.appSecWarnBody2,
buttons: [{ result: '', title: Locale.appSecWarnBtn, error: true }],
complete: () => {
showView();

View File

@ -38,10 +38,10 @@ const AppRightsChecker = {
icon: 'lock',
header: Locale.appRightsAlert,
body:
Locale.appRightsAlertBody1.replace('{}', `<code>${this.AppPath}</code>`) +
'<br/>' +
Locale.appRightsAlertBody2 +
`: <pre>${command}</pre>`,
Locale.appRightsAlertBody1.replace('{}', this.AppPath) +
'\n' +
Locale.appRightsAlertBody2,
pre: command,
buttons: [
{ result: 'skip', title: Locale.alertDoNotAsk, error: true },
Alerts.buttons.ok

View File

@ -6,7 +6,6 @@ import { Otp } from 'util/data/otp';
import { Features } from 'util/features';
import { Locale } from 'util/locale';
import { Logger } from 'util/logger';
import { escape } from 'util/fn';
const logger = new Logger('otp-qr-reader');
@ -53,7 +52,7 @@ class OtpQrReader {
Locale.detSetupOtpAlertBody2.replace('{}', screenshotKey || ''),
line3,
Locale.detSetupOtpAlertBody4
].join('<br/>'),
].join('\n'),
esc: '',
click: '',
enter: '',
@ -141,11 +140,8 @@ class OtpQrReader {
logger.error('Error parsing QR code', err);
Alerts.error({
header: Locale.detOtpQrWrong,
body:
Locale.detOtpQrWrongBody +
'<pre class="modal__pre">' +
escape(err.toString()) +
'</pre>'
body: Locale.detOtpQrWrongBody,
pre: err.toString()
});
}
} catch (e) {

View File

@ -14,6 +14,9 @@ const Launcher = {
thirdPartyStoragesSupported: true,
clipboardSupported: true,
req: window.require,
reqNative(module) {
return this.req(`@keeweb/keeweb-native-modules/${module}.${process.platform}.node`);
},
platform() {
return process.platform;
},

View File

@ -35,6 +35,7 @@ const DefaultAppSettings = {
allowIframes: false, // allow displaying the app in IFrames
useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices
nativeArgon2: true, // use native argon2 module
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
yubiKeyAutoOpen: true, // auto-load one-time codes when there are open files

View File

@ -522,6 +522,8 @@
"setFileCloseNoSave": "Close and lose changes",
"setFileDontClose": "Don't close",
"setFileFormatVersion": "File format",
"saveFileExportRaw": "Exporting your passwords",
"saveFileExportRawBody": "The exported file will contain your passwords, they will not be encrypted there. Would you like to proceed?",
"setShTitle": "Shortcuts",
"setShShowAll": "show all items",

View File

@ -409,7 +409,9 @@ class AppModel {
newEntry.copyFromTemplate(templateEntry);
return newEntry;
} else {
return EntryModel.newEntry(sel.group, sel.file);
return EntryModel.newEntry(sel.group, sel.file, {
tag: this.filter.tag
});
}
}

View File

@ -792,12 +792,15 @@ class EntryModel extends Model {
return model;
}
static newEntry(group, file) {
static newEntry(group, file, opts) {
const model = new EntryModel();
const entry = file.db.createEntry(group.group);
if (AppSettingsModel.useGroupIconForEntries && group.icon && group.iconId) {
entry.icon = group.iconId;
}
if (opts && opts.tag) {
entry.tags = [opts.tag];
}
model.setEntry(entry, group, file);
model.entry.times.update();
model.unsaved = true;

View File

@ -13,7 +13,14 @@ class StorageWebDav extends StorageBase {
getOpenConfig() {
return {
fields: [
{ id: 'path', title: 'openUrl', desc: 'openUrlDesc', type: 'text', required: true },
{
id: 'path',
title: 'openUrl',
desc: 'openUrlDesc',
type: 'text',
required: true,
pattern: '^https://.+'
},
{
id: 'user',
title: 'openUser',

View File

@ -1,6 +1,8 @@
import kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import { Features } from 'util/features';
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
const logger = new Logger('argon2');
@ -27,6 +29,40 @@ const KdbxwebInit = {
if (!global.WebAssembly) {
return Promise.reject('WebAssembly is not supported');
}
if (Launcher && Launcher.reqNative && AppSettingsModel.nativeArgon2) {
const ts = logger.ts();
const argon2 = Launcher.reqNative('argon2');
logger.debug('Native argon2 runtime loaded (main thread)', logger.ts(ts));
this.runtimeModule = {
hash(args) {
return new Promise((resolve, reject) => {
const ts = logger.ts();
argon2.hash(
Buffer.from(args.password),
Buffer.from(args.salt),
{
type: args.type,
version: args.version,
hashLength: args.length,
saltLength: args.salt.length,
timeCost: args.iterations,
parallelism: args.parallelism,
memoryCost: args.memory
},
(err, res) => {
if (err) {
logger.error('Argon2 error', err);
return reject(err);
}
logger.debug('Argon2 hash calculated', logger.ts(ts));
resolve(res);
}
);
});
}
};
return Promise.resolve(this.runtimeModule);
}
return new Promise((resolve, reject) => {
const loadTimeout = setTimeout(() => reject('timeout'), 5000);
try {

View File

@ -1004,11 +1004,8 @@ class DetailsView extends View {
deleteFromTrash() {
Alerts.yesno({
header: Locale.detDelFromTrash,
body:
Locale.detDelFromTrashBody +
' <p class="muted-color">' +
Locale.detDelFromTrashBodyHint +
'</p>',
body: Locale.detDelFromTrashBody,
hint: Locale.detDelFromTrashBodyHint,
icon: 'minus-circle',
success: () => {
this.model.deleteFromTrash();

View File

@ -22,8 +22,8 @@ class FieldViewUrl extends FieldViewText {
}
fixUrl(url) {
const proto = new URL(url, 'dummy://').protocol;
if (proto === 'dummy:') {
const proto = new URL(url, 'ws://x').protocol;
if (proto === 'ws:') {
return 'https://' + url;
}
if (!AllowedProtocols.includes(proto)) {
@ -37,7 +37,7 @@ class FieldViewUrl extends FieldViewText {
}
getTextValue() {
return this.fixUrl(this.value);
return this.value;
}
}

View File

@ -123,11 +123,12 @@ class ImportCsvView extends View {
this.groups = [];
for (const file of this.appModel.files) {
file.forEachGroup(group => {
let title = escape(group.title);
const title = group.title;
const spaces = [];
for (let parent = group; parent.parentGroup; parent = parent.parentGroup) {
title = '&nbsp;&nbsp;' + title;
spaces.push(' ', ' ');
}
this.groups.push({ id: group.id, fileId: file.id, title });
this.groups.push({ id: group.id, fileId: file.id, spaces, title });
});
}
}

View File

@ -137,15 +137,15 @@ class ListSearchView extends View {
this.sortOptions.forEach(opt => {
opt.html = opt.loc();
});
const entryDesc = Features.isMobile
? ''
: ' <span class="muted-color">(' +
Locale.searchShiftClickOr +
' ' +
Shortcuts.altShortcutSymbol(true) +
'N)</span>';
this.createOptions = [
{ value: 'entry', icon: 'key', html: StringFormat.capFirst(Locale.entry) + entryDesc },
{
value: 'entry',
icon: 'key',
text: StringFormat.capFirst(Locale.entry),
hint: Features.isMobile
? null
: `(${Locale.searchShiftClickOr} ${Shortcuts.altShortcutSymbol(true)})`
},
{ value: 'group', icon: 'folder', text: StringFormat.capFirst(Locale.group) }
];
if (this.el) {

View File

@ -105,7 +105,7 @@ class ListView extends View {
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
}, this);
const html = itemsTemplate(
{ items: itemsHtml, columns: this.tableColumns },
{ itemsHtml, columns: this.tableColumns },
DefaultTemplateOptions
);
this.itemsEl.html(html);
@ -123,8 +123,8 @@ class ListView extends View {
}
}
renderPlainItems(itemsHtml) {
return itemsHtml.items;
renderPlainItems(data) {
return data.itemsHtml;
}
getItemTemplate() {
@ -180,13 +180,13 @@ class ListView extends View {
}
createTemplate() {
if (!this.model.settings.templateHelpShown) {
if (!this.model.settings.templateHelpShown_) {
Alerts.yesno({
icon: 'sticky-note-o',
header: Locale.listAddTemplateHeader,
body:
Locale.listAddTemplateBody1.replace('{}', '<i class="fa fa-plus"></i>') +
'<br/>' +
Locale.listAddTemplateBody1.replace('{}', '"+"') +
'\n' +
Locale.listAddTemplateBody2.replace('{}', 'Templates'),
buttons: [Alerts.buttons.ok, Alerts.buttons.cancel],
success: () => {

View File

@ -29,7 +29,10 @@ class ModalView extends View {
}
render() {
super.render(this.model);
super.render({
...this.model,
body: this.model.body ? this.model.body.toString().split('\n') : ''
});
this.$el.addClass('modal--hidden');
setTimeout(() => {
this.$el.removeClass('modal--hidden');
@ -43,7 +46,7 @@ class ModalView extends View {
change(config) {
if (config.header) {
this.$el.find('.modal__header').html(config.header);
this.$el.find('.modal__header').text(config.header);
}
}

View File

@ -19,7 +19,7 @@ import { Logger } from 'util/logger';
import { InputFx } from 'util/ui/input-fx';
import { OpenConfigView } from 'views/open-config-view';
import { StorageFileListView } from 'views/storage-file-list-view';
import { escape, omit } from 'util/fn';
import { omit } from 'util/fn';
import { GeneratorView } from 'views/generator-view';
import template from 'templates/open.hbs';
@ -77,6 +77,7 @@ class OpenView extends View {
this.once('remove', () => {
this.passwordInput.reset();
});
this.listenTo(Events, 'user-idle', this.userIdle);
}
render() {
@ -674,11 +675,8 @@ class OpenView extends View {
}
Alerts.error({
header: Locale.openError,
body:
Locale.openErrorDescription +
'<pre class="modal__pre">' +
escape(err.toString()) +
'</pre>'
body: Locale.openErrorDescription,
pre: err.toString()
});
}
} else {
@ -757,11 +755,8 @@ class OpenView extends View {
if (err.lastIndexOf('OAuth', 0) !== 0 && !Alerts.alertDisplayed) {
Alerts.error({
header: Locale.openError,
body:
Locale.openListErrorBody +
'<pre class="modal__pre">' +
escape(err.toString()) +
'</pre>'
body: Locale.openListErrorBody,
pre: err.toString()
});
}
return;
@ -979,6 +974,11 @@ class OpenView extends View {
this.views.gen = generator;
}
userIdle() {
this.inputEl.val('');
this.passwordInput.reset();
}
usbDevicesChanged() {
if (this.model.settings.canOpenOtpDevice && this.model.settings.yubiKeyShowIcon) {
const hasYubiKeys = !!UsbListener.attachedYubiKeys.length;

View File

@ -12,7 +12,7 @@ import { PasswordPresenter } from 'util/formatting/password-presenter';
import { Locale } from 'util/locale';
import { FileSaver } from 'util/ui/file-saver';
import { OpenConfigView } from 'views/open-config-view';
import { escape, omit } from 'util/fn';
import { omit } from 'util/fn';
import template from 'templates/settings/settings-file.hbs';
const DefaultBackupPath = 'Backups/{name}.{date}.bak';
@ -240,8 +240,8 @@ class SettingsFileView extends View {
if (err) {
Alerts.error({
header: Locale.setFileSaveError,
body:
Locale.setFileSaveErrorBody + ' ' + path + ': \n' + err
body: Locale.setFileSaveErrorBody + ' ' + path + ':',
pre: err
});
}
});
@ -256,16 +256,28 @@ class SettingsFileView extends View {
}
saveToXml() {
this.model.getXml(xml => {
const blob = new Blob([xml], { type: 'text/xml' });
FileSaver.saveAs(blob, this.model.name + '.xml');
Alerts.yesno({
header: Locale.saveFileExportRaw,
body: Locale.saveFileExportRawBody,
success: () => {
this.model.getXml(xml => {
const blob = new Blob([xml], { type: 'text/xml' });
FileSaver.saveAs(blob, this.model.name + '.xml');
});
}
});
}
saveToHtml() {
this.model.getHtml(html => {
const blob = new Blob([html], { type: 'text/html' });
FileSaver.saveAs(blob, this.model.name + '.html');
Alerts.yesno({
header: Locale.saveFileExportRaw,
body: Locale.saveFileExportRawBody,
success: () => {
this.model.getHtml(html => {
const blob = new Blob([html], { type: 'text/html' });
FileSaver.saveAs(blob, this.model.name + '.html');
});
}
});
}
@ -332,10 +344,7 @@ class SettingsFileView extends View {
if (existingFile) {
Alerts.yesno({
header: Locale.setFileAlreadyExists,
body: Locale.setFileAlreadyExistsBody.replace(
'{}',
this.model.escape('name')
),
body: Locale.setFileAlreadyExistsBody.replace('{}', this.model.name),
success: () => {
this.model.syncing = true;
storage.remove(existingFile.path, err => {
@ -587,11 +596,8 @@ class SettingsFileView extends View {
}
Alerts.error({
title,
body:
description +
'<pre class="modal__pre">' +
escape(err.toString()) +
'</pre>'
body: description,
pre: err.toString()
});
}
});

View File

@ -1,7 +1,6 @@
import { View } from 'framework/views/view';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { escape } from 'util/fn';
import { Launcher } from 'comp/launcher';
import template from 'templates/settings/settings-help.hbs';
@ -30,7 +29,7 @@ class SettingsHelpView extends View {
encodeURIComponent('!please describe your issue here!\n\n' + appInfo),
desktopLink: Links.Desktop,
webAppLink: Links.WebApp,
appInfo: escape(appInfo)
appInfo
});
}
}

View File

@ -2,7 +2,7 @@
id="at-select__item--{{id}}">
<td>
{{~#if customIcon~}}
<img src="{{{customIcon}}}" class="at-select__item-icon at-select__item-icon--custom {{#if color}}{{color}}{{/if}}" />
<img src="{{customIcon}}" class="at-select__item-icon at-select__item-icon--custom {{#if color}}{{color}}{{/if}}" />
{{~else~}}
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} at-select__item-icon"></i>
{{~/if~}}

View File

@ -16,7 +16,7 @@
<h1 class="details__header-title">{{#if title}}{{title}}{{else}}(no title){{/if}}</h1>
{{#if customIcon}}
<div class="details__header-icon details__header-icon--icon" title="{{res 'detSetIcon'}}">
<img class="details__header-icon-img" src="{{{customIcon}}}" />
<img class="details__header-icon-img" src="{{customIcon}}" />
</div>
{{else}}
<i class="details__header-icon fa fa-{{icon}}" title="{{res 'detSetIcon'}}"></i>

View File

@ -3,10 +3,9 @@
<div class="dropdown__item {{#if option.active}}dropdown__item--active{{/if}}" data-value="{{option.value}}">
<i class="fa fa-{{option.icon}} dropdown__item-icon"></i>
<span class="dropdown__item-text">
{{~#if option.text~}}
{{option.text}}
{{~else~}}
{{{option.html}}}
{{option.text}}
{{~#if option.hint~}}
&nbsp;<span class="muted-color">{{option.hint}}</span>
{{~/if~}}
</span>
</div>

View File

@ -19,7 +19,7 @@
<label>{{Res 'icon'}}:</label>
<div class="grp__icon-wrap">
{{#if customIcon}}
<img src="{{{customIcon}}}" class="grp__icon grp__icon--image" />
<img src="{{customIcon}}" class="grp__icon grp__icon--image" />
{{else}}
<i class="fa fa-{{icon}} grp__icon"></i>
{{/if}}

View File

@ -19,7 +19,7 @@
{{#each customIcons as |icon ci|}}
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
data-val="{{ci}}">
<img src="{{{icon}}}" />
<img src="{{icon}}" />
</span>
{{/each}}
</div>

View File

@ -57,7 +57,10 @@
<select class="import-csv__target-select" id="import-csv__target-select">
<option value="" selected>{{res 'importNewFile'}}</option>
{{#each groups as |group|}}
<option value="{{group.id}}" data-file="{{group.fileId}}">{{{group.title}}}</option>
<option value="{{group.id}}" data-file="{{group.fileId}}">
{{#if spaces}}{{#each spaces}}&nbsp;{{/each}}{{/if}}
{{group.title}}
</option>
{{/each}}
</select>
</div>

View File

@ -1,6 +1,6 @@
<div class="list__item {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
{{#if customIcon~}}
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
{{~else~}}
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
{{~/if}}

View File

@ -1,7 +1,7 @@
<tr class="list__item list__item--table {{#if active}}list__item--active{{/if}} {{#if expired}}list__item--expired{{/if}}" id="{{id}}" draggable="true">
<td>
{{~#if customIcon~}}
<img src="{{{customIcon}}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
<img src="{{customIcon}}" class="list__item-icon list__item-icon--custom {{#if color}}{{color}}{{/if}}" />
{{~else~}}
<i class="fa fa-{{icon}} {{#if color}}{{color}}-color{{/if}} list__item-icon"></i>
{{~/if~}}

View File

@ -8,6 +8,6 @@
</tr>
</thead>
<tbody>
{{{items}}}
{{{itemsHtml}}}
</tbody>
</table>

View File

@ -8,7 +8,7 @@
<div class="menu__item-body" {{#if drag}}draggable="true"{{/if}}>
{{#if drag}}<div class="menu__item-drag-top"></div>{{/if}}
{{#if customIcon~}}
<img src="{{{customIcon}}}" class="menu__item-icon menu__item-icon--image" />
<img src="{{customIcon}}" class="menu__item-icon menu__item-icon--image" />
{{~else~}}
<i class="menu__item-icon fa {{#if icon}}fa-{{icon}}{{else}}menu__item-icon--no-icon{{/if}}"></i>
{{~/if}}

View File

@ -1,9 +1,14 @@
<div class="modal modal--hidden {{#if opaque}}modal--opaque{{/if}}">
<div class="modal__content">
<i class="modal__icon fa fa-{{icon}}"></i>
<div class="modal__header">{{{header}}}</div>
<div class="modal__header">{{header}}</div>
<div class="modal__body">
{{{body}}}
{{#each body as |item|}}
{{item}}
{{#unless @last}}<br/>{{/unless}}
{{/each}}
{{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}}
{{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}}
{{#if checkbox}}
<div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div>
{{/if}}

View File

@ -9,7 +9,7 @@
autocomplete="off" autocapitalize="off"
{{#if placeholder}}placeholder="{{res placeholder}}"{{/if}}
{{#if required}}required{{/if}}
{{#if pattern}}pattern="{{{pattern}}}"{{/if}}
{{#if pattern}}pattern="{{pattern}}"{{/if}}
/>
{{/each}}
</div>

View File

@ -36,6 +36,7 @@
<h3>Desktop modules</h3>
<ul>
<li><a href="https://github.com/antelle/node-stream-zip" target="_blank">node-stream-zip</a><span class="muted-color">, node.js library for fast reading of large ZIPs</span></li>
<li><a href="https://github.com/ranisalt/node-argon2" target="_blank">node-argon2</a><span class="muted-color">, node.js bindings for Argon2 hashing algorithm</span></li>
</ul>
<h3>Utils</h3>

View File

@ -25,7 +25,7 @@
data-id="{{id}}"
{{#if placeholder}}placeholder="{{res placeholder}}"{{/if}}
{{#if required}}required{{/if}}
{{#if pattern}}pattern="{{{pattern}}}"{{/if}}
{{#if pattern}}pattern="{{pattern}}"{{/if}}
/>
{{/ifeq}}
{{/each}}

View File

@ -1,5 +1,13 @@
Release notes
-------------
##### v1.15.0 (WIP)
`+` #557: Argon2 speed improvements in desktop apps
`+` #1400: auto-apply tag when creating new entry in tag view
`+` #1342: hint that the data will be stored in unencrypted form after exporting
`*` #1471: WebDAV url validation, only HTTPS is allowed
`+` #1350: clearing master password after auto lock period
`-` fix #1463: copying the original url instead of adding https:
##### v1.14.0 (2020-04-18)
`+` using OAuth authorization code grant for all storage providers
`-` fixed a number of vulnerabilities in opening untrusted kdbx files