Skip to content

Commit

Permalink
Kdbx 4.1 (#6)
Browse files Browse the repository at this point in the history
Support KDBX v4.1 (now default)

Also adds some 4.0 missing features like Group custom data

Remove deprecations, lint issues, etc.
  • Loading branch information
luckyrat authored Feb 15, 2024
1 parent b9f7e7d commit ae7f17b
Show file tree
Hide file tree
Showing 19 changed files with 876 additions and 134 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
with:
sdk: ${{ matrix.sdk }}
Expand Down
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ analyzer:
missing_return: warning
# allow having TODOs in the code
todo: ignore
invalid_use_of_protected_member: error
language:
strict-casts: true
strict-raw-types: true
Expand Down
4 changes: 4 additions & 0 deletions dart_test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
tags:
kdbx3: {}
kdbx4: {}

# macOs github runners are slower
on_platform:
mac-os: {timeout: 2x}
82 changes: 75 additions & 7 deletions lib/src/kdbx_custom_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:xml/xml.dart' as xml;

class KdbxCustomData extends KdbxNode {
KdbxCustomData.create()
class KdbxObjectCustomData extends KdbxNode {
KdbxObjectCustomData.create()
: _data = {},
super.create(TAG_NAME);
super.create(KdbxXml.NODE_CUSTOM_DATA);

KdbxCustomData.read(xml.XmlElement node)
KdbxObjectCustomData.read(xml.XmlElement node)
: _data = Map.fromEntries(
node.findElements(KdbxXml.NODE_CUSTOM_DATA_ITEM).map((el) {
final key = el.singleTextNode(KdbxXml.NODE_KEY);
Expand All @@ -17,8 +17,6 @@ class KdbxCustomData extends KdbxNode {
})),
super.read(node);

static const String TAG_NAME = KdbxXml.NODE_CUSTOM_DATA;

final Map<String, String> _data;

Iterable<MapEntry<String, String>> get entries => _data.entries;
Expand All @@ -29,6 +27,7 @@ class KdbxCustomData extends KdbxNode {
}

bool containsKey(String key) => _data.containsKey(key);
String? remove(String key) => modify(() => _data.remove(key));

@override
xml.XmlElement toXml() {
Expand All @@ -44,7 +43,76 @@ class KdbxCustomData extends KdbxNode {
return el;
}

void overwriteFrom(KdbxCustomData other) {
void overwriteFrom(KdbxObjectCustomData other) {
_data.clear();
_data.addAll(other._data);
}
}

typedef KdbxMetaCustomDataItem = ({
String value,
DateTime? lastModified,
});

class KdbxMetaCustomData extends KdbxNode {
KdbxMetaCustomData.create()
: _data = {},
super.create(KdbxXml.NODE_CUSTOM_DATA);

KdbxMetaCustomData.read(xml.XmlElement node)
: _data = Map.fromEntries(
node.findElements(KdbxXml.NODE_CUSTOM_DATA_ITEM).map((el) {
final key = el.singleTextNode(KdbxXml.NODE_KEY);
final value = el.singleTextNode(KdbxXml.NODE_VALUE);
final lastModified =
el.singleElement(KdbxXml.NODE_LAST_MODIFICATION_TIME)?.innerText;
return MapEntry(key, (
value: value,
lastModified: lastModified != null
? DateTimeUtils.fromBase64(lastModified)
: null
));
})),
super.read(node);

final Map<String, KdbxMetaCustomDataItem> _data;

Iterable<MapEntry<String, KdbxMetaCustomDataItem>> get entries =>
_data.entries;

KdbxMetaCustomDataItem? operator [](String key) => _data[key];
void operator []=(String key, KdbxMetaCustomDataItem value) {
modify(() => _data[key] = value);
}

bool containsKey(String key) => _data.containsKey(key);
KdbxMetaCustomDataItem? remove(String key) => modify(() => _data.remove(key));

@override
xml.XmlElement toXml() {
final el = super.toXml();
el.children.clear();
el.children.addAll(
_data.entries.map((e) {
//TODO: We don't have any context here so have to output everything regardless
// of intended kdbx version. Maybe we can improve that one day to allow
// safer output of earlier kdbx versions?
final d = e.value.lastModified != null
? DateTimeUtils.toBase64(e.value.lastModified!)
: null;

return XmlUtils.createNode(KdbxXml.NODE_CUSTOM_DATA_ITEM, [
XmlUtils.createTextNode(KdbxXml.NODE_KEY, e.key),
XmlUtils.createTextNode(KdbxXml.NODE_VALUE, e.value.value),
if (d != null)
XmlUtils.createTextNode(KdbxXml.NODE_LAST_MODIFICATION_TIME, d),
]);
}),
);
return el;
}

void overwriteFrom(KdbxMetaCustomData other) {
_data.clear();
_data.addAll(other._data);
}
Expand Down
32 changes: 18 additions & 14 deletions lib/src/kdbx_entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ extension KdbxEntryInternal on KdbxEntry {
foregroundColor,
backgroundColor,
overrideURL,
tags,
qualityCheck,
];

void _overwriteFrom(
Expand Down Expand Up @@ -426,6 +426,8 @@ extension KdbxEntryInternal on KdbxEntry {
));
_binaries.clear();
_binaries.addAll(newBinaries);
// Dart doesn't know this is actually OK since it's an extension to a subclass
// ignore: invalid_use_of_protected_member
customData.overwriteFrom(other.customData);
times.overwriteFrom(other.times);
if (includeHistory) {
Expand Down Expand Up @@ -456,8 +458,7 @@ class KdbxEntry extends KdbxObject {
KdbxFile file,
KdbxGroup parent, {
this.isHistoryEntry = false,
}) : customData = KdbxCustomData.create(),
history = [],
}) : history = [],
super.create(file.ctx, file, 'Entry', parent) {
icon.set(KdbxIcon.Key);
_browserSettings = BrowserEntrySettings(
Expand All @@ -467,11 +468,7 @@ class KdbxEntry extends KdbxObject {

KdbxEntry.read(KdbxReadWriteContext ctx, KdbxGroup? parent, XmlElement node,
{this.isHistoryEntry = false})
: customData = node
.singleElement(KdbxXml.NODE_CUSTOM_DATA)
?.let((e) => KdbxCustomData.read(e)) ??
KdbxCustomData.create(),
history = [],
: history = [],
super.read(ctx, parent, node) {
_strings.addEntries(node.findElements(KdbxXml.NODE_STRING).map((el) {
final key = KdbxKey(el.findElements(KdbxXml.NODE_KEY).single.text);
Expand Down Expand Up @@ -552,16 +549,17 @@ class KdbxEntry extends KdbxObject {
_browserSettings = null;
}

final KdbxCustomData customData;

final bool isHistoryEntry;

final List<KdbxEntry> history;

ColorNode get foregroundColor => ColorNode(this, 'ForegroundColor');
ColorNode get backgroundColor => ColorNode(this, 'BackgroundColor');
StringNode get overrideURL => StringNode(this, 'OverrideURL');
StringListNode get tags => StringListNode(this, 'Tags');
ColorNode get foregroundColor =>
ColorNode(this, KdbxXml.NODE_FOREGROUND_COLOR);
ColorNode get backgroundColor =>
ColorNode(this, KdbxXml.NODE_BACKGROUND_COLOR);
StringNode get overrideURL => StringNode(this, KdbxXml.NODE_OVERRIDE_URL);
NullableBooleanNode get qualityCheck =>
NullableBooleanNode(this, KdbxXml.NODE_QUALITY_CHECK);

@override
set file(KdbxFile? file) {
Expand Down Expand Up @@ -617,6 +615,12 @@ class KdbxEntry extends KdbxObject {
@override
XmlElement toXml() {
final el = super.toXml()..replaceSingle(customData.toXml());

if (ctx.version < KdbxVersion.V4_1) {
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_QUALITY_CHECK);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_PREVIOUS_PARENT_GROUP);
}

XmlUtils.removeChildrenByName(el, KdbxXml.NODE_STRING);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_HISTORY);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_BINARY);
Expand Down
50 changes: 38 additions & 12 deletions lib/src/kdbx_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ import 'dart:async';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_dao.dart';
import 'package:kdbx/src/kdbx_exceptions.dart';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:logging/logging.dart';
import 'package:quiver/check.dart';
import 'package:synchronized/synchronized.dart';
Expand Down Expand Up @@ -150,17 +143,50 @@ class KdbxFile {
return recycleBin ?? _createRecycleBin();
}

/// Upgrade v3 file to v4.
void upgrade(int majorVersion) {
/// Upgrade v3 file to v4.x
void upgrade(int majorVersion, int minorVersion) {
checkArgument(majorVersion == 4, message: 'Must be majorVersion 4');
body.meta.settingsChanged.setToNow();
body.meta.headerHash.remove();
header.upgrade(majorVersion);
header.version.major == 4
? header.upgradeMinor(majorVersion, minorVersion)
: header.upgrade(majorVersion, minorVersion);

upgradeDateTimeFormatV4();

body.meta.settingsChanged.setToNow();
}

void upgradeDateTimeFormatV4() {
body.meta.databaseNameChanged.upgrade();
body.meta.databaseDescriptionChanged.upgrade();
body.meta.defaultUserNameChanged.upgrade();
body.meta.masterKeyChanged.upgrade();
body.meta.recycleBinChanged.upgrade();
body.meta.entryTemplatesGroupChanged.upgrade();
body.meta.settingsChanged.upgrade();
body.rootGroup.getAllGroups().values.forEach(upgradeAllObjectTimesV4);
body.rootGroup.getAllEntries().values.forEach(upgradeAllObjectTimesV4);
}

void upgradeAllObjectTimesV4(KdbxObject obj) {
obj.times.creationTime.upgrade();
obj.times.lastModificationTime.upgrade();
obj.times.lastAccessTime.upgrade();
obj.times.expiryTime.upgrade();
obj.times.locationChanged.upgrade();

if (obj is KdbxEntry) {
obj.history.forEach(upgradeAllObjectTimesV4);
}
}

/// Merges the given file into this file.
/// Both files must have the same origin (ie. same root group UUID).
MergeContext merge(KdbxFile other) {
if (header.version < other.header.version) {
throw KdbxUnsupportedException(
'Kdbx version of source is newer. Upgrade file version before attempting to merge.');
}
if (other.body.rootGroup.uuid != body.rootGroup.uuid) {
throw KdbxUnsupportedException(
'Root groups of source and dest file do not match.');
Expand Down
8 changes: 5 additions & 3 deletions lib/src/kdbx_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class KdbxReadWriteContext {
final KdbxHeader header;

int get versionMajor => header.version.major;
KdbxVersion get version => header.version;

void initContext(Iterable<KdbxBinary> binaries,
Iterable<KdbxDeletedObject> deletedObjects) {
Expand Down Expand Up @@ -316,7 +317,8 @@ class KdbxBody extends KdbxNode {
final now = clock.now().toUtc();
final historyMaxItems = (meta.historyMaxItems.get() ?? 0) > 0
? meta.historyMaxItems.get()
: double.maxFinite as int;
: (double.maxFinite).toInt();

final usedCustomIcons = HashSet<KdbxUuid>();
final unusedCustomIcons = HashSet<KdbxUuid>();
final usedBinaries = <int>{};
Expand Down Expand Up @@ -519,14 +521,14 @@ class KdbxFormat {
static bool dartWebWorkaround = false;

/// Creates a new, empty [KdbxFile] with default settings.
/// If [header] is not given by default a kdbx 4.0 file will be created.
/// If [header] is not given by default a kdbx 4.1 file will be created.
KdbxFile create(
Credentials credentials,
String name, {
String? generator,
KdbxHeader? header,
}) {
header ??= KdbxHeader.createV4();
header ??= KdbxHeader.createV4_1();
final ctx = KdbxReadWriteContext(header: header);
final meta = KdbxMeta.create(
databaseName: name,
Expand Down
9 changes: 8 additions & 1 deletion lib/src/kdbx_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ class KdbxGroup extends KdbxObject {

@override
XmlElement toXml() {
final el = super.toXml();
final el = super.toXml()..replaceSingle(customData.toXml());

if (ctx.version < KdbxVersion.V4_1) {
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_TAGS);
XmlUtils.removeChildrenByName(el, KdbxXml.NODE_PREVIOUS_PARENT_GROUP);
}

XmlUtils.removeChildrenByName(el, 'Group');
XmlUtils.removeChildrenByName(el, 'Entry');
el.children.addAll(groups.values.map((g) => g.toXml()));
Expand Down Expand Up @@ -254,6 +260,7 @@ class KdbxGroup extends KdbxObject {
overwriteSubNodesFrom(mergeContext, _overwriteNodes, other._overwriteNodes);
// we should probably check that [lastTopVisibleEntry] is still a
// valid reference?
customData.overwriteFrom(other.customData);
times.overwriteFrom(other.times);
}

Expand Down
13 changes: 10 additions & 3 deletions lib/src/kdbx_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -569,11 +569,18 @@ class KdbxHeader {
void writeKdfParameters(VarDictionary kdfParameters) =>
_setHeaderField(HeaderFields.KdfParameters, kdfParameters.write());

void upgrade(int majorVersion) {
void upgradeMinor(int majorVersion, int minorVersion) {
checkArgument(majorVersion == KdbxVersion.V4.major,
message: 'Can only upgrade v4');
_logger.info('Upgrading header to $minorVersion');
_version = KdbxVersion._(majorVersion, minorVersion);
}

void upgrade(int majorVersion, int minorVersion) {
checkArgument(majorVersion == KdbxVersion.V4.major,
message: 'Can only upgrade to 4');
_logger.info('Upgrading header to $majorVersion');
_version = KdbxVersion._(majorVersion, 0);
_logger.info('Upgrading header to $majorVersion.$minorVersion');
_version = KdbxVersion._(majorVersion, minorVersion);
if (fields[HeaderFields.KdfParameters] == null) {
_logger.fine('Creating kdf parameters.');
writeKdfParameters(_createKdfDefaultParameters());
Expand Down
Loading

0 comments on commit ae7f17b

Please sign in to comment.