pull/1856/head
antelle 2 years ago
parent a845a42d0d
commit 0cbdd6281a
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
  1. 1
      .eslintrc
  2. 2
      app/scripts/comp/app/online-password-checker.js
  3. 2
      app/scripts/comp/app/updater.js
  4. 4
      app/scripts/comp/browser/secure-input.js
  5. 2
      app/scripts/comp/extension/protocol-impl.js
  6. 10
      app/scripts/comp/format/kdbx-to-html.js
  7. 2
      app/scripts/comp/launcher/native-modules.js
  8. 72
      app/scripts/models/entry-model.js
  9. 17
      app/scripts/models/file-model.js
  10. 6
      app/scripts/models/group-model.js
  11. 2
      app/scripts/plugins/plugin-gallery.js
  12. 2
      app/scripts/plugins/plugin.js
  13. 2
      app/scripts/storage/impl/storage-webdav.js
  14. 6
      app/scripts/storage/pkce.js
  15. 2
      app/scripts/util/data/signature-verifier.js
  16. 2
      app/scripts/util/formatting/icon-url-format.js
  17. 8
      app/scripts/util/generators/password-generator.js
  18. 4
      app/scripts/util/kdbxweb/kdbxweb-init.js
  19. 4
      app/scripts/util/kdbxweb/protected-value-ex.js
  20. 2
      app/scripts/views/details/details-view.js
  21. 2
      app/scripts/views/fields/field-view-custom.js
  22. 2
      app/scripts/views/fields/field-view-text.js
  23. 2
      app/scripts/views/fields/field-view.js
  24. 2
      app/scripts/views/import-csv-view.js
  25. 2
      app/scripts/views/open-view.js
  26. 2
      app/scripts/views/settings/settings-file-view.js
  27. 27
      package-lock.json
  28. 2
      package.json
  29. 1
      release-notes.md

