diff --git a/example/pubspec.lock b/example/pubspec.lock index 2e98f26..75ae5b8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -117,7 +117,7 @@ packages: path: ".." relative: true source: path - version: "0.3.0" + version: "0.3.1" logging: dependency: transitive description: diff --git a/lib/kdbx.dart b/lib/kdbx.dart index a97fdeb..0e22864 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -8,6 +8,7 @@ export 'src/kdbx_consts.dart'; export 'src/kdbx_custom_data.dart'; export 'src/kdbx_dao.dart' show KdbxDao; export 'src/kdbx_entry.dart'; +export 'src/kdbx_file.dart'; export 'src/kdbx_format.dart'; export 'src/kdbx_group.dart'; export 'src/kdbx_header.dart' diff --git a/lib/src/kdbx_dao.dart b/lib/src/kdbx_dao.dart index 1664635..46c0f06 100644 --- a/lib/src/kdbx_dao.dart +++ b/lib/src/kdbx_dao.dart @@ -1,4 +1,5 @@ import 'package:kdbx/kdbx.dart'; +import 'package:kdbx/src/kdbx_file.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; @@ -32,28 +33,6 @@ extension KdbxDao on KdbxFile { return group; } - /// Returns the recycle bin, if it exists, null otherwise. - KdbxGroup get recycleBin { - final uuid = body.meta.recycleBinUUID.get(); - if (uuid?.isNil != false) { - return null; - } - try { - return findGroupByUuid(uuid); - } catch (e, stackTrace) { - _logger.warning(() { - final groupDebug = body.rootGroup - .getAllGroups() - .map((g) => '${g.uuid}: ${g.name}') - .join('\n'); - return 'All Groups: $groupDebug'; - }); - _logger.severe('Inconsistency error, uuid $uuid not found in groups.', e, - stackTrace); - rethrow; - } - } - KdbxGroup getRecycleBinOrCreate() { return recycleBin ?? _createRecycleBin(); } diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index 59f773c..c1e32f0 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -1,6 +1,6 @@ import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/kdbx_consts.dart'; -import 'package:kdbx/src/kdbx_format.dart'; +import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_xml.dart'; diff --git a/lib/src/kdbx_file.dart b/lib/src/kdbx_file.dart new file mode 100644 index 0000000..16926c4 --- /dev/null +++ b/lib/src/kdbx_file.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:kdbx/src/crypto/protected_value.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:xml/xml.dart' as xml; +import 'package:kdbx/src/kdbx_dao.dart'; + +final _logger = Logger('kdbx_file'); + +class KdbxFile { + KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) { + for (final obj in _allObjects) { + obj.file = this; + } + } + + static final protectedValues = Expando(); + + static ProtectedValue protectedValueForNode(xml.XmlElement node) { + return protectedValues[node]; + } + + static void setProtectedValueForNode( + xml.XmlElement node, ProtectedValue value) { + protectedValues[node] = value; + } + + final KdbxFormat kdbxFormat; + final Credentials credentials; + final KdbxHeader header; + final KdbxBody body; + final Set dirtyObjects = {}; + final StreamController> _dirtyObjectsChanged = + StreamController>.broadcast(); + + Stream> get dirtyObjectsChanged => + _dirtyObjectsChanged.stream; + + Future save() async { + return kdbxFormat.save(this); + } + + /// Marks all dirty objects as clean. Called by [KdbxFormat.save]. + void onSaved() { + dirtyObjects.clear(); + _dirtyObjectsChanged.add(dirtyObjects); + } + + Iterable get _allObjects => body.rootGroup + .getAllGroups() + .cast() + .followedBy(body.rootGroup.getAllEntries()); + + void dirtyObject(KdbxObject kdbxObject) { + dirtyObjects.add(kdbxObject); + _dirtyObjectsChanged.add(dirtyObjects); + } + + void dispose() { + _dirtyObjectsChanged.close(); + } + + KdbxGroup _recycleBin; + + /// Returns the recycle bin, if it exists, null otherwise. + KdbxGroup get recycleBin => _recycleBin ??= _findRecycleBin(); + + KdbxGroup _findRecycleBin() { + final uuid = body.meta.recycleBinUUID.get(); + if (uuid?.isNil != false) { + return null; + } + try { + return findGroupByUuid(uuid); + } catch (e, stackTrace) { + _logger.warning(() { + final groupDebug = body.rootGroup + .getAllGroups() + .map((g) => '${g.uuid}: ${g.name}') + .join('\n'); + return 'All Groups: $groupDebug'; + }); + _logger.severe('Inconsistency error, uuid $uuid not found in groups.', e, + stackTrace); + rethrow; + } + } + +// void _subscribeToChildren() { +// final allObjects = _allObjects; +// for (final obj in allObjects) { +// _subscriptions.handle(obj.changes.listen((event) { +// if (event.isDirty) { +// isDirty = true; +// if (event.object is KdbxGroup) { +// Future(() { +// // resubscribe, just in case some child groups/entries have changed. +// _subscriptions.cancelSubscriptions(); +// _subscribeToChildren(); +// }); +// } +// } +// })); +// } +// } +} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 863dc23..ee50301 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -14,6 +14,7 @@ import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; +import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_meta.dart'; @@ -120,98 +121,6 @@ class HashCredentials implements Credentials { Uint8List getHash() => hash; } -class KdbxFile { - KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) { - for (final obj in _allObjects) { - obj.file = this; - } - } - - static final protectedValues = Expando(); - - static ProtectedValue protectedValueForNode(xml.XmlElement node) { - return protectedValues[node]; - } - - static void setProtectedValueForNode( - xml.XmlElement node, ProtectedValue value) { - protectedValues[node] = value; - } - - final KdbxFormat kdbxFormat; - final Credentials credentials; - final KdbxHeader header; - final KdbxBody body; - final Set dirtyObjects = {}; - final StreamController> _dirtyObjectsChanged = - StreamController>.broadcast(); - - Stream> get dirtyObjectsChanged => - _dirtyObjectsChanged.stream; - - Future save() async { - final output = BytesBuilder(); - final writer = WriterHelper(output); - header.generateSalts(); - header.write(writer); - final headerHash = - (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List); - - if (header.versionMajor <= 3) { - final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes; - final gen = ProtectedSaltGenerator(streamKey); - - body.meta.headerHash.set(headerHash.buffer); - await body.writeV3(writer, this, gen); - } else if (header.versionMajor <= 4) { - final headerBytes = writer.output.toBytes(); - writer.writeBytes(headerHash); - final gen = kdbxFormat._createProtectedSaltGenerator(header); - final keys = await kdbxFormat._computeKeysV4(header, credentials); - final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey); - writer.writeBytes(headerHmac.bytes as Uint8List); - body.writeV4(writer, this, gen, keys); - } else { - throw UnsupportedError('Unsupported version ${header.versionMajor}'); - } - dirtyObjects.clear(); - _dirtyObjectsChanged.add(dirtyObjects); - return output.toBytes(); - } - - Iterable get _allObjects => body.rootGroup - .getAllGroups() - .cast() - .followedBy(body.rootGroup.getAllEntries()); - - void dirtyObject(KdbxObject kdbxObject) { - dirtyObjects.add(kdbxObject); - _dirtyObjectsChanged.add(dirtyObjects); - } - - void dispose() { - _dirtyObjectsChanged.close(); - } - -// void _subscribeToChildren() { -// final allObjects = _allObjects; -// for (final obj in allObjects) { -// _subscriptions.handle(obj.changes.listen((event) { -// if (event.isDirty) { -// isDirty = true; -// if (event.object is KdbxGroup) { -// Future(() { -// // resubscribe, just in case some child groups/entries have changed. -// _subscriptions.cancelSubscriptions(); -// _subscribeToChildren(); -// }); -// } -// } -// })); -// } -// } -} - class KdbxBody extends KdbxNode { KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') { node.children.add(meta.node); @@ -379,6 +288,39 @@ class KdbxFormat { } } + Future save(KdbxFile file) async { + final body = file.body; + final header = file.header; + + final output = BytesBuilder(); + final writer = WriterHelper(output); + header.generateSalts(); + header.write(writer); + final headerHash = + (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List); + + if (file.header.versionMajor <= 3) { + final streamKey = + file.header.fields[HeaderFields.ProtectedStreamKey].bytes; + final gen = ProtectedSaltGenerator(streamKey); + + body.meta.headerHash.set(headerHash.buffer); + await body.writeV3(writer, file, gen); + } else if (header.versionMajor <= 4) { + final headerBytes = writer.output.toBytes(); + writer.writeBytes(headerHash); + final gen = _createProtectedSaltGenerator(header); + final keys = await _computeKeysV4(header, file.credentials); + final headerHmac = _getHeaderHmac(headerBytes, keys.hmacKey); + writer.writeBytes(headerHmac.bytes as Uint8List); + body.writeV4(writer, file, gen, keys); + } else { + throw UnsupportedError('Unsupported version ${header.versionMajor}'); + } + file.onSaved(); + return output.toBytes(); + } + Future _loadV3( KdbxHeader header, ReaderHelper reader, Credentials credentials) async { // _getMasterKeyV3(header, credentials); diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index eeea3b6..8ba2348 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -150,7 +150,7 @@ class KdbxHeader { } void _validate() { - for (HeaderFields required in _requiredFields(versionMajor)) { + for (final required in _requiredFields(versionMajor)) { if (fields[required] == null) { throw KdbxCorruptedFileException('Missing header $required'); } @@ -351,14 +351,14 @@ class KdbxHeader { List fields, T createField(TE field, Uint8List bytes)) sync* { while (true) { final headerId = reader.readUint8(); - final int bodySize = + final bodySize = versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); // _logger.fine('Reading header with id $headerId (size: $bodySize)}'); final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; // _logger.finer( // 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); if (headerId > 0) { - final TE field = fields[headerId]; + final field = fields[headerId]; yield createField(field, bodyBytes); /* else { if (field == InnerHeaderFields.InnerRandomStreamID) { @@ -448,7 +448,7 @@ class HashedBlockReader { Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList()); static Iterable readNextBlock(ReaderHelper reader) sync* { - int expectedBlockIndex = 0; + var expectedBlockIndex = 0; while (true) { // ignore: unused_local_variable final blockIndex = reader.readUint32(); @@ -471,7 +471,7 @@ class HashedBlockReader { // static Uint8List writeBlocks(WriterHelper writer) => static void writeBlocks(ReaderHelper reader, WriterHelper writer) { - for (int blockIndex = 0;; blockIndex++) { + for (var blockIndex = 0;; blockIndex++) { final block = reader.readBytesUpTo(BLOCK_SIZE); if (block.lengthInBytes == 0) { // written all data, write a last empty block. diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index f40f692..65c5f97 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:kdbx/src/kdbx_format.dart'; +import 'package:kdbx/src/kdbx_file.dart'; import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_times.dart'; import 'package:kdbx/src/kdbx_xml.dart'; diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 690b427..1c42a15 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -200,12 +200,12 @@ class XmlUtils { class DateTimeUtils { static String toIso8601StringSeconds(DateTime dateTime) { - final String y = _fourDigits(dateTime.year); - final String m = _twoDigits(dateTime.month); - final String d = _twoDigits(dateTime.hour); - final String h = _twoDigits(dateTime.hour); - final String min = _twoDigits(dateTime.minute); - final String sec = _twoDigits(dateTime.second); + final y = _fourDigits(dateTime.year); + final m = _twoDigits(dateTime.month); + final d = _twoDigits(dateTime.hour); + final h = _twoDigits(dateTime.hour); + final min = _twoDigits(dateTime.minute); + final sec = _twoDigits(dateTime.second); return '$y-$m-${d}T$h:$min:${sec}Z'; }