You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
8.4 KiB
228 lines
8.4 KiB
import 'dart:convert'; |
|
import 'dart:typed_data'; |
|
|
|
import 'package:collection/collection.dart'; |
|
import 'package:kdbx/src/internal/extension_utils.dart'; |
|
import 'package:kdbx/src/kdbx_binary.dart'; |
|
import 'package:kdbx/src/kdbx_custom_data.dart'; |
|
import 'package:kdbx/src/kdbx_exceptions.dart'; |
|
import 'package:kdbx/src/kdbx_format.dart'; |
|
import 'package:kdbx/src/kdbx_header.dart'; |
|
import 'package:kdbx/src/kdbx_object.dart'; |
|
import 'package:kdbx/src/kdbx_xml.dart'; |
|
import 'package:logging/logging.dart'; |
|
import 'package:quiver/iterables.dart'; |
|
import 'package:xml/xml.dart' as xml; |
|
import 'package:xml/xml.dart'; |
|
|
|
final _logger = Logger('kdbx_meta'); |
|
|
|
class KdbxMeta extends KdbxNode implements KdbxNodeContext { |
|
KdbxMeta.create({ |
|
required String databaseName, |
|
required this.ctx, |
|
String? generator, |
|
}) : customData = KdbxCustomData.create(), |
|
binaries = [], |
|
_customIcons = {}, |
|
super.create('Meta') { |
|
this.databaseName.set(databaseName); |
|
databaseDescription.set(null, force: true); |
|
defaultUserName.set(null, force: true); |
|
this.generator.set(generator ?? 'kdbx.dart'); |
|
settingsChanged.setToNow(); |
|
masterKeyChanged.setToNow(); |
|
recycleBinChanged.setToNow(); |
|
historyMaxItems.set(Consts.DefaultHistoryMaxItems); |
|
historyMaxSize.set(Consts.DefaultHistoryMaxSize); |
|
} |
|
|
|
KdbxMeta.read(xml.XmlElement node, this.ctx) |
|
: customData = node |
|
.singleElement(KdbxXml.NODE_CUSTOM_DATA) |
|
?.let((e) => KdbxCustomData.read(e)) ?? |
|
KdbxCustomData.create(), |
|
binaries = node |
|
.singleElement(KdbxXml.NODE_BINARIES) |
|
?.let((el) sync* { |
|
for (final binaryNode in el.findElements(KdbxXml.NODE_BINARY)) { |
|
final id = int.parse(binaryNode.getAttribute(KdbxXml.ATTR_ID)!); |
|
yield MapEntry( |
|
id, |
|
KdbxBinary.readBinaryXml(binaryNode, isInline: false), |
|
); |
|
} |
|
}) |
|
.toList() |
|
.let((binaries) { |
|
binaries.sort((a, b) => a.key.compareTo(b.key)); |
|
for (var i = 0; i < binaries.length; i++) { |
|
if (i != binaries[i].key) { |
|
throw KdbxCorruptedFileException( |
|
'Invalid ID for binary. expected $i,' |
|
' but was ${binaries[i].key}'); |
|
} |
|
} |
|
return binaries.map((e) => e.value).toList(); |
|
}), |
|
_customIcons = node |
|
.singleElement(KdbxXml.NODE_CUSTOM_ICONS) |
|
?.let((el) sync* { |
|
for (final iconNode in el.findElements(KdbxXml.NODE_ICON)) { |
|
yield KdbxCustomIcon( |
|
uuid: KdbxUuid( |
|
iconNode.singleTextNode(KdbxXml.NODE_UUID)), |
|
data: base64.decode( |
|
iconNode.singleTextNode(KdbxXml.NODE_DATA))); |
|
} |
|
}) |
|
.map((e) => MapEntry(e.uuid, e)) |
|
.let((that) => Map.fromEntries(that)) ?? |
|
{}, |
|
super.read(node); |
|
|
|
@override |
|
final KdbxReadWriteContext ctx; |
|
|
|
final KdbxCustomData customData; |
|
|
|
/// only used in Kdbx 3 |
|
final List<KdbxBinary>? binaries; |
|
|
|
final Map<KdbxUuid?, KdbxCustomIcon> _customIcons; |
|
|
|
Map<KdbxUuid?, KdbxCustomIcon> get customIcons => |
|
UnmodifiableMapView(_customIcons); |
|
|
|
void addCustomIcon(KdbxCustomIcon customIcon) { |
|
if (_customIcons.containsKey(customIcon.uuid)) { |
|
return; |
|
} |
|
modify(() => _customIcons[customIcon.uuid] = customIcon); |
|
} |
|
|
|
StringNode get generator => StringNode(this, 'Generator'); |
|
|
|
StringNode get databaseName => StringNode(this, 'DatabaseName') |
|
..setOnModifyListener(() => databaseNameChanged.setToNow()); |
|
DateTimeUtcNode get databaseNameChanged => |
|
DateTimeUtcNode(this, 'DatabaseNameChanged'); |
|
|
|
StringNode get databaseDescription => StringNode(this, 'DatabaseDescription') |
|
..setOnModifyListener(() => databaseDescriptionChanged.setToNow()); |
|
DateTimeUtcNode get databaseDescriptionChanged => |
|
DateTimeUtcNode(this, 'DatabaseDescriptionChanged'); |
|
|
|
StringNode get defaultUserName => StringNode(this, 'DefaultUserName') |
|
..setOnModifyListener(() => defaultUserNameChanged.setToNow()); |
|
DateTimeUtcNode get defaultUserNameChanged => |
|
DateTimeUtcNode(this, 'DefaultUserNameChanged'); |
|
|
|
DateTimeUtcNode get masterKeyChanged => |
|
DateTimeUtcNode(this, 'MasterKeyChanged'); |
|
|
|
Base64Node get headerHash => Base64Node(this, 'HeaderHash'); |
|
|
|
BooleanNode get recycleBinEnabled => BooleanNode(this, 'RecycleBinEnabled'); |
|
|
|
UuidNode get recycleBinUUID => UuidNode(this, 'RecycleBinUUID') |
|
..setOnModifyListener(() => recycleBinChanged.setToNow()); |
|
|
|
DateTimeUtcNode get settingsChanged => |
|
DateTimeUtcNode(this, 'SettingsChanged'); |
|
|
|
DateTimeUtcNode get recycleBinChanged => |
|
DateTimeUtcNode(this, 'RecycleBinChanged'); |
|
|
|
UuidNode get entryTemplatesGroup => UuidNode(this, 'EntryTemplatesGroup') |
|
..setOnModifyListener(() => entryTemplatesGroupChanged.setToNow()); |
|
DateTimeUtcNode get entryTemplatesGroupChanged => |
|
DateTimeUtcNode(this, 'EntryTemplatesGroupChanged'); |
|
|
|
IntNode get historyMaxItems => IntNode(this, 'HistoryMaxItems'); |
|
|
|
/// max size of history in bytes. |
|
IntNode get historyMaxSize => IntNode(this, 'HistoryMaxSize'); |
|
|
|
/// not sure what this node is supposed to do actually. |
|
IntNode get maintenanceHistoryDays => IntNode(this, 'MaintenanceHistoryDays'); |
|
|
|
// void addCustomIcon |
|
|
|
@override |
|
xml.XmlElement toXml() { |
|
final ret = super.toXml()..replaceSingle(customData.toXml()); |
|
XmlUtils.removeChildrenByName(ret, KdbxXml.NODE_BINARIES); |
|
// with kdbx >= 4 we assume the binaries were already written in the header. |
|
if (ctx.versionMajor < 4) { |
|
ret.children.add( |
|
XmlElement(XmlName(KdbxXml.NODE_BINARIES)) |
|
..children.addAll( |
|
enumerate(ctx.binariesIterable).map((indexed) { |
|
final xmlBinary = XmlUtils.createNode(KdbxXml.NODE_BINARY) |
|
..addAttribute(KdbxXml.ATTR_ID, indexed.index.toString()); |
|
indexed.value.saveToXml(xmlBinary); |
|
return xmlBinary; |
|
}), |
|
), |
|
); |
|
} |
|
XmlUtils.removeChildrenByName(ret, KdbxXml.NODE_CUSTOM_ICONS); |
|
ret.children.add( |
|
XmlElement(XmlName(KdbxXml.NODE_CUSTOM_ICONS)) |
|
..children.addAll(customIcons.values.map( |
|
(e) => XmlUtils.createNode(KdbxXml.NODE_ICON, [ |
|
XmlUtils.createTextNode(KdbxXml.NODE_UUID, e.uuid.uuid), |
|
XmlUtils.createTextNode(KdbxXml.NODE_DATA, base64.encode(e.data)) |
|
]), |
|
)), |
|
); |
|
return ret; |
|
} |
|
|
|
// Merge in changes in [other] into this meta data. |
|
void merge(KdbxMeta other) { |
|
// FIXME make sure this is finished |
|
if (other.databaseNameChanged.isAfter(databaseNameChanged)) { |
|
databaseName.set(other.databaseName.get()); |
|
databaseNameChanged.set(other.databaseNameChanged.get()); |
|
} |
|
if (other.databaseDescriptionChanged.isAfter(databaseDescriptionChanged)) { |
|
databaseDescription.set(other.databaseDescription.get()); |
|
databaseDescriptionChanged.set(other.databaseDescriptionChanged.get()); |
|
} |
|
if (other.defaultUserNameChanged.isAfter(defaultUserNameChanged)) { |
|
defaultUserName.set(other.defaultUserName.get()); |
|
defaultUserNameChanged.set(other.defaultUserNameChanged.get()); |
|
} |
|
if (other.masterKeyChanged.isAfter(masterKeyChanged)) { |
|
// throw UnimplementedError( |
|
// 'Other database changed master key. not supported.'); |
|
_logger.shout('MasterKey was changed? We will not merge this (yet).'); |
|
} |
|
if (other.recycleBinChanged.isAfter(recycleBinChanged)) { |
|
recycleBinEnabled.set(other.recycleBinEnabled.get()); |
|
recycleBinUUID.set(other.recycleBinUUID.get()); |
|
recycleBinChanged.set(other.recycleBinChanged.get()); |
|
} |
|
final otherIsNewer = other.settingsChanged.isAfter(settingsChanged); |
|
// merge custom data |
|
customData.merge(other.customData, otherIsNewer); |
|
// merge custom icons |
|
for (final otherCustomIcon in other._customIcons.values) { |
|
_customIcons[otherCustomIcon.uuid] ??= otherCustomIcon; |
|
} |
|
|
|
settingsChanged.set(other.settingsChanged.get()); |
|
} |
|
} |
|
|
|
class KdbxCustomIcon { |
|
KdbxCustomIcon({required this.uuid, required this.data}); |
|
|
|
/// uuid of the icon, must be unique within each file. |
|
final KdbxUuid uuid; |
|
|
|
/// Encoded png data of the image. will be base64 encoded into the kdbx file. |
|
final Uint8List data; |
|
}
|
|
|