Browse Source

fix concurrent save bug: only allow one save at a time.

pull/3/head
Herbert Poul 4 years ago
parent
commit
3adb6af91a
  1. 2
      CHANGELOG.md
  2. 10
      lib/src/kdbx_file.dart
  3. 7
      lib/src/kdbx_format.dart
  4. 3
      pubspec.yaml
  5. 45
      test/kdbx_test.dart

2
CHANGELOG.md

@ -2,6 +2,8 @@
- Use kdbx 4.x by default when creating new files.
- Implemented support for custom icons.
- Implemented file merging/synchronization.
- Fixed threading problem on save: only allow one save at a time for each file.
## 0.4.1

10
lib/src/kdbx_file.dart

@ -11,6 +11,7 @@ import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:logging/logging.dart';
import 'package:quiver/check.dart';
import 'package:synchronized/synchronized.dart';
import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx_file');
@ -44,6 +45,11 @@ class KdbxFile {
final StreamController<Set<KdbxObject>> _dirtyObjectsChanged =
StreamController<Set<KdbxObject>>.broadcast();
/// lock used by [KdbxFormat] to synchronize saves,
/// because save actions are not thread save.
/// see [KdbxFileInternal.saveLock].
final Lock _saveLock = Lock();
Stream<Set<KdbxObject>> get dirtyObjectsChanged =>
_dirtyObjectsChanged.stream;
@ -132,6 +138,10 @@ class KdbxFile {
}
}
extension KdbxInternal on KdbxFile {
Lock get saveLock => _saveLock;
}
class CachedValue<T> {
CachedValue.withNull() : value = null;
CachedValue.withValue(this.value) : assert(value != null);

7
lib/src/kdbx_format.dart

@ -517,7 +517,14 @@ class KdbxFormat {
}
}
/// Saves the given file.
Future<Uint8List> save(KdbxFile file) async {
_logger.finer('Saving ${file.body.rootGroup.uuid} '
'(locked: ${file.saveLock.locked})');
return file.saveLock.synchronized(() => _saveSynchronized(file));
}
Future<Uint8List> _saveSynchronized(KdbxFile file) async {
final body = file.body;
final header = file.header;

3
pubspec.yaml

@ -1,6 +1,6 @@
name: kdbx
description: KeepassX format implementation in pure dart. (kdbx 3.x and 4.x support).
version: 0.4.1
version: 0.4.2
homepage: https://github.com/authpass/kdbx.dart
environment:
@ -24,6 +24,7 @@ dependencies:
quiver: '>=2.1.0 <3.0.0'
archive: '>=2.0.13 <3.0.0'
supercharged_dart: '>=1.2.0 <2.0.0'
synchronized: '>=2.2.0 <3.0.0'
collection: '>=1.14.0 <2.0.0'

45
test/kdbx_test.dart

@ -1,6 +1,7 @@
@Tags(['kdbx3'])
import 'dart:io';
import 'dart:typed_data';
import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart';
@ -8,10 +9,13 @@ import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_format.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:synchronized/synchronized.dart';
import 'package:test/test.dart';
import 'internal/test_utils.dart';
final _logger = Logger('kdbx_test');
class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
@override
String decryptBase64(String protectedValue) => 'fake';
@ -23,13 +27,13 @@ class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
void main() {
Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root);
final kdbxForamt = KdbxFormat();
final kdbxFormat = KdbxFormat();
group('Reading', () {
setUp(() {});
test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes();
await kdbxForamt.read(
await kdbxFormat.read(
data, Credentials(ProtectedValue.fromString('FooBar')));
});
});
@ -41,7 +45,7 @@ void main() {
final cred = Credentials.composite(
ProtectedValue.fromString('asdf'), keyFileBytes);
final data = await File('test/password-and-keyfile.kdbx').readAsBytes();
final file = await kdbxForamt.read(data, cred);
final file = await kdbxFormat.read(data, cred);
expect(file.body.rootGroup.entries, hasLength(2));
});
test('Read with PW and hex keyfile', () async {
@ -50,14 +54,14 @@ void main() {
final cred = Credentials.composite(
ProtectedValue.fromString('testing99'), keyFileBytes);
final data = await File('test/keyfile/newdatabase2.kdbx').readAsBytes();
final file = await kdbxForamt.read(data, cred);
final file = await kdbxFormat.read(data, cred);
expect(file.body.rootGroup.entries, hasLength(3));
});
});
group('Creating', () {
test('Simple create', () {
final kdbx = kdbxForamt.create(
final kdbx = kdbxFormat.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
expect(kdbx, isNotNull);
expect(kdbx.body.rootGroup, isNotNull);
@ -68,7 +72,7 @@ void main() {
.toXmlString(pretty: true));
});
test('Create Entry', () {
final kdbx = kdbxForamt.create(
final kdbx = kdbxFormat.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup);
@ -113,7 +117,7 @@ void main() {
test('Simple save and load', () async {
final credentials = Credentials(ProtectedValue.fromString('FooBar'));
final saved = await (() async {
final kdbx = kdbxForamt.create(credentials, 'CreateTest');
final kdbx = kdbxFormat.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry);
@ -124,7 +128,7 @@ void main() {
// print(ByteUtils.toHexList(saved));
final kdbx = await kdbxForamt.read(saved, credentials);
final kdbx = await kdbxFormat.read(saved, credentials);
expect(
kdbx.body.rootGroup.entries.first
.getString(KdbxKeyCommon.PASSWORD)
@ -132,5 +136,30 @@ void main() {
'LoremIpsum');
File('test.kdbx').writeAsBytesSync(saved);
});
test('concurrent save test', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
final readLock = Lock();
Future<KdbxFile> doSave(
Future<Uint8List> byteFuture, String debug) async {
_logger.fine('$debug: Waiting...');
final bytes = await byteFuture;
return await readLock.synchronized(() {
try {
final ret = TestUtil.readKdbxFileBytes(bytes);
_logger.fine('$debug FINISHED: success');
return ret;
} catch (e, stackTrace) {
_logger.shout(
'$debug FINISHED: error while reading file', e, stackTrace);
rethrow;
}
});
}
final save1 = doSave(file.save(), 'first ');
final save2 = doSave(file.save(), 'second');
expect((await save1).body.meta.databaseName.get(), isNotNull);
expect((await save2).body.meta.databaseName.get(), isNotNull);
});
});
}

Loading…
Cancel
Save