From b89cce0f740badb8331210c78bb728ab7db5b757 Mon Sep 17 00:00:00 2001 From: Herbert Poul Date: Tue, 27 Aug 2019 14:59:07 +0200 Subject: [PATCH] implemented dirty tracking, modification updates, etc. --- lib/src/internal/async_utils.dart | 29 ++++++++ lib/src/kdbx_format.dart | 112 ++++++++++++++++++++---------- lib/src/kdbx_group.dart | 14 +++- lib/src/kdbx_object.dart | 34 +++++++-- lib/src/kdbx_times.dart | 7 +- lib/src/kdbx_xml.dart | 1 + test/FooBar.kdbx | Bin 2078 -> 2430 bytes 7 files changed, 153 insertions(+), 44 deletions(-) create mode 100644 lib/src/internal/async_utils.dart diff --git a/lib/src/internal/async_utils.dart b/lib/src/internal/async_utils.dart new file mode 100644 index 0000000..3ee99fc --- /dev/null +++ b/lib/src/internal/async_utils.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +/// Base class which can be used as a mixin directly, but you have to call `cancelSubscriptions`. +/// If used inside a [State], use [StreamSubscriberMixin]. +mixin StreamSubscriberBase { + final List> _subscriptions = >[]; + + /// Listens to a stream and saves it to the list of subscriptions. + void listen(Stream stream, void onData(dynamic data), {Function onError}) { + if (stream != null) { + _subscriptions.add(stream.listen(onData, onError: onError)); + } + } + + void handle(StreamSubscription subscription) { + _subscriptions.add(subscription); + } + + /// Cancels all streams that were previously added with listen(). + void cancelSubscriptions() { + _subscriptions.forEach(_cancelSubscription); + _subscriptions.clear(); + } + + Future _cancelSubscription(StreamSubscription subscription) => + subscription.cancel(); +} + +class StreamSubscriptions with StreamSubscriberBase {} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 4bc77ee..dbf7550 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -6,6 +6,7 @@ import 'package:convert/convert.dart' as convert; import 'package:crypto/crypto.dart' as crypto; import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/internal/async_utils.dart'; import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/kdbx_group.dart'; @@ -34,8 +35,10 @@ class Credentials { } } -class KdbxFile { - KdbxFile(this.credentials, this.header, this.body); +class KdbxFile with Changeable { + KdbxFile(this.credentials, this.header, this.body) { + _subscribeToChildren(); + } static final protectedValues = Expando(); @@ -43,23 +46,57 @@ class KdbxFile { return protectedValues[node]; } - static void setProtectedValueForNode(xml.XmlElement node, ProtectedValue value) { + static void setProtectedValueForNode( + xml.XmlElement node, ProtectedValue value) { protectedValues[node] = value; } + final StreamSubscriptions _subscriptions = StreamSubscriptions(); + final Credentials credentials; final KdbxHeader header; final KdbxBody body; Uint8List save() { + assert(header.versionMajor == 3); final output = BytesBuilder(); final writer = WriterHelper(output); header.generateSalts(); header.write(writer); - body.write(writer, this); + + final streamKey = + header.fields[HeaderFields.ProtectedStreamKey].bytes.asUint8List(); + final gen = ProtectedSaltGenerator(streamKey); + + body.meta.headerHash.set( + (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) + .buffer); + body.writeV3(writer, this, gen); return output.toBytes(); } + Iterable get _allObjects => body.rootGroup + .getAllGroups() + .cast() + .followedBy(body.rootGroup.getAllEntries()); + + 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 { @@ -77,35 +114,32 @@ class KdbxBody extends KdbxNode { final KdbxMeta meta; final KdbxGroup rootGroup; - void write(WriterHelper writer, KdbxFile kdbxFile) { - assert(kdbxFile.header.versionMajor == 3); - final streamKey = kdbxFile - .header.fields[HeaderFields.ProtectedStreamKey].bytes - .asUint8List(); - final gen = ProtectedSaltGenerator(streamKey); - - _writeV3(writer, kdbxFile, gen); - } - - void _writeV3(WriterHelper writer, KdbxFile kdbxFile, + void writeV3(WriterHelper writer, KdbxFile kdbxFile, ProtectedSaltGenerator saltGenerator) { - meta.headerHash.set( - (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) - .buffer); final xml = generateXml(saltGenerator); final xmlBytes = utf8.encode(xml.toXmlString()); - final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ? - GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List; + final Uint8List compressedBytes = + (kdbxFile.header.compression == Compression.gzip + ? GZipCodec().encode(xmlBytes) + : xmlBytes) as Uint8List; + final encrypted = _encryptV3(kdbxFile, compressedBytes); + writer.writeBytes(encrypted); + } + + Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) { final byteWriter = WriterHelper(); - byteWriter.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List()); + byteWriter.writeBytes(kdbxFile + .header.fields[HeaderFields.StreamStartBytes].bytes + .asUint8List()); HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter); final bytes = byteWriter.output.toBytes(); - final masterKey = KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials); - final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List()); -// writer.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List()); - writer.writeBytes(encrypted); + final masterKey = + KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials); + final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, + kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List()); + return encrypted; } xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { @@ -124,12 +158,16 @@ class KdbxBody extends KdbxNode { } } - final builder = xml.XmlBuilder(); - builder.processing('xml', 'version="1.0" encoding="utf-8" standalone="yes"'); - builder.element('KeePassFile', nest: [ - meta.toXml(), - () => builder.element('Root', nest: rootGroupNode),],); + builder.processing( + 'xml', 'version="1.0" encoding="utf-8" standalone="yes"'); + builder.element( + 'KeePassFile', + nest: [ + meta.toXml(), + () => builder.element('Root', nest: rootGroupNode), + ], + ); // final doc = xml.XmlDocument(); // doc.children.add(xml.XmlProcessing( // 'xml', 'version="1.0" encoding="utf-8" standalone="yes"')); @@ -224,7 +262,8 @@ class KdbxFormat { final decryptCipher = CBCBlockCipher(AESFastEngine()); decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); - final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); + final paddedDecrypted = + AesHelper.processBlocks(decryptCipher, encryptedPayload); final decrypted = AesHelper.unpad(paddedDecrypted); final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; @@ -265,11 +304,12 @@ class KdbxFormat { return masterKey; } - static Uint8List _encryptDataAes(Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) { + static Uint8List _encryptDataAes( + Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) { final encryptCipher = CBCBlockCipher(AESFastEngine()); - encryptCipher.init(true, - ParametersWithIV(KeyParameter(masterKey), encryptionIv)); - return AesHelper.processBlocks(encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); - + encryptCipher.init( + true, ParametersWithIV(KeyParameter(masterKey), encryptionIv)); + return AesHelper.processBlocks( + encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); } } diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 180ea49..3a4d7f1 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -1,3 +1,4 @@ +import 'package:kdbx/src/internal/async_utils.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_xml.dart'; @@ -7,7 +8,8 @@ import 'package:xml/xml.dart'; import 'kdbx_object.dart'; class KdbxGroup extends KdbxObject { - KdbxGroup.create({@required this.parent, @required String name}) : super.create('Group') { + KdbxGroup.create({@required this.parent, @required String name}) + : super.create('Group') { this.name.set(name); icon.set(KdbxIcon.Folder); expanded.set(true); @@ -23,7 +25,9 @@ class KdbxGroup extends KdbxObject { .map((el) => KdbxEntry.read(this, el)) .forEach(_entries.add); } - + + final StreamSubscriptions _subscriptions = StreamSubscriptions(); + @override XmlElement toXml() { final el = super.toXml(); @@ -46,18 +50,22 @@ class KdbxGroup extends KdbxObject { /// null if this is the root group. final KdbxGroup parent; final List groups = []; + List get entries => List.unmodifiable(_entries); final List _entries = []; void addEntry(KdbxEntry entry) { if (entry.parent != this) { - throw StateError('Invalid operation. Trying to add entry which is already in another group.'); + throw StateError( + 'Invalid operation. Trying to add entry which is already in another group.'); } _entries.add(entry); node.children.add(entry.node); + isDirty = true; } StringNode get name => StringNode(this, 'Name'); + // String get name => text('Name') ?? ''; BooleanNode get expanded => BooleanNode(this, 'IsExpanded'); } diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 37b009f..78ea8af 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -8,8 +9,29 @@ import 'package:uuid/uuid.dart'; import 'package:uuid/uuid_util.dart'; import 'package:xml/xml.dart'; -abstract class KdbxNode { - KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)); +class ChangeEvent { + ChangeEvent({this.object, this.isDirty}); + + final T object; + final bool isDirty; +} + +mixin Changeable { + final _controller = StreamController>.broadcast(); + Stream> get changes => _controller.stream; + + bool _isDirty = false; + set isDirty(bool dirty) { + _isDirty = dirty; + _controller.add(ChangeEvent(object: this as T, isDirty: dirty)); + } + bool get isDirty => _isDirty; +} + +abstract class KdbxNode with Changeable { + KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) { + isDirty = true; + } KdbxNode.read(this.node); @@ -18,8 +40,6 @@ abstract class KdbxNode { // @protected // String text(String nodeName) => _opt(nodeName)?.text; - KdbxSubTextNode textNode(String nodeName) => StringNode(this, nodeName); - @mustCallSuper XmlElement toXml() { final el = node.copy() as XmlElement; @@ -42,6 +62,12 @@ abstract class KdbxObject extends KdbxNode { IconNode get icon => IconNode(this, 'IconID'); + @override + set isDirty(bool dirty) { + super.isDirty = dirty; + times.modifiedNow(); + } + @override XmlElement toXml() { final el = super.toXml(); diff --git a/lib/src/kdbx_times.dart b/lib/src/kdbx_times.dart index 888a665..598e118 100644 --- a/lib/src/kdbx_times.dart +++ b/lib/src/kdbx_times.dart @@ -27,6 +27,11 @@ class KdbxTimes extends KdbxNode { DateTimeUtcNode get locationChanged => DateTimeUtcNode(this, 'LocationChanged'); void accessedNow() { - lastAccessTime.set(clock.now()); + lastAccessTime.set(clock.now().toUtc()); + } + + void modifiedNow() { + accessedNow(); + lastModificationTime.set(clock.now().toUtc()); } } diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 90231e7..adcfbc6 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -41,6 +41,7 @@ abstract class KdbxSubTextNode extends KdbxSubNode { @override void set(T value) { + node.isDirty = true; final el = node.node.findElements(name).singleWhere((x) => true, orElse: () { final el = XmlElement(XmlName(name)); diff --git a/test/FooBar.kdbx b/test/FooBar.kdbx index 465440acf2d131d1ffbcbf1abfc412f83866e58c..18981360039118f1e4b7aaad95c2b67abbba55f1 100644 GIT binary patch delta 2414 zcmV-!36b`m5dIR7DSvgOW1<}$p%i?h@&3`*=6UF@At`2_l(i4sUQ+j4H5COQ0P_V* zv+%F8JAqQ~1apv1jAjse-feIS?IO<M$(`AOI_vda`U4H{BQNnvOxlvkKp)TpIrFl=gr}qJLM~yqpgSAOJ;rVoV$G ztG+^B$GuL)JiZtm0J=wKu^xYe&p`jhIBN<700IC2000C40MM-pyj?7vr5ypf{@DE= zRDsq@j7apGyDgL zoMLhC^NgqHkbfGwlq?@EL8oU9Ptb)KlbQYDYh`^j**Y7I;wdFel%aTDjE{zoBKcN) zO-Mvdqh%)-fhZu0P2(LA{-Gqqy$(<23;Lc60lm2)U_5DBLCnsp^$KB9`md~XqRP{I zgKreHEpWZ?mQazse1<~K4*4636Q9r}CbG+_EpUsz#eYs&zl_1|!!LTJZGsey)#EbQ z3q44(j(<-}u1b`pU*GI&!IO~En&Qfv3JtI3qQJ(nZZXdouq9n;Y8}TPMkeWBx*6X_ z6n`PH6*>AqUaW#~Ac6Gx>uGx%g48SU@Fw6@czjvMvD$j(_if(~iQWnrNl$F<1;_{I z(4o*Awtp*641~MCo0$Zkc&#gi6xi&u<3%~CIs<`4CT2e@(`guYC=*3(ig7;~cQe&Z z-)tA%rZ|5;mLzq7YR9bPDMXZ*DO4el04+-SXKsm6zcs>{o--hizNbB&qE>ohP2(RQ^z2KPlpZJVqdk{j%0!y- z|C#JesXV=v+nTUET#)TeW4)Ks1Y6vWNwm6t#BkQD5}4AUOfkzynnijHKdO!SSDbat zfh+OA&)uxS7a93RYwfCJ&P6wagz~@)#CU@Q@qva%@QTdi!*1(4?E4v#@rq%%&{y$pj~}geOZM3=}1qL}qx1BH8%#SYc+Cv`U(0&Ki&amI&_e2Wo5GU6ksAhMnw^cioA29@9pL1yGCzzPY?QURrl^rx||3*YMYx!++tt zdbqLj-q4@khYwGTV)}6zD7ICuEjLCWSnvEXUww!#lZg&$96P-obJX5Z3I>#mLTh0{ zNl3cHQ|JM{Uq_^ym5lz$dxH_;YjASDZ;ql6A0s9KM^%I9c>CD~%WWKD2ErX`6 z;znNOIFd;F8xPR=R_@PAk!xah7tG-{F4UJCH9vRqZPFFlQ z7%nqIzNuSXzk@d{`{ra$GCj|b*CoHajv)J}9In$&MGP5|P}b`a%ztKK%w&vA98)K5 z_z=gMBr`-t><8aqOT^u0t{Q<*neon5XGPpX=OV2Y6Mx=?DFlG4vL|WXSgnWVoLD6o;H3uu73))-zvo}Oh-gQ-m!9N?|)HdLUn?fj!Pd;mFB0VY72Nbsr*5M1)HGAjp@=0pW7jz9s;8Mru_X@{}qqB*lQ5mrdJAbiNPh*wv(og} zL%7z}Z-2W=_L;lQY1jx+RasaZ zCzjsFQs-v0=guRVJ&EPY*~q<7RW8O`DErLRwDgM6@*!RPMvRTNQ@VWNGZfCh{`$)- gUoQaBTT&qnDGz)Rh$gYdZq+xbs@qJ6Vt@S;fg^30qyPW_ delta 2060 zcmV+n2=n*;5}pu{DSth`hYjh-;vzAYt!N%tOU>A;mE>{ybJ^W1KKZNOhnfW-0D42B zTbmJ#6VZx9r@CM~{l4RR#r-Z}Ie;KP2nM!dCI$!qa2Ego000002M_=lIl1io2@M8F z6l(Wnyw(=NjgufVcewLw}1m7a5+TTGkW(6~VAK_f#&nj-28Id*00IC2000C40MM-pF`*J0f*Q;3jdfm; zu+bbmCpoO|ZaFB%}Eg?1FuOm{?c9tDmW{_J`LyV{F?ng_+m_&6KAIH1FMT zI0tAr3fKE2M_Z|{(mn1J4gG`ysZG0xlHl`eYd4)w;Q&N$m^>7Fc2!tZQ|1N7-tUwF zn}%<@lYhWYp|*l2wHI5vQM(CoEj{`UjFpnBotWE>rnA^dLCdN*_JW$K3f*{%{ID&~ zsrvJ(4)gV$AJ?0)05}?6`AVfK@{c@qYiKFzJnNGI1K#Sl{Po~u&f?@rs@13TMZ+L> z4vqJu2JEPa^kRf9<-)OSvrt`C+;B>&hf@n**LMOKM<`q`MV@_+XC z0JoW54D9s`fXydMCeDc`4rh5)*~h!-%r1cyv|JfZTw^#qV(3g5Z_S)8{u+nZMOs`O zPu`$%UpiW0%e}x*AYZ!*SUSBOH?D)K%sz^@zCZzTAPsBBD7F-e<*IL~;67*+MAmw@ zD)%W=N)l$-2&oc2i9#RJNsu)~mw$IMm+2wG7xLSljm_q!s5*zG2Onle?wjI_&ZH{s zt3Pm)v!F)vnsHcQz7+Xrx!-jfd#-TMKiX|;Q+uWvExdNEzAW;st%O^U94JrY8uZnsZv^bZB2@QI#1?iUiQ#MN@vcpY^-bTKdqiG7fk28Mw|yV0ZY`n?@I za+V$NkWU)rMAw}^4w#p2=zrGmn3xF)A>$dQ`=Pm+vTD|2^M-Mv_&(`8I4Qt$7xcG9 zmONRaL(x_`fL#gFyWl2C2An9-!L|$ajlHx0#mOUyf$M;Z*yVQDYw40Z zVDhIQ*YU~K=BhHK6l)T&n1hPsZ;!Q^pMC9FK1d(&Xc>Lspax@KX~A%lFDkqI7;_L%tjFDaHJJJ_?9YJj}b zA3hCXPKDTF>m0X$ihrdf+c=H5o$eoIkv!n$X@j2TAPWY~!wOP{Mi>}ae~4 z+)eUBuG51Zz6m~P2c=QE7#UD6ev^k9%alX2@U-BkzI0d_D)%Ez=qExot(ubFP#3s9 zj{B1yz*|W(_HQ3ey}AGzi{hz9?q!oT!q;Cx+BMJJC4VyO8KCzeM&-_U;Vm!* zsi0p2pvnjNwK(A1@YDrJ!6FSp`m>*02$w`oXlSd8*gIlXT+Nd?CmE)IX&RNQWfq}b zbE^juis4q3LenqgwGEH^pRoQAM;=x*vX$t^zvS!F8pL|_kovxXuaPbP)|?%tg{f0I!r@#KYg#rNjACyFr{<%^F`N=WsIfl(p= zG*t~lB$Bl(7INlm8^*U+;~SXkvy;A!XeUKW76vD{M50doZpT!f8hqp3mE_T`wy@>j q2|Q2(>+~vKlwLBKP%}!u^I;&Bc(zaTSq;%jIvDZ2k_CIr)PT!2|NDIa