diff --git a/example/pubspec.lock b/example/pubspec.lock index 22a1bc3..9637e5a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -117,7 +117,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "3.0.0" pointycastle: dependency: transitive description: @@ -192,6 +192,6 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.7.0" sdks: - dart: ">=2.4.0 <3.0.0" + dart: ">=2.7.0 <3.0.0" diff --git a/lib/kdbx.dart b/lib/kdbx.dart index b2c5458..c2630c1 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -4,6 +4,7 @@ library kdbx; export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_consts.dart'; +export 'src/kdbx_custom_data.dart'; export 'src/kdbx_entry.dart'; export 'src/kdbx_format.dart'; export 'src/kdbx_header.dart' @@ -12,4 +13,5 @@ export 'src/kdbx_header.dart' KdbxInvalidKeyException, KdbxCorruptedFileException, KdbxUnsupportedException; +export 'src/kdbx_meta.dart'; export 'src/kdbx_object.dart'; diff --git a/lib/src/internal/extension_utils.dart b/lib/src/internal/extension_utils.dart new file mode 100644 index 0000000..0094721 --- /dev/null +++ b/lib/src/internal/extension_utils.dart @@ -0,0 +1,35 @@ +import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:xml/xml.dart' as xml; + +extension XmlElementExt on xml.XmlElement { + xml.XmlElement singleElement(String nodeName, + {xml.XmlElement Function() orElse}) { + final elements = findElements(nodeName); + if (elements.isEmpty) { + if (orElse != null) { + final ret = orElse(); + children.add(ret); + return ret; + } else { + return null; + } + } + return elements.single; + } + + String singleTextNode(String nodeName) { + return findElements(nodeName).single.text; + } + + /// If an element child with the given name already exists, + /// it will be removed and the given element will be added. + /// otherwise it will be only added. + void replaceSingle(xml.XmlElement element) { + XmlUtils.removeChildrenByName(this, element.name.local); + children.add(element); + } +} + +extension ObjectExt on T { + R let(R Function(T that) op) => op(this); +} diff --git a/lib/src/kdbx_custom_data.dart b/lib/src/kdbx_custom_data.dart new file mode 100644 index 0000000..4935c92 --- /dev/null +++ b/lib/src/kdbx_custom_data.dart @@ -0,0 +1,37 @@ +import 'package:kdbx/src/kdbx_object.dart'; +import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:xml/xml.dart' as xml; +import 'package:kdbx/src/internal/extension_utils.dart'; + +class KdbxCustomData extends KdbxNode { + KdbxCustomData.create() + : data = {}, + super.create(TAG_NAME); + + KdbxCustomData.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); + return MapEntry(key, value); + })), + super.read(node); + + static const String TAG_NAME = 'CustomData'; + + final Map data; + + @override + xml.XmlElement toXml() { + final el = super.toXml(); + el.children.clear(); + el.children.addAll( + data.entries + .map((e) => XmlUtils.createNode(KdbxXml.NODE_CUSTOM_DATA_ITEM, [ + XmlUtils.createTextNode(KdbxXml.NODE_KEY, e.key), + XmlUtils.createTextNode(KdbxXml.NODE_VALUE, e.value), + ])), + ); + return el; + } +} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index b9cea35..4129447 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -11,13 +11,14 @@ import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_header.dart'; +import 'package:kdbx/src/kdbx_meta.dart'; +import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:pointycastle/export.dart'; import 'package:xml/xml.dart' as xml; - -import 'kdbx_object.dart'; +import 'package:kdbx/src/internal/extension_utils.dart'; final _logger = Logger('kdbx.format'); @@ -37,6 +38,7 @@ abstract class Credentials { class KeyFileComposite implements Credentials { KeyFileComposite({@required this.password, @required this.keyFile}); + PasswordCredentials password; KeyFileCredentials keyFile; @@ -79,6 +81,7 @@ class KeyFileCredentials implements CredentialsPart { return KeyFileCredentials._(ProtectedValue.fromBinary(bytes)); } } + KeyFileCredentials._(this._keyFileValue); static final RegExp _hexValuePattern = RegExp(r'/^[a-f\d]{64}$/i'); @@ -136,6 +139,7 @@ class KdbxFile { final Set dirtyObjects = {}; final StreamController> _dirtyObjectsChanged = StreamController>.broadcast(); + Stream> get dirtyObjectsChanged => _dirtyObjectsChanged.stream; @@ -273,28 +277,13 @@ class KdbxBody extends KdbxNode { } } -class KdbxMeta extends KdbxNode { - KdbxMeta.create({@required String databaseName}) : super.create('Meta') { - this.databaseName.set(databaseName); - } - - KdbxMeta.read(xml.XmlElement node) : super.read(node); - - StringNode get databaseName => StringNode(this, 'DatabaseName'); - - Base64Node get headerHash => Base64Node(this, 'HeaderHash'); - - @override - // ignore: unnecessary_overrides - xml.XmlElement toXml() { - return super.toXml(); - } -} - class KdbxFormat { static KdbxFile create(Credentials credentials, String name) { final header = KdbxHeader.create(); - final meta = KdbxMeta.create(databaseName: name); + final meta = KdbxMeta.create( + databaseName: name, + generator: 'AuthPass', + ); final rootGroup = KdbxGroup.create(parent: null, name: name); final body = KdbxBody.create(meta, rootGroup); return KdbxFile(credentials, header, body); diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart new file mode 100644 index 0000000..42661e2 --- /dev/null +++ b/lib/src/kdbx_meta.dart @@ -0,0 +1,42 @@ +import 'package:kdbx/src/internal/extension_utils.dart'; +import 'package:kdbx/src/kdbx_custom_data.dart'; +import 'package:kdbx/src/kdbx_object.dart'; +import 'package:kdbx/src/kdbx_xml.dart'; +import 'package:meta/meta.dart'; +import 'package:xml/xml.dart' as xml; + +class KdbxMeta extends KdbxNode { + KdbxMeta.create({ + @required String databaseName, + String generator = 'kdbx.dart', + }) : customData = KdbxCustomData.create(), + super.create('Meta') { + this.databaseName.set(databaseName); + this.generator.set(generator); + } + + KdbxMeta.read(xml.XmlElement node) + : customData = node + .singleElement('CustomData') + ?.let((e) => KdbxCustomData.read(e)) ?? + KdbxCustomData.create(), + super.read(node); + + final KdbxCustomData customData; + + StringNode get generator => StringNode(this, 'Generator'); + + StringNode get databaseName => StringNode(this, 'DatabaseName'); + + Base64Node get headerHash => Base64Node(this, 'HeaderHash'); + + BooleanNode get recycleBinEnabled => BooleanNode(this, 'RecycleBinEnabled'); + + UuidNode get recycleBinUUID => UuidNode(this, 'RecycleBinUUID'); + + DateTimeUtcNode get recycleBinChanged => + DateTimeUtcNode(this, 'RecycleBinChanged'); + + @override + xml.XmlElement toXml() => super.toXml()..replaceSingle(customData.toXml()); +} diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index f312e96..7745b40 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -12,6 +12,8 @@ class KdbxXml { static const NODE_VALUE = 'Value'; static const ATTR_PROTECTED = 'Protected'; static const NODE_HISTORY = 'History'; + + static const NODE_CUSTOM_DATA_ITEM = 'Item'; } abstract class KdbxSubNode { @@ -163,6 +165,15 @@ class XmlUtils { node.children .removeWhere((node) => node is XmlElement && node.name.local == name); } + + static XmlElement createTextNode(String localName, String value) => + createNode(localName, [XmlText(value)]); + + static XmlElement createNode( + String localName, [ + List children = const [], + ]) => + XmlElement(XmlName(localName))..children.addAll(children); } class DateTimeUtils { diff --git a/pubspec.yaml b/pubspec.yaml index c43f85b..646d3c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,16 +4,16 @@ version: 0.2.1 homepage: https://github.com/authpass/kdbx.dart environment: - sdk: '>=2.4.0 <3.0.0' + sdk: '>=2.7.0 <3.0.0' dependencies: - flutter: - sdk: flutter +# flutter: +# sdk: flutter # path: ^1.6.0 logging: '>=0.11.3+2 <1.0.0' crypto: '>=2.0.0 <3.0.0' pointycastle: '>=1.0.1 <2.0.0' - xml: '>=3.5.0 <4.0.0' + xml: '>=3.7.0 <4.0.0' uuid: '>=2.0.0 <3.0.0' meta: '>=1.0.0 <2.0.0' clock: '>=1.0.0 <2.0.0' @@ -27,8 +27,6 @@ dependencies: logging_appenders: '>=0.1.0 <1.0.0' dev_dependencies: - flutter_test: - sdk: flutter pedantic: '>=1.7.0 <2.0.0' test: '>=1.6.0 <2.0.0' diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index 30f27cc..d34a145 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -37,7 +37,7 @@ void main() { ProtectedValue.fromString('asdf'), keyFileBytes); final data = await File('test/password-and-keyfile.kdbx').readAsBytes(); final file = KdbxFormat.read(data, cred); - expect(file.body.rootGroup.entries.length, 1); + expect(file.body.rootGroup.entries, hasLength(2)); }); });