Browse Source

implemented dirty tracking, modification updates, etc.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
b89cce0f74
  1. 29
      lib/src/internal/async_utils.dart
  2. 112
      lib/src/kdbx_format.dart
  3. 14
      lib/src/kdbx_group.dart
  4. 34
      lib/src/kdbx_object.dart
  5. 7
      lib/src/kdbx_times.dart
  6. 1
      lib/src/kdbx_xml.dart
  7. BIN
      test/FooBar.kdbx

29
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<StreamSubscription<dynamic>> _subscriptions = <StreamSubscription<dynamic>>[];
/// Listens to a stream and saves it to the list of subscriptions.
void listen(Stream<dynamic> stream, void onData(dynamic data), {Function onError}) {
if (stream != null) {
_subscriptions.add(stream.listen(onData, onError: onError));
}
}
void handle(StreamSubscription<dynamic> subscription) {
_subscriptions.add(subscription);
}
/// Cancels all streams that were previously added with listen().
void cancelSubscriptions() {
_subscriptions.forEach(_cancelSubscription);
_subscriptions.clear();
}
Future<void> _cancelSubscription(StreamSubscription<dynamic> subscription) =>
subscription.cancel();
}
class StreamSubscriptions with StreamSubscriberBase {}

112
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:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.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/byte_utils.dart';
import 'package:kdbx/src/internal/crypto_utils.dart'; import 'package:kdbx/src/internal/crypto_utils.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
@ -34,8 +35,10 @@ class Credentials {
} }
} }
class KdbxFile { class KdbxFile with Changeable<KdbxFile> {
KdbxFile(this.credentials, this.header, this.body); KdbxFile(this.credentials, this.header, this.body) {
_subscribeToChildren();
}
static final protectedValues = Expando<ProtectedValue>(); static final protectedValues = Expando<ProtectedValue>();
@ -43,23 +46,57 @@ class KdbxFile {
return protectedValues[node]; return protectedValues[node];
} }
static void setProtectedValueForNode(xml.XmlElement node, ProtectedValue value) { static void setProtectedValueForNode(
xml.XmlElement node, ProtectedValue value) {
protectedValues[node] = value; protectedValues[node] = value;
} }
final StreamSubscriptions _subscriptions = StreamSubscriptions();
final Credentials credentials; final Credentials credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;
Uint8List save() { Uint8List save() {
assert(header.versionMajor == 3);
final output = BytesBuilder(); final output = BytesBuilder();
final writer = WriterHelper(output); final writer = WriterHelper(output);
header.generateSalts(); header.generateSalts();
header.write(writer); 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(); return output.toBytes();
} }
Iterable<KdbxObject> get _allObjects => body.rootGroup
.getAllGroups()
.cast<KdbxObject>()
.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 { class KdbxBody extends KdbxNode {
@ -77,35 +114,32 @@ class KdbxBody extends KdbxNode {
final KdbxMeta meta; final KdbxMeta meta;
final KdbxGroup rootGroup; final KdbxGroup rootGroup;
void write(WriterHelper writer, KdbxFile kdbxFile) { void writeV3(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,
ProtectedSaltGenerator saltGenerator) { ProtectedSaltGenerator saltGenerator) {
meta.headerHash.set(
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List)
.buffer);
final xml = generateXml(saltGenerator); final xml = generateXml(saltGenerator);
final xmlBytes = utf8.encode(xml.toXmlString()); final xmlBytes = utf8.encode(xml.toXmlString());
final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ? final Uint8List compressedBytes =
GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List; (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(); 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); HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter);
final bytes = byteWriter.output.toBytes(); final bytes = byteWriter.output.toBytes();
final masterKey = KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials); final masterKey =
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List()); KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials);
// writer.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List()); final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes,
writer.writeBytes(encrypted); kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List());
return encrypted;
} }
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
@ -124,12 +158,16 @@ class KdbxBody extends KdbxNode {
} }
} }
final builder = xml.XmlBuilder(); final builder = xml.XmlBuilder();
builder.processing('xml', 'version="1.0" encoding="utf-8" standalone="yes"'); builder.processing(
builder.element('KeePassFile', nest: [ 'xml', 'version="1.0" encoding="utf-8" standalone="yes"');
meta.toXml(), builder.element(
() => builder.element('Root', nest: rootGroupNode),],); 'KeePassFile',
nest: [
meta.toXml(),
() => builder.element('Root', nest: rootGroupNode),
],
);
// final doc = xml.XmlDocument(); // final doc = xml.XmlDocument();
// doc.children.add(xml.XmlProcessing( // doc.children.add(xml.XmlProcessing(
// 'xml', 'version="1.0" encoding="utf-8" standalone="yes"')); // 'xml', 'version="1.0" encoding="utf-8" standalone="yes"'));
@ -224,7 +262,8 @@ class KdbxFormat {
final decryptCipher = CBCBlockCipher(AESFastEngine()); final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(false, decryptCipher.init(false,
ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List()));
final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload);
final decrypted = AesHelper.unpad(paddedDecrypted); final decrypted = AesHelper.unpad(paddedDecrypted);
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
@ -265,11 +304,12 @@ class KdbxFormat {
return masterKey; return masterKey;
} }
static Uint8List _encryptDataAes(Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) { static Uint8List _encryptDataAes(
Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) {
final encryptCipher = CBCBlockCipher(AESFastEngine()); final encryptCipher = CBCBlockCipher(AESFastEngine());
encryptCipher.init(true, encryptCipher.init(
ParametersWithIV(KeyParameter(masterKey), encryptionIv)); true, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
return AesHelper.processBlocks(encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); return AesHelper.processBlocks(
encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize));
} }
} }

