KeepassX format implementation in pure dart.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

178 lines
5.3 KiB

import 'dart:async';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/kdbx_consts.dart';
import 'package:kdbx/src/kdbx_dao.dart';
import 'package:kdbx/src/kdbx_exceptions.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:kdbx/src/utils/sequence.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');
typedef FileSaveCallback<T> = Future<T> Function(Uint8List bytes);
class KdbxFile {
KdbxFile(
this.ctx,
this.kdbxFormat,
this._credentials,
this.header,
this.body,
) {
for (final obj in _allObjects) {
obj.file = this;
}
}
static final protectedValues = Expando<ProtectedValue>();
static ProtectedValue? protectedValueForNode(xml.XmlElement node) {
return protectedValues[node];
}
static void setProtectedValueForNode(
xml.XmlElement node, ProtectedValue? value) {
protectedValues[node] = value;
}
final KdbxFormat kdbxFormat;
final KdbxReadWriteContext ctx;
Credentials get credentials => _credentials;
set credentials(Credentials credentials) {
body.meta.modify(() => _credentials = credentials);
}
Credentials _credentials;
final KdbxHeader header;
final KdbxBody body;
final Set<KdbxObject> dirtyObjects = {};
bool get isDirty => dirtyObjects.isNotEmpty;
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;
// ignore: prefer_function_declarations_over_variables
static final FileSaveCallback<Uint8List> _saveToBytes = (bytes) async {
return bytes;
};
// @Deprecated('Use [saveTo] instead.')
Future<Uint8List> save() async {
return kdbxFormat.save(this, _saveToBytes);
}
Future<T> saveTo<T>(FileSaveCallback<T> saveBytes) {
return kdbxFormat.save(this, saveBytes);
}
/// Marks all dirty objects as clean. Called by [KdbxFormat.save].
void onSaved(TimeSequence savedAt) {
final cleanedObjects = dirtyObjects.where((e) => e.clean(savedAt)).toList();
dirtyObjects.removeAll(cleanedObjects);
_logger.finer('Saved. Remaining dirty objects: ${dirtyObjects.length}');
_dirtyObjectsChanged.add(dirtyObjects);
}
Iterable<KdbxObject> get _allObjects => body.rootGroup
.getAllGroups()
.cast<KdbxObject>()
.followedBy(body.rootGroup.getAllEntries());
void dirtyObject(KdbxObject kdbxObject) {
dirtyObjects.add(kdbxObject);
_dirtyObjectsChanged.add(UnmodifiableSetView(Set.of(dirtyObjects)));
}
void dispose() {
_dirtyObjectsChanged.close();
}
CachedValue<KdbxGroup>? _recycleBin;
/// Returns the recycle bin, if it exists, null otherwise.
KdbxGroup? get recycleBin => (_recycleBin ??= _findRecycleBin()).value;
CachedValue<KdbxGroup> _findRecycleBin() {
final uuid = body.meta.recycleBinUUID.get();
if (uuid?.isNil != false) {
return CachedValue.withNull();
}
try {
return CachedValue.withValue(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);
return CachedValue.withNull();
}
}
KdbxGroup _createRecycleBin() {
body.meta.recycleBinEnabled.set(true);
final group = createGroup(parent: body.rootGroup, name: 'Trash');
group.icon.set(KdbxIcon.TrashBin);
group.enableAutoType.set(false);
group.enableSearching.set(false);
body.meta.recycleBinUUID.set(group.uuid);
_recycleBin = CachedValue.withValue(group);
return group;
}
KdbxGroup getRecycleBinOrCreate() {
return recycleBin ?? _createRecycleBin();
}
/// Upgrade v3 file to v4.
void upgrade(int majorVersion) {
checkArgument(majorVersion == 4, message: 'Must be majorVersion 4');
body.meta.settingsChanged.setToNow();
body.meta.headerHash.remove();
header.upgrade(majorVersion);
}
/// Merges the given file into this file.
/// Both files must have the same origin (ie. same root group UUID).
/// FIXME: THiS iS NOT YET FINISHED, DO NOT USE.
MergeContext merge(KdbxFile other) {
if (other.body.rootGroup.uuid != body.rootGroup.uuid) {
throw KdbxUnsupportedException(
'Root groups of source and dest file do not match.');
}
return body.merge(other.body);
}
}
extension KdbxInternal on KdbxFile {
Lock get saveLock => _saveLock;
}
class CachedValue<T> {
CachedValue.withNull() : value = null;
CachedValue.withValue(this.value) : assert(value != null);
final T? value;
}