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('CustomData') ?.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? binaries; final Map _customIcons; Map 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 for (final otherCustomDataEntry in other.customData.entries) { if (otherIsNewer || !customData.containsKey(otherCustomDataEntry.key)) { customData[otherCustomDataEntry.key] = otherCustomDataEntry.value; } } // 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; }