@ -48,7 +48,6 @@
"import/no-webpack-loader-syntax": "off",
"import/no-relative-parent-imports": "error",
"import/first": "error",
"import/no-namespace": "error",
"import/no-default-export": "error",
"babel/no-unused-expressions": "error",
"node/no-callback-literal": "off"

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
const logger = new Logger('online-password-checker');

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const SecureInput = function () {
this.el = null;
@ -79,7 +79,7 @@ Object.defineProperty(SecureInput.prototype, 'value', {
const len = pseudoValue.length;
let byteLength = 0;
const valueBytes = new Uint8Array(len * 4);
const saltBytes = kdbxweb.Random.getBytes(len * 4);
const saltBytes = kdbxweb.CryptoEngine.random(len * 4);
let ch;
let bytes;
for (let i = 0; i < len; i++) {

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { box as tweetnaclBox } from 'tweetnacl';

@ -1,5 +1,5 @@
/* eslint-disable import/no-commonjs */
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { RuntimeInfo } from 'const/runtime-info';
import { Links } from 'const/links';
import { DateFormat } from 'comp/i18n/date-format';
@ -51,14 +51,14 @@ function walkEntry(db, entry, parents) {
});
}
}
for (const fieldName of Object.keys(entry.fields)) {
for (const [fieldName, fieldValue] of entry.fields) {
if (!KnownFields[fieldName]) {
const value = entryField(entry, fieldName);
if (value) {
fields.push({
title: fieldName,
value,
protect: entry.fields[fieldName].isProtected
protect: fieldValue.isProtected
});
}
}
@ -69,7 +69,7 @@ function walkEntry(db, entry, parents) {
expires = DateFormat.dtStr(entry.times.expiryTime);
}
const attachments = Object.entries(entry.binaries)
const attachments = [...entry.binaries]
.map(([name, data]) => {
if (data && data.ref) {
data = data.value;
@ -95,7 +95,7 @@ function walkEntry(db, entry, parents) {
}
function entryField(entry, fieldName) {
const value = entry.fields[fieldName];
const value = entry.fields.get(fieldName);
return (value && value.isProtected && value.getText()) || value || '';
}

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Model } from 'framework/model';
import { AppSettingsModel } from 'models/app-settings-model';
import { KdbxToHtml } from 'comp/format/kdbx-to-html';
@ -52,7 +52,7 @@ class EntryModel extends Model {
this.icon = this._iconFromId(entry.icon);
this.tags = entry.tags;
this.color = this._colorToModel(entry.bgColor) || this._colorToModel(entry.fgColor);
this.fields = this._fieldsToModel(entry.fields);
this.fields = this._fieldsToModel();
this.attachments = this._attachmentsToModel(entry.binaries);
this.created = entry.times.creationTime;
this.updated = entry.times.lastModTime;
@ -71,7 +71,7 @@ class EntryModel extends Model {
}
_getPassword() {
const password = this.entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
const password = this.entry.fields.get('Password') || kdbxweb.ProtectedValue.fromString('');
if (!password.isProtected) {
return kdbxweb.ProtectedValue.fromString(password);
}
@ -79,7 +79,7 @@ class EntryModel extends Model {
}
_getFieldString(field) {
const val = this.entry.fields[field];
const val = this.entry.fields.get(field);
if (!val) {
return '';
}
@ -103,7 +103,7 @@ class EntryModel extends Model {
_buildSearchText() {
let text = '';
for (const value of Object.values(this.entry.fields)) {
for (const value of this.entry.fields.values()) {
if (typeof value === 'string') {
text += value.toLowerCase() + '\n';
}
@ -122,7 +122,7 @@ class EntryModel extends Model {
this.customIconId = null;
if (this.entry.customIcon) {
this.customIcon = IconUrlFormat.toDataUrl(
this.file.db.meta.customIcons[this.entry.customIcon]
this.file.db.meta.customIcons.get(this.entry.customIcon.id)?.data
);
this.customIconId = this.entry.customIcon.toString();
}
@ -164,13 +164,13 @@ class EntryModel extends Model {
return color ? Color.getNearest(color) : null;
}
_fieldsToModel(fields) {
return omit(fields, BuiltInFields);
_fieldsToModel() {
return omit(this.getAllFields(), BuiltInFields);
}
_attachmentsToModel(binaries) {
const att = [];
for (let [title, data] of Object.entries(binaries)) {
for (let [title, data] of binaries) {
if (data && data.ref) {
data = data.value;
}
@ -210,7 +210,11 @@ class EntryModel extends Model {
}
getAllFields() {
return this.entry.fields;
const fields = {};
for (const [key, value] of this.entry.fields) {
fields[key] = value;
}
return fields;
}
getHistoryEntriesForSearch() {
@ -232,7 +236,7 @@ class EntryModel extends Model {
getFieldValue(field) {
field = field.toLowerCase();
let resolvedField;
Object.keys(this.entry.fields).some((entryField) => {
[...this.entry.fields.keys()].some((entryField) => {
if (entryField.toLowerCase() === field) {
resolvedField = entryField;
return true;
@ -240,7 +244,7 @@ class EntryModel extends Model {
return false;
});
if (resolvedField) {
let fieldValue = this.entry.fields[resolvedField];
let fieldValue = this.entry.fields.get(resolvedField);
const refValue = this._resolveFieldReference(fieldValue);
if (refValue !== undefined) {
fieldValue = refValue;
@ -276,7 +280,7 @@ class EntryModel extends Model {
if (!entry) {
return;
}
return entry.entry.fields[FieldRefIds[fieldRefId]];
return entry.entry.fields.get(FieldRefIds[fieldRefId]);
}
setColor(color) {
@ -329,10 +333,10 @@ class EntryModel extends Model {
if (hasValue || allowEmpty || BuiltInFields.indexOf(field) >= 0) {
this._entryModified();
val = this.sanitizeFieldValue(val);
this.entry.fields[field] = val;
} else if (Object.prototype.hasOwnProperty.call(this.entry.fields, field)) {
this.entry.fields.set(field, val);
} else if (this.entry.fields.has(field)) {
this._entryModified();
delete this.entry.fields[field];
this.entry.fields.delete(field);
}
this._fillByEntry();
}
@ -347,7 +351,7 @@ class EntryModel extends Model {
}
hasField(field) {
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
return this.entry.fields.has(field);
}
addAttachment(name, data) {
@ -360,7 +364,7 @@ class EntryModel extends Model {
removeAttachment(name) {
this._entryModified();
delete this.entry.binaries[name];
this.entry.binaries.delete(name);
this._fillByEntry();
}
@ -390,8 +394,8 @@ class EntryModel extends Model {
this.entry.pushHistory();
this.unsaved = true;
this.file.setModified();
this.entry.fields = {};
this.entry.binaries = {};
this.entry.fields = new Map();
this.entry.binaries = new Map();
this.entry.copyFrom(historyEntry);
this._entryModified();
this._fillByEntry();
@ -402,8 +406,8 @@ class EntryModel extends Model {
this.unsaved = false;
const historyEntry = this.entry.history[this.entry.history.length - 1];
this.entry.removeHistory(this.entry.history.length - 1);
this.entry.fields = {};
this.entry.binaries = {};
this.entry.fields = new Map();
this.entry.binaries = new Map();
this.entry.copyFrom(historyEntry);
this._fillByEntry();
}
@ -476,14 +480,14 @@ class EntryModel extends Model {
otpUrl = Otp.makeUrl(args.key, args.step, args.size);
}
}
} else if (this.entry.fields['TOTP Seed']) {
} else if (this.entry.fields.get('TOTP Seed')) {
// TrayTOTP plugin format
let secret = this.entry.fields['TOTP Seed'];
let secret = this.entry.fields.get('TOTP Seed');
if (secret.isProtected) {
secret = secret.getText();
}
if (secret) {
let settings = this.entry.fields['TOTP Settings'];
let settings = this.entry.fields.get('TOTP Settings');
if (settings && settings.isProtected) {
settings = settings.getText();
}
@ -522,8 +526,8 @@ class EntryModel extends Model {
setOtpUrl(url) {
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
delete this.entry.fields['TOTP Seed'];
delete this.entry.fields['TOTP Settings'];
this.entry.fields.delete('TOTP Seed');
this.entry.fields.delete('TOTP Settings');
}
getEffectiveEnableAutoType() {
@ -574,7 +578,7 @@ class EntryModel extends Model {
newEntry.entry.uuid = uuid;
newEntry.entry.times.update();
newEntry.entry.times.creationTime = newEntry.entry.times.lastModTime;
newEntry.entry.fields.Title = this.title + nameSuffix;
newEntry.entry.fields.set('Title', this.title + nameSuffix);
newEntry._fillByEntry();
this.file.reload();
return newEntry;
@ -586,7 +590,7 @@ class EntryModel extends Model {
this.entry.uuid = uuid;
this.entry.times.update();
this.entry.times.creationTime = this.entry.times.lastModTime;
this.entry.fields.Title = '';
this.entry.fields.set('Title', '');
this._fillByEntry();
}
@ -612,7 +616,7 @@ class EntryModel extends Model {
const allFields = Object.keys(fieldWeights).concat(Object.keys(this.fields));
return allFields.reduce((rank, fieldName) => {
const val = this.entry.fields[fieldName];
const val = this.entry.fields.get(fieldName);
if (!val) {
return rank;
}
@ -630,20 +634,20 @@ class EntryModel extends Model {
}
canCheckPasswordIssues() {
return !this.entry.customData?.IgnorePwIssues;
return !this.entry.customData?.has('IgnorePwIssues');
}
setIgnorePasswordIssues() {
if (!this.entry.customData) {
this.entry.customData = {};
this.entry.customData = new Map();
}
this.entry.customData.IgnorePwIssues = '1';
this.entry.customData.set('IgnorePwIssues', '1');
this._entryModified();
}
getNextUrlFieldName() {
const takenFields = new Set(
Object.keys(this.entry.fields).filter((f) => f.startsWith(ExtraUrlFieldName))
[...this.entry.fields.keys()].filter((f) => f.startsWith(ExtraUrlFieldName))
);
for (let i = 0; ; i++) {
const fieldName = i ? `${ExtraUrlFieldName}_${i}` : ExtraUrlFieldName;

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import demoFileData from 'demo.kdbx';
import { Model } from 'framework/model';
import { Events } from 'framework/events';
@ -669,16 +669,19 @@ class FileModel extends Model {
}
getCustomIcons() {
return mapObject(this.db.meta.customIcons, (customIcon) =>
IconUrlFormat.toDataUrl(customIcon)
);
const customIcons = {};
for (const [id, icon] of this.db.meta.customIcons) {
customIcons[id] = IconUrlFormat.toDataUrl(icon.data);
}
return customIcons;
}
addCustomIcon(iconData) {
const uuid = kdbxweb.KdbxUuid.random();
this.db.meta.customIcons[uuid] = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.base64ToBytes(iconData)
);
this.db.meta.customIcons[uuid] = {
data: kdbxweb.ByteUtils.arrayToBuffer(kdbxweb.ByteUtils.base64ToBytes(iconData)),
lastModified: new Date()
};
return uuid.toString();
}

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { IconMap } from 'const/icon-map';
import { EntryModel } from 'models/entry-model';
import { MenuItemModel } from 'models/menu/menu-item-model';
@ -86,7 +86,9 @@ class GroupModel extends MenuItemModel {
_buildCustomIcon() {
this.customIcon = null;
if (this.group.customIcon) {
return IconUrlFormat.toDataUrl(this.file.db.meta.customIcons[this.group.customIcon]);
return IconUrlFormat.toDataUrl(
this.file.db.meta.customIcons.get(this.group.customIcon.id)?.data
);
}
return null;
}

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { SettingsStore } from 'comp/settings/settings-store';
import { Links } from 'const/links';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import BaseLocale from 'locales/base.json';
import { Model } from 'framework/model';
import { RuntimeInfo } from 'const/runtime-info';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { StorageBase } from 'storage/storage-base';
import { Locale } from 'util/locale';

@ -1,12 +1,12 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
let newOAuthSession;
function createOAuthSession() {
const session = newOAuthSession;
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(64));
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.Random.getBytes(50));
const state = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(64));
const codeVerifier = kdbxweb.ByteUtils.bytesToHex(kdbxweb.CryptoEngine.random(50));
const codeVerifierBytes = kdbxweb.ByteUtils.arrayToBuffer(
kdbxweb.ByteUtils.stringToBytes(codeVerifier)

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import publicKeyData from 'public-key.pem';
import publicKeyDataNew from 'public-key-new.pem';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const IconUrlFormat = {
toDataUrl(iconData) {

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { phonetic } from 'util/generators/phonetic';
import { shuffle } from 'util/fn';
@ -54,8 +54,8 @@ const PasswordGenerator = {
}
}
const rangeIxRandomBytes = kdbxweb.Random.getBytes(countDefaultChars);
const rangeCharRandomBytes = kdbxweb.Random.getBytes(countDefaultChars);
const rangeIxRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const rangeCharRandomBytes = kdbxweb.CryptoEngine.random(countDefaultChars);
const defaultRangeGeneratedChars = [];
for (let i = 0; i < countDefaultChars; i++) {
const rangeIx = i < ranges.length ? i : rangeIxRandomBytes[i] % ranges.length;
@ -65,7 +65,7 @@ const PasswordGenerator = {
}
shuffle(defaultRangeGeneratedChars);
const randomBytes = kdbxweb.Random.getBytes(opts.length);
const randomBytes = kdbxweb.CryptoEngine.random(opts.length);
const chars = [];
for (let i = 0; i < opts.length; i++) {
const rand = Math.round(Math.random() * 1000) + randomBytes[i];

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
import { Features } from 'util/features';
import { NativeModules } from 'comp/launcher/native-modules';
@ -7,7 +7,7 @@ const logger = new Logger('argon2');
const KdbxwebInit = {
init() {
kdbxweb.CryptoEngine.argon2 = (...args) => this.argon2(...args);
kdbxweb.CryptoEngine.setArgon2Impl((...args) => this.argon2(...args));
},
argon2(password, salt, memory, iterations, length, parallelism, type, version) {

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
const ExpectedFieldRefChars = '{REF:0@I:00000000000000000000000000000000}'.split('');
const ExpectedFieldRefByteLength = ExpectedFieldRefChars.length;
@ -175,7 +175,7 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
return true;
};
const RandomSalt = kdbxweb.Random.getBytes(128);
const RandomSalt = kdbxweb.CryptoEngine.random(128);
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
if (!this.byteLength) {

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { AutoType } from 'auto-type';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Keys } from 'const/keys';
import { Locale } from 'util/locale';
import { Tip } from 'util/ui/tip';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { Events } from 'framework/events';
import { KeyHandler } from 'comp/browser/key-handler';
import { Keys } from 'const/keys';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { CopyPaste } from 'comp/browser/copy-paste';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Scrollable } from 'framework/views/scrollable';
import template from 'templates/import-csv.hbs';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { Storage } from 'storage';

@ -1,4 +1,4 @@
import kdbxweb from 'kdbxweb';
import * as kdbxweb from 'kdbxweb';
import { View } from 'framework/views/view';
import { Storage } from 'storage';
import { Shortcuts } from 'comp/app/shortcuts';

27
package-lock.json generated

@ -67,7 +67,7 @@
"jquery": "3.6.0",
"json-loader": "^0.5.7",
"jsqrcode": "github:antelle/jsqrcode#0.1.3",
"kdbxweb": "^1.14.4",
"kdbxweb": "^2.0.0",
"load-grunt-tasks": "5.1.0",
"lodash": "^4.17.21",
"marked": "^2.0.3",
@ -11706,13 +11706,16 @@
}
},
"node_modules/kdbxweb": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/kdbxweb/-/kdbxweb-1.14.4.tgz",
"integrity": "sha512-QhLQ6lU12Atba33/D0h/3UNLGYqmHXwjzcfgnFQ91A1PIXLS99/UpWHg1GvBniZrpXzfi8TnTEXD4wgLTK3ktw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kdbxweb/-/kdbxweb-2.0.0.tgz",
"integrity": "sha512-oomAWfJRkgWU+1Bi20N2SXJbHLT2wnCibIsdXQm5HNQ8ot2wRa6weAMxvfekYWKbo3/mLKwLV8mq4cNhjXwhTw==",
"dependencies": {
"pako": "github:keeweb/pako#653c0b00d8941c89d09ed4546d2179001ec44efc",
"text-encoding": "github:keeweb/text-encoding#4dfb7cb0954c222852092f8b06ae4f6b4f60bfbb",
"xmldom": "github:keeweb/xmldom#ec8f61f723e2f403adaf7a1bbf55ced4ff1ea0c6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/antelle"
}
},
"node_modules/kdbxweb/node_modules/xmldom": {
@ -18398,9 +18401,6 @@
"node": ">=0.10.0"
}
},
"node_modules/text-encoding": {
"resolved": "git+https://git@github.com/keeweb/text-encoding.git#4dfb7cb0954c222852092f8b06ae4f6b4f60bfbb"
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -29846,12 +29846,11 @@
}
},
"kdbxweb": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/kdbxweb/-/kdbxweb-1.14.4.tgz",
"integrity": "sha512-QhLQ6lU12Atba33/D0h/3UNLGYqmHXwjzcfgnFQ91A1PIXLS99/UpWHg1GvBniZrpXzfi8TnTEXD4wgLTK3ktw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kdbxweb/-/kdbxweb-2.0.0.tgz",
"integrity": "sha512-oomAWfJRkgWU+1Bi20N2SXJbHLT2wnCibIsdXQm5HNQ8ot2wRa6weAMxvfekYWKbo3/mLKwLV8mq4cNhjXwhTw==",
"requires": {
"pako": "github:keeweb/pako#653c0b00d8941c89d09ed4546d2179001ec44efc",
"text-encoding": "github:keeweb/text-encoding#4dfb7cb0954c222852092f8b06ae4f6b4f60bfbb",
"xmldom": "github:keeweb/xmldom#ec8f61f723e2f403adaf7a1bbf55ced4ff1ea0c6"
},
"dependencies": {
@ -35131,10 +35130,6 @@
}
}
},
"text-encoding": {
"version": "git+https://git@github.com/keeweb/text-encoding.git#4dfb7cb0954c222852092f8b06ae4f6b4f60bfbb",
"from": "text-encoding@github:keeweb/text-encoding#4dfb7cb0954c222852092f8b06ae4f6b4f60bfbb"
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

@ -69,7 +69,7 @@
"jquery": "3.6.0",
"json-loader": "^0.5.7",
"jsqrcode": "github:antelle/jsqrcode#0.1.3",
"kdbxweb": "^1.14.4",
"kdbxweb": "^2.0.0",
"load-grunt-tasks": "5.1.0",
"lodash": "^4.17.21",
"marked": "^2.0.3",

@ -4,6 +4,7 @@ Release notes
`+` browser extension "KeeWeb Connect"
`+` support for KeePassXC-Browser
`+` optimized memory consumption for large files
`+` KDBX4.1 support
`+` option to use short-lived tokens in cloud storages
`+` opening XML and CSV files using the Open button
`*` password generator now includes all selected character ranges

Loading…
Cancel
Save