Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
luckyrat committed Feb 14, 2024
1 parent 0197f5c commit 3ad19ea
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 56 deletions.
17 changes: 7 additions & 10 deletions lib/src/kdbx_custom_data.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:xml/xml.dart' as xml;
import 'package:collection/collection.dart';

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

KdbxObjectCustomData.read(xml.XmlElement node)
: _data = Map.fromEntries(
Expand All @@ -19,8 +17,6 @@ class KdbxObjectCustomData 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 @@ -31,7 +27,7 @@ class KdbxObjectCustomData extends KdbxNode {
}

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

@override
xml.XmlElement toXml() {
Expand Down Expand Up @@ -61,7 +57,7 @@ typedef KdbxMetaCustomDataItem = ({
class KdbxMetaCustomData extends KdbxNode {
KdbxMetaCustomData.create()
: _data = {},
super.create(TAG_NAME);
super.create(KdbxXml.NODE_CUSTOM_DATA);

KdbxMetaCustomData.read(xml.XmlElement node)
: _data = Map.fromEntries(
Expand All @@ -79,8 +75,6 @@ class KdbxMetaCustomData extends KdbxNode {
})),
super.read(node);

static const String TAG_NAME = KdbxXml.NODE_CUSTOM_DATA;

final Map<String, KdbxMetaCustomDataItem> _data;

Iterable<MapEntry<String, KdbxMetaCustomDataItem>> get entries =>
Expand All @@ -92,14 +86,17 @@ class KdbxMetaCustomData extends KdbxNode {
}

bool containsKey(String key) => _data.containsKey(key);
KdbxMetaCustomDataItem? remove(String key) => _data.remove(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;
Expand Down
34 changes: 33 additions & 1 deletion lib/src/kdbx_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_consts.dart';
Expand Down Expand Up @@ -153,16 +154,47 @@ class KdbxFile {
/// 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.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
3 changes: 2 additions & 1 deletion lib/src/kdbx_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -317,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
20 changes: 7 additions & 13 deletions lib/src/kdbx_meta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -278,22 +278,12 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
final otherIsNewer = other.settingsChanged.isAfter(settingsChanged);

// merge custom data
// for (final otherCustomDataEntry in other.customData.entries) {
// if ((otherIsNewer || !customData.containsKey(otherCustomDataEntry.key)) &&
// !ctx.deletedObjects.containsKey(otherCustomDataEntry.key)) {
// customData[otherCustomDataEntry.key] = otherCustomDataEntry.value;
// }
// }
mergeKdbxMetaCustomDataWithDates(
customData, other.customData, ctx, otherIsNewer);

mergeCustomIconsWithDates(_customIcons, other._customIcons, ctx);
// merge custom icons
// Unused icons will be cleaned up later
// //TODO: Use modified dates for better merging?
// for (final otherCustomIcon in other._customIcons.values) {
// _customIcons[otherCustomIcon.uuid] ??= otherCustomIcon;
// }
mergeCustomIconsWithDates(_customIcons, other._customIcons, ctx);

if (other.entryTemplatesGroupChanged.isAfter(entryTemplatesGroupChanged)) {
entryTemplatesGroup.set(other.entryTemplatesGroup.get());
Expand Down Expand Up @@ -597,8 +587,12 @@ class BrowserDbSettings {
}

class KdbxCustomIcon {
KdbxCustomIcon(
{required this.uuid, required this.data, this.name, this.lastModified});
KdbxCustomIcon({
required this.uuid,
required this.data,
this.name,
this.lastModified,
});

/// uuid of the icon, must be unique within each file.
final KdbxUuid uuid;
Expand Down
7 changes: 7 additions & 0 deletions lib/src/kdbx_xml.dart
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ abstract class KdbxSubTextNode<T> extends KdbxSubNode<T?> {
String toString() {
return '$runtimeType{${_opt(name)?.text}}';
}

void upgrade() {
final T? value = get();
if (value != null) {
_opt(name)?.innerText = encode(value) ?? '';
}
}
}

class IntNode extends KdbxSubTextNode<int?> {
Expand Down
32 changes: 1 addition & 31 deletions test/kdbx4_1_test.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
@Tags(['kdbx4_1'])

import 'dart:io';

import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:test/test.dart';

import 'internal/test_utils.dart';

final _logger = Logger('kdbx4_1_test');
Expand All @@ -21,32 +17,8 @@ void main() {
if (!kdbxFormat.argon2.isFfi) {
throw StateError('Expected ffi!');
}
var now = DateTime.fromMillisecondsSinceEpoch(0);

final fakeClock = Clock(() => now);
void proceedSeconds(int seconds) {
now = now.add(Duration(seconds: seconds));
}

setUp(() {
now = DateTime.fromMillisecondsSinceEpoch(0);
});

group('11111111111111', () {
// test('TODO', () async {
// final file = await TestUtil.readKdbxFile('test/kdbx4.kdbx');
// //final file = await TestUtil.readKdbxFile('test/kdbx4_1.kdbx');
// final firstEntry = file.body.rootGroup.entries.first;
// final d = DateTime.utc(2020, 4, 5, 10, 0);
// firstEntry.times.lastModificationTime.set(d);
// final saved = await file.save();
// {
// final file2 = await TestUtil.readKdbxFileBytes(saved);
// final firstEntry = file2.body.rootGroup.entries.first;
// expect(firstEntry.times.lastModificationTime.get(), d);
// }
// });

group('Kdbx v4.1', () {
// Probably should do similar to make v3 more robust too but we don't use that and there's no risk of regression so not now.
test('New features fail on v4.0', () async {
final credentials = Credentials(ProtectedValue.fromString('asdf'));
Expand All @@ -66,7 +38,6 @@ void main() {

final loadedKdbx = await kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf')));
//final file2 = await TestUtil.readKdbxFileBytes(saved);

_logger.fine('Successfully loaded kdbx $loadedKdbx');
final entry1 = loadedKdbx.body.rootGroup.entries.first;
Expand All @@ -92,7 +63,6 @@ void main() {

final loadedKdbx = await kdbxFormat.read(
saved, Credentials(ProtectedValue.fromString('asdf')));
//final file2 = await TestUtil.readKdbxFileBytes(saved);

_logger.fine('Successfully loaded kdbx $loadedKdbx');
final firstEntry = loadedKdbx.body.rootGroup.entries.first;
Expand Down
58 changes: 58 additions & 0 deletions test/kdbx_upgrade_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@Tags(['kdbx3', 'kdbx4'])

import 'dart:convert';

import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/internal/extension_utils.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:test/test.dart';

import 'internal/test_utils.dart';
Expand Down Expand Up @@ -38,10 +42,64 @@ void main() {
expect(v4.header.version, KdbxVersion.V4_1);
await TestUtil.saveTestOutput('kdbx4upgrade4-41', v4);
}, tags: 'kdbx4');

test('kdbx4.1 is the new default', () async {
final file =
format.create(Credentials(ProtectedValue.fromString('asdf')), 'test');
expect(file.header.version, KdbxVersion.V4_1);
});

test('Upgrade from < v4 transforms persisted date format', () async {
final file =
await TestUtil.readKdbxFile('test/FooBar.kdbx', password: 'FooBar');
expect(file.header.version, KdbxVersion.V3_1);
file.upgrade(KdbxVersion.V4.major, 1);
final v4 = await TestUtil.saveAndRead(await TestUtil.saveAndRead(file));
expect(v4.header.version, KdbxVersion.V4_1);

final metaValues = [
v4.body.meta.node.singleElement('DatabaseNameChanged')?.text,
v4.body.meta.node.singleElement('DatabaseDescriptionChanged')?.text,
v4.body.meta.node.singleElement('DefaultUserNameChanged')?.text,
v4.body.meta.node.singleElement('MasterKeyChanged')?.text,
v4.body.meta.node.singleElement('RecycleBinChanged')?.text,
v4.body.meta.node.singleElement('EntryTemplatesGroupChanged')?.text,
v4.body.meta.node.singleElement('SettingsChanged')?.text,
];
metaValues.forEach(checkIsBase64Date);

v4.body.rootGroup
.getAllEntries()
.values
.forEach(checkObjectHasBase64Dates);
v4.body.rootGroup
.getAllGroups()
.values
.forEach(checkObjectHasBase64Dates);
}, tags: 'kdbx4');
}, tags: ['kdbx4']);
}

// Sometimes the nodes can contain an XmlNodeList with a single element, rather than directly containing an XmlText node. Bug in XML lib?
// Have to work around by using deprecated text property which works no matter which approach the library decides to take this time.
void checkObjectHasBase64Dates(KdbxObject? obj) {
if (obj != null) {
[
obj.times.node.singleElement('CreationTime')?.text,
obj.times.node.singleElement('LastModificationTime')?.text,
obj.times.node.singleElement('LastAccessTime')?.text,
obj.times.node.singleElement('ExpiryTime')?.text,
obj.times.node.singleElement('LocationChanged')?.text,
].forEach(checkIsBase64Date);

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

void checkIsBase64Date(String? val) {
if (val != null) {
expect(DateTimeUtils.fromBase64(val), isA<DateTime>());
}
}
21 changes: 21 additions & 0 deletions test/merge/kdbx_merge_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,26 @@ void main() {
);
});

// We don't prevent merging into a newer KDBX version since that might be the only
// way to avoid permanent merge failures. However, updating each DB to the latest
// version before merging is probably safest, especially for major version differences.
test('Generates merge error when merging into an older KDBX version',
() async {
final file = await TestUtil.createRealFile(proceedSeconds);
final file2 = await TestUtil.saveAndRead(file);
file.header.upgradeMinor(4, 0);
expect(
() => file.merge(file2),
throwsA(
isA<KdbxUnsupportedException>().having(
(error) => error.hint,
'hint',
'Kdbx version of source is newer. Upgrade file version before attempting to merge.',
),
),
);
});

test('Local v4.0 file gets all custom icons', () async {
await withClock(fakeClock, () async {
final file = await TestUtil.createRealFile(proceedSeconds);
Expand Down Expand Up @@ -290,6 +310,7 @@ void main() {
expect(sutIcon5?.lastModified, null);
});
});

test(
'Local v4.1 file gets all custom icons and new modified date for merged icon',
() async {
Expand Down

0 comments on commit 3ad19ea

Please sign in to comment.