14
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_consts.dart';
import 'package:kdbx/src/kdbx_entry.dart'; import 'package:kdbx/src/kdbx_entry.dart';
import 'package:kdbx/src/kdbx_xml.dart'; import 'package:kdbx/src/kdbx_xml.dart';
@ -7,7 +8,8 @@ import 'package:xml/xml.dart';
import 'kdbx_object.dart'; import 'kdbx_object.dart';
class KdbxGroup extends KdbxObject { 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); this.name.set(name);
icon.set(KdbxIcon.Folder); icon.set(KdbxIcon.Folder);
expanded.set(true); expanded.set(true);
@ -23,7 +25,9 @@ class KdbxGroup extends KdbxObject {
.map((el) => KdbxEntry.read(this, el)) .map((el) => KdbxEntry.read(this, el))
.forEach(_entries.add); .forEach(_entries.add);
} }
final StreamSubscriptions _subscriptions = StreamSubscriptions();
@override @override
XmlElement toXml() { XmlElement toXml() {
final el = super.toXml(); final el = super.toXml();
@ -46,18 +50,22 @@ class KdbxGroup extends KdbxObject {
/// null if this is the root group. /// null if this is the root group.
final KdbxGroup parent; final KdbxGroup parent;
final List<KdbxGroup> groups = []; final List<KdbxGroup> groups = [];
List<KdbxEntry> get entries => List.unmodifiable(_entries); List<KdbxEntry> get entries => List.unmodifiable(_entries);
final List<KdbxEntry> _entries = []; final List<KdbxEntry> _entries = [];
void addEntry(KdbxEntry entry) { void addEntry(KdbxEntry entry) {
if (entry.parent != this) { 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); _entries.add(entry);
node.children.add(entry.node); node.children.add(entry.node);
isDirty = true;
} }
StringNode get name => StringNode(this, 'Name'); StringNode get name => StringNode(this, 'Name');
// String get name => text('Name') ?? ''; // String get name => text('Name') ?? '';
BooleanNode get expanded => BooleanNode(this, 'IsExpanded'); BooleanNode get expanded => BooleanNode(this, 'IsExpanded');
} }

34
lib/src/kdbx_object.dart

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
@ -8,8 +9,29 @@ import 'package:uuid/uuid.dart';
import 'package:uuid/uuid_util.dart'; import 'package:uuid/uuid_util.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
abstract class KdbxNode { class ChangeEvent<T> {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)); ChangeEvent({this.object, this.isDirty});
final T object;
final bool isDirty;
}
mixin Changeable<T> {
final _controller = StreamController<ChangeEvent<T>>.broadcast();
Stream<ChangeEvent<T>> 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> {
KdbxNode.create(String nodeName) : node = XmlElement(XmlName(nodeName)) {
isDirty = true;
}
KdbxNode.read(this.node); KdbxNode.read(this.node);
@ -18,8 +40,6 @@ abstract class KdbxNode {
// @protected // @protected
// String text(String nodeName) => _opt(nodeName)?.text; // String text(String nodeName) => _opt(nodeName)?.text;
KdbxSubTextNode textNode(String nodeName) => StringNode(this, nodeName);
@mustCallSuper @mustCallSuper
XmlElement toXml() { XmlElement toXml() {
final el = node.copy() as XmlElement; final el = node.copy() as XmlElement;
@ -42,6 +62,12 @@ abstract class KdbxObject extends KdbxNode {
IconNode get icon => IconNode(this, 'IconID'); IconNode get icon => IconNode(this, 'IconID');
@override
set isDirty(bool dirty) {
super.isDirty = dirty;
times.modifiedNow();
}
@override @override
XmlElement toXml() { XmlElement toXml() {
final el = super.toXml(); final el = super.toXml();

7
lib/src/kdbx_times.dart

@ -27,6 +27,11 @@ class KdbxTimes extends KdbxNode {
DateTimeUtcNode get locationChanged => DateTimeUtcNode(this, 'LocationChanged'); DateTimeUtcNode get locationChanged => DateTimeUtcNode(this, 'LocationChanged');
void accessedNow() { void accessedNow() {
lastAccessTime.set(clock.now()); lastAccessTime.set(clock.now().toUtc());
}
void modifiedNow() {
accessedNow();
lastModificationTime.set(clock.now().toUtc());
} }
} }

1
lib/src/kdbx_xml.dart

@ -41,6 +41,7 @@ abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {
@override @override
void set(T value) { void set(T value) {
node.isDirty = true;
final el = final el =
node.node.findElements(name).singleWhere((x) => true, orElse: () { node.node.findElements(name).singleWhere((x) => true, orElse: () {
final el = XmlElement(XmlName(name)); final el = XmlElement(XmlName(name));

BIN
test/FooBar.kdbx

Binary file not shown.
Loading…
Cancel
Save