Browse Source

WIP: prepare to switch to kdbx v4 by default.

pull/3/head
Herbert Poul 4 years ago
parent
commit
b6b8f5c6a7
  1. 2
      README.md
  2. 9
      lib/src/kdbx_file.dart
  3. 23
      lib/src/kdbx_format.dart
  4. 199
      lib/src/kdbx_header.dart
  5. 4
      lib/src/kdbx_meta.dart
  6. 6
      lib/src/kdbx_xml.dart
  7. 4
      test/kdbx_test.dart

2
README.md

@ -28,6 +28,8 @@ https://github.com/authpass/argon2_ffi
* argon2_ffi/ios/Classes * argon2_ffi/ios/Classes
* `cmake . && cmake --build .` * `cmake . && cmake --build .`
* `cp libargon2_ffi.dylib kdbx.dart/` * `cp libargon2_ffi.dylib kdbx.dart/`
* Might need to run: `codesign --remove-signature /usr/local/bin/dart`
https://github.com/dart-lang/sdk/issues/39231#issuecomment-579743656
* Linux: * Linux:
* argon2_ffi/ios/Classes * argon2_ffi/ios/Classes
* `cmake . && cmake --build .` * `cmake . && cmake --build .`

9
lib/src/kdbx_file.dart

@ -10,6 +10,7 @@ import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_object.dart'; import 'package:kdbx/src/kdbx_object.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:quiver/check.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx_file'); final _logger = Logger('kdbx_file');
@ -109,6 +110,14 @@ class KdbxFile {
KdbxGroup getRecycleBinOrCreate() { KdbxGroup getRecycleBinOrCreate() {
return recycleBin ?? _createRecycleBin(); 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);
}
} }
class CachedValue<T> { class CachedValue<T> {

23
lib/src/kdbx_format.dart

@ -94,7 +94,7 @@ class KdbxReadWriteContext {
final KdbxHeader header; final KdbxHeader header;
int get versionMajor => header.versionMajor; int get versionMajor => header.version.major;
KdbxBinary binaryById(int id) { KdbxBinary binaryById(int id) {
if (id >= _binaries.length) { if (id >= _binaries.length) {
@ -327,13 +327,15 @@ class KdbxFormat {
final Argon2 argon2; final Argon2 argon2;
static bool dartWebWorkaround = false; static bool dartWebWorkaround = false;
/// Creates a new, empty [KdbxFile] with default settings.
/// If [header] is not given by default a kdbx 4.0 file will be created.
KdbxFile create( KdbxFile create(
Credentials credentials, Credentials credentials,
String name, { String name, {
String generator, String generator,
KdbxHeader header, KdbxHeader header,
}) { }) {
header ??= KdbxHeader.create(); header ??= KdbxHeader.createV3();
final ctx = KdbxReadWriteContext(binaries: [], header: header); final ctx = KdbxReadWriteContext(binaries: [], header: header);
final meta = KdbxMeta.create( final meta = KdbxMeta.create(
databaseName: name, databaseName: name,
@ -354,14 +356,14 @@ class KdbxFormat {
Future<KdbxFile> read(Uint8List input, Credentials credentials) async { Future<KdbxFile> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input); final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader); final header = KdbxHeader.read(reader);
if (header.versionMajor == 3) { if (header.version.major == KdbxVersion.V3.major) {
return await _loadV3(header, reader, credentials); return await _loadV3(header, reader, credentials);
} else if (header.versionMajor == 4) { } else if (header.version.major == KdbxVersion.V4.major) {
return await _loadV4(header, reader, credentials); return await _loadV4(header, reader, credentials);
} else { } else {
_logger.finer('Unsupported version for $header'); _logger.finer('Unsupported version for $header');
throw KdbxUnsupportedException('Unsupported kdbx version ' throw KdbxUnsupportedException('Unsupported kdbx version '
'${header.versionMajor}.${header.versionMinor}.' '${header.version}.'
' Only 3.x and 4.x is supported.'); ' Only 3.x and 4.x is supported.');
} }
} }
@ -377,14 +379,16 @@ class KdbxFormat {
final headerHash = final headerHash =
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List); (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List);
if (file.header.versionMajor <= 3) { if (file.header.version < KdbxVersion.V3) {
throw UnsupportedError('Unsupported version ${header.version}');
} else if (file.header.version < KdbxVersion.V4) {
final streamKey = final streamKey =
file.header.fields[HeaderFields.ProtectedStreamKey].bytes; file.header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey); final gen = ProtectedSaltGenerator(streamKey);
body.meta.headerHash.set(headerHash.buffer); body.meta.headerHash.set(headerHash.buffer);
await body.writeV3(writer, file, gen); await body.writeV3(writer, file, gen);
} else if (header.versionMajor <= 4) { } else if (header.version.major == KdbxVersion.V4.major) {
final headerBytes = writer.output.toBytes(); final headerBytes = writer.output.toBytes();
writer.writeBytes(headerHash); writer.writeBytes(headerHash);
final gen = _createProtectedSaltGenerator(header); final gen = _createProtectedSaltGenerator(header);
@ -393,7 +397,7 @@ class KdbxFormat {
writer.writeBytes(headerHmac.bytes as Uint8List); writer.writeBytes(headerHmac.bytes as Uint8List);
body.writeV4(writer, file, gen, keys); body.writeV4(writer, file, gen, keys);
} else { } else {
throw UnsupportedError('Unsupported version ${header.versionMajor}'); throw UnsupportedError('Unsupported version ${header.version}');
} }
file.onSaved(); file.onSaved();
return output.toBytes(); return output.toBytes();
@ -450,7 +454,8 @@ class KdbxFormat {
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final content = KdbxFormat._gzipDecode(decrypted); final content = KdbxFormat._gzipDecode(decrypted);
final contentReader = ReaderHelper(content); final contentReader = ReaderHelper(content);
final innerHeader = KdbxHeader.readInnerHeaderFields(contentReader, 4); final innerHeader =
KdbxHeader.readInnerHeaderFields(contentReader, header.version);
// _logger.fine('inner header fields: $headerFields'); // _logger.fine('inner header fields: $headerFields');
// header.innerFields.addAll(headerFields); // header.innerFields.addAll(headerFields);

199
lib/src/kdbx_header.dart

@ -7,9 +7,10 @@ import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/kdbx_binary.dart'; import 'package:kdbx/src/kdbx_binary.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart'; import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:kdbx/src/utils/scope_functions.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:quiver/check.dart';
import 'package:quiver/core.dart';
final _logger = Logger('kdbx.header'); final _logger = Logger('kdbx.header');
@ -31,6 +32,16 @@ enum Compression {
/// id: 1 /// id: 1
gzip, gzip,
} }
const _compressionIds = {
Compression.none: 0,
Compression.gzip: 1,
};
final _compressionIdsById =
_compressionIds.map((key, value) => MapEntry(value, key));
extension on Compression {
int get id => _compressionIds[this];
}
/// how protected values are encrypted in the xml. /// how protected values are encrypted in the xml.
enum ProtectedValueEncryption { plainText, arc4variant, salsa20, chaCha20 } enum ProtectedValueEncryption { plainText, arc4variant, salsa20, chaCha20 }
@ -51,6 +62,43 @@ enum HeaderFields {
PublicCustomData, PublicCustomData,
} }
class KdbxVersion {
const KdbxVersion._(this.major, this.minor);
static const V3 = KdbxVersion._(3, 0);
static const V3_1 = KdbxVersion._(3, 1);
static const V4 = KdbxVersion._(4, 0);
final int major;
final int minor;
bool operator <(KdbxVersion other) =>
major < other.major || (major == other.major && minor < other.minor);
bool operator >(KdbxVersion other) =>
major > other.major || (major == other.major && minor > other.minor);
bool operator >=(KdbxVersion other) => this == other || this > other;
@override
bool operator ==(Object other) =>
other is KdbxVersion && major == other.major && minor == other.minor;
@override
int get hashCode => hash2(major, minor);
@override
String toString() => '$major.$minor';
}
const _headerFieldsByVersion = {
HeaderFields.TransformSeed: [KdbxVersion.V3],
HeaderFields.TransformRounds: [KdbxVersion.V3],
HeaderFields.ProtectedStreamKey: [KdbxVersion.V3],
HeaderFields.StreamStartBytes: [KdbxVersion.V3],
HeaderFields.InnerRandomStreamID: [KdbxVersion.V3],
};
enum InnerHeaderFields { enum InnerHeaderFields {
EndOfHeader, EndOfHeader,
InnerRandomStreamID, InnerRandomStreamID,
@ -86,19 +134,18 @@ class KdbxHeader {
KdbxHeader({ KdbxHeader({
@required this.sig1, @required this.sig1,
@required this.sig2, @required this.sig2,
@required this.versionMinor, @required KdbxVersion version,
@required this.versionMajor,
@required this.fields, @required this.fields,
@required this.endPos, @required this.endPos,
Map<InnerHeaderFields, InnerHeaderField> innerFields, Map<InnerHeaderFields, InnerHeaderField> innerFields,
}) : innerHeader = InnerHeader(fields: innerFields ?? {}); }) : _version = version,
innerHeader = InnerHeader(fields: innerFields ?? {});
KdbxHeader.create() KdbxHeader.createV3()
: this( : this(
sig1: Consts.FileMagic, sig1: Consts.FileMagic,
sig2: Consts.Sig2Kdbx, sig2: Consts.Sig2Kdbx,
versionMinor: 1, version: KdbxVersion.V3_1,
versionMajor: 3,
fields: _defaultFieldValues(), fields: _defaultFieldValues(),
endPos: null, endPos: null,
); );
@ -107,15 +154,15 @@ class KdbxHeader {
: this( : this(
sig1: Consts.FileMagic, sig1: Consts.FileMagic,
sig2: Consts.Sig2Kdbx, sig2: Consts.Sig2Kdbx,
versionMinor: 1, version: KdbxVersion.V4,
versionMajor: 4,
fields: _defaultFieldValuesV4(), fields: _defaultFieldValuesV4(),
innerFields: _defaultInnerFieldValuesV4(), innerFields: _defaultInnerFieldValuesV4(),
endPos: null, endPos: null,
); );
// TODO: user KdbxVersion
static List<HeaderFields> _requiredFields(int majorVersion) { static List<HeaderFields> _requiredFields(int majorVersion) {
if (majorVersion < 3) { if (majorVersion < KdbxVersion.V3.major) {
throw KdbxUnsupportedException('Unsupported version: $majorVersion'); throw KdbxUnsupportedException('Unsupported version: $majorVersion');
} }
final baseHeaders = [ final baseHeaders = [
@ -124,7 +171,7 @@ class KdbxHeader {
HeaderFields.MasterSeed, HeaderFields.MasterSeed,
HeaderFields.EncryptionIV, HeaderFields.EncryptionIV,
]; ];
if (majorVersion < 4) { if (majorVersion < KdbxVersion.V4.major) {
return baseHeaders + return baseHeaders +
[ [
HeaderFields.TransformSeed, HeaderFields.TransformSeed,
@ -151,7 +198,7 @@ class KdbxHeader {
} }
void _validate() { void _validate() {
for (final required in _requiredFields(versionMajor)) { for (final required in _requiredFields(version.major)) {
if (fields[required] == null) { if (fields[required] == null) {
throw KdbxCorruptedFileException('Missing header $required'); throw KdbxCorruptedFileException('Missing header $required');
} }
@ -182,13 +229,13 @@ class KdbxHeader {
// TODO make sure default algorithm is "secure" engouh. Or whether we should // TODO make sure default algorithm is "secure" engouh. Or whether we should
// use like [SecureRandom] from PointyCastle? // use like [SecureRandom] from PointyCastle?
_setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(32)); _setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(32));
if (versionMajor < 4) { if (version.major == KdbxVersion.V3.major) {
_setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32)); _setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32));
_setHeaderField(HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32)); _setHeaderField(HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32));
_setHeaderField( _setHeaderField(
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32)); HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32));
_setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16)); _setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16));
} else if (versionMajor < 5) { } else if (version.major == KdbxVersion.V4.major) {
_setInnerHeaderField( _setInnerHeaderField(
InnerHeaderFields.InnerRandomStreamKey, ByteUtils.randomBytes(64)); InnerHeaderFields.InnerRandomStreamKey, ByteUtils.randomBytes(64));
final kdfParameters = readKdfParameters; final kdfParameters = readKdfParameters;
@ -203,7 +250,7 @@ class KdbxHeader {
HeaderFields.EncryptionIV, ByteUtils.randomBytes(ivLength)); HeaderFields.EncryptionIV, ByteUtils.randomBytes(ivLength));
} else { } else {
throw KdbxUnsupportedException( throw KdbxUnsupportedException(
'We do not support Kdbx 3.x and 4.x right now. ($versionMajor.$versionMinor)'); 'We do not support Kdbx 3.x and 4.x right now. ($version)');
} }
} }
@ -213,8 +260,8 @@ class KdbxHeader {
writer.writeUint32(Consts.FileMagic); writer.writeUint32(Consts.FileMagic);
writer.writeUint32(Consts.Sig2Kdbx); writer.writeUint32(Consts.Sig2Kdbx);
// write version // write version
writer.writeUint16(versionMinor); writer.writeUint16(version.minor);
writer.writeUint16(versionMajor); writer.writeUint16(version.major);
for (final field for (final field
in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) { in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) {
_writeField(writer, field); _writeField(writer, field);
@ -225,7 +272,7 @@ class KdbxHeader {
} }
void writeInnerHeader(WriterHelper writer) { void writeInnerHeader(WriterHelper writer) {
assert(versionMajor >= 4); assert(version >= KdbxVersion.V4);
_validateInner(); _validateInner();
for (final field in InnerHeaderFields.values for (final field in InnerHeaderFields.values
.where((f) => f != InnerHeaderFields.EndOfHeader)) { .where((f) => f != InnerHeaderFields.EndOfHeader)) {
@ -268,37 +315,37 @@ class KdbxHeader {
} }
void _writeFieldSize(WriterHelper writer, int size) { void _writeFieldSize(WriterHelper writer, int size) {
if (versionMajor >= 4) { if (version >= KdbxVersion.V4) {
writer.writeUint32(size); writer.writeUint32(size);
} else { } else {
writer.writeUint16(size); writer.writeUint16(size);
} }
} }
static Map<HeaderFields, HeaderField> _defaultFieldValues() => static Map<HeaderFields, HeaderField> _defaultFieldValues() => _headerFields({
Map.fromEntries([ HeaderFields.CipherID: CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes(),
HeaderField(HeaderFields.CipherID, HeaderFields.CompressionFlags:
CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes()), WriterHelper.singleUint32Bytes(Compression.gzip.id),
HeaderField( HeaderFields.TransformRounds: WriterHelper.singleUint64Bytes(6000),
HeaderFields.CompressionFlags, WriterHelper.singleUint32Bytes(1)), HeaderFields.InnerRandomStreamID: WriterHelper.singleUint32Bytes(
HeaderField( ProtectedValueEncryption.values
HeaderFields.TransformRounds, WriterHelper.singleUint64Bytes(6000)), .indexOf(ProtectedValueEncryption.salsa20)),
HeaderField( });
HeaderFields.InnerRandomStreamID,
WriterHelper.singleUint32Bytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.salsa20))),
].map((f) => MapEntry(f.field, f)));
static Map<HeaderFields, HeaderField> _defaultFieldValuesV4() => static Map<HeaderFields, HeaderField> _defaultFieldValuesV4() =>
_defaultFieldValues() _headerFields({
..remove(HeaderFields.TransformRounds) HeaderFields.CipherID: CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes(),
..remove(HeaderFields.InnerRandomStreamID) HeaderFields.CompressionFlags:
..remove(HeaderFields.ProtectedStreamKey) WriterHelper.singleUint32Bytes(Compression.gzip.id),
..also((fields) { HeaderFields.KdfParameters: _createKdfDefaultParameters().write(),
fields[HeaderFields.KdfParameters] = HeaderField( HeaderFields.InnerRandomStreamID: WriterHelper.singleUint32Bytes(
HeaderFields.KdfParameters, ProtectedValueEncryption.values
_createKdfDefaultParameters().write()); .indexOf(ProtectedValueEncryption.chaCha20)),
}); });
static Map<HeaderFields, HeaderField> _headerFields(
Map<HeaderFields, Uint8List> headerFields) =>
headerFields.map((key, value) => MapEntry(key, HeaderField(key, value)));
static Map<InnerHeaderFields, InnerHeaderField> static Map<InnerHeaderFields, InnerHeaderField>
_defaultInnerFieldValuesV4() => Map.fromEntries([ _defaultInnerFieldValuesV4() => Map.fromEntries([
@ -321,35 +368,32 @@ class KdbxHeader {
// reading version // reading version
final versionMinor = reader.readUint16(); final versionMinor = reader.readUint16();
final versionMajor = reader.readUint16(); final versionMajor = reader.readUint16();
final version = KdbxVersion._(versionMajor, versionMinor);
_logger.finer('Reading version: $versionMajor.$versionMinor'); _logger.finer('Reading version: $version');
final headerFields = readAllFields( final headerFields = readAllFields(reader, version, HeaderFields.values,
reader,
versionMajor,
HeaderFields.values,
(HeaderFields field, value) => HeaderField(field, value)); (HeaderFields field, value) => HeaderField(field, value));
return KdbxHeader( return KdbxHeader(
sig1: sig1, sig1: sig1,
sig2: sig2, sig2: sig2,
versionMinor: versionMinor, version: version,
versionMajor: versionMajor,
fields: headerFields, fields: headerFields,
endPos: reader.pos, endPos: reader.pos,
); );
} }
static Map<HeaderFields, HeaderField> readHeaderFields( static Map<HeaderFields, HeaderField> readHeaderFields(
ReaderHelper reader, int versionMajor) => ReaderHelper reader, KdbxVersion version) =>
readAllFields(reader, versionMajor, HeaderFields.values, readAllFields(reader, version, HeaderFields.values,
(HeaderFields field, value) => HeaderField(field, value)); (HeaderFields field, value) => HeaderField(field, value));
static InnerHeader readInnerHeaderFields( static InnerHeader readInnerHeaderFields(
ReaderHelper reader, int versionMajor) => ReaderHelper reader, KdbxVersion version) =>
InnerHeader.fromFields( InnerHeader.fromFields(
readField( readField(
reader, reader,
versionMajor, version,
InnerHeaderFields.values, InnerHeaderFields.values,
(InnerHeaderFields field, value) => (InnerHeaderFields field, value) =>
InnerHeaderField(field, value)).toList(growable: false), InnerHeaderField(field, value)).toList(growable: false),
@ -357,22 +401,21 @@ class KdbxHeader {
static Map<TE, T> readAllFields<T extends HeaderFieldBase<TE>, TE>( static Map<TE, T> readAllFields<T extends HeaderFieldBase<TE>, TE>(
ReaderHelper reader, ReaderHelper reader,
int versionMajor, KdbxVersion version,
List<TE> fields, List<TE> fields,
T Function(TE field, Uint8List bytes) createField) => T Function(TE field, Uint8List bytes) createField) =>
Map<TE, T>.fromEntries( Map<TE, T>.fromEntries(readField(reader, version, fields, createField)
readField(reader, versionMajor, fields, createField) .map((field) => MapEntry(field.field, field)));
.map((field) => MapEntry(field.field, field)));
static Iterable<T> readField<T, TE>( static Iterable<T> readField<T, TE>(
ReaderHelper reader, ReaderHelper reader,
int versionMajor, KdbxVersion version,
List<TE> fields, List<TE> fields,
T Function(TE field, Uint8List bytes) createField) sync* { T Function(TE field, Uint8List bytes) createField) sync* {
while (true) { while (true) {
final headerId = reader.readUint8(); final headerId = reader.readUint8();
final bodySize = final bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); version >= KdbxVersion.V4 ? reader.readUint32() : reader.readUint16();
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
// _logger.finer( // _logger.finer(
// 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); // 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
@ -396,8 +439,10 @@ class KdbxHeader {
final int sig1; final int sig1;
final int sig2; final int sig2;
final int versionMinor; KdbxVersion _version;
final int versionMajor; KdbxVersion get version => _version;
// int get versionMinor => _versionMinor;
// int get versionMajor => _versionMajor;
final Map<HeaderFields, HeaderField> fields; final Map<HeaderFields, HeaderField> fields;
final InnerHeader innerHeader; final InnerHeader innerHeader;
@ -405,26 +450,21 @@ class KdbxHeader {
final int endPos; final int endPos;
Compression get compression { Compression get compression {
switch (ReaderHelper.singleUint32( final id =
fields[HeaderFields.CompressionFlags].bytes)) { ReaderHelper.singleUint32(fields[HeaderFields.CompressionFlags].bytes);
case 0: return _compressionIdsById[id] ??
return Compression.none; (() => throw KdbxUnsupportedException('invalid compression $id'))();
case 1:
return Compression.gzip;
default:
throw KdbxUnsupportedException('compression');
}
} }
ProtectedValueEncryption get innerRandomStreamEncryption => ProtectedValueEncryption get innerRandomStreamEncryption =>
ProtectedValueEncryption ProtectedValueEncryption
.values[ReaderHelper.singleUint32(_innerRandomStreamEncryptionBytes)]; .values[ReaderHelper.singleUint32(_innerRandomStreamEncryptionBytes)];
Uint8List get _innerRandomStreamEncryptionBytes => versionMajor >= 4 Uint8List get _innerRandomStreamEncryptionBytes => version >= KdbxVersion.V4
? innerHeader.fields[InnerHeaderFields.InnerRandomStreamID].bytes ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamID].bytes
: fields[HeaderFields.InnerRandomStreamID].bytes; : fields[HeaderFields.InnerRandomStreamID].bytes;
Uint8List get protectedStreamKey => versionMajor >= 4 Uint8List get protectedStreamKey => version >= KdbxVersion.V4
? innerHeader.fields[InnerHeaderFields.InnerRandomStreamKey].bytes ? innerHeader.fields[InnerHeaderFields.InnerRandomStreamKey].bytes
: fields[HeaderFields.ProtectedStreamKey].bytes; : fields[HeaderFields.ProtectedStreamKey].bytes;
@ -434,9 +474,24 @@ class KdbxHeader {
void writeKdfParameters(VarDictionary kdfParameters) => void writeKdfParameters(VarDictionary kdfParameters) =>
_setHeaderField(HeaderFields.KdfParameters, kdfParameters.write()); _setHeaderField(HeaderFields.KdfParameters, kdfParameters.write());
void upgrade(int majorVersion) {
checkArgument(majorVersion == KdbxVersion.V4.major,
message: 'Can only upgrade to 4');
_logger.info('Upgrading header to $majorVersion');
_version = KdbxVersion._(majorVersion, 0);
if (fields[HeaderFields.KdfParameters] == null) {
_logger.fine('Creating kdf parameters.');
writeKdfParameters(_createKdfDefaultParameters());
}
_setHeaderField(
HeaderFields.InnerRandomStreamID,
WriterHelper.singleUint32Bytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.chaCha20)));
}
@override @override
String toString() { String toString() {
return 'KdbxHeader{sig1: $sig1, sig2: $sig2, versionMajor: $versionMajor, versionMinor: $versionMinor}'; return 'KdbxHeader{sig1: $sig1, sig2: $sig2, version: $version}';
} }
} }

4
lib/src/kdbx_meta.dart

@ -25,6 +25,7 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
super.create('Meta') { super.create('Meta') {
this.databaseName.set(databaseName); this.databaseName.set(databaseName);
this.generator.set(generator ?? 'kdbx.dart'); this.generator.set(generator ?? 'kdbx.dart');
settingsChanged.setToNow();
} }
KdbxMeta.read(xml.XmlElement node, this.ctx) KdbxMeta.read(xml.XmlElement node, this.ctx)
@ -90,6 +91,9 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext {
UuidNode get recycleBinUUID => UuidNode(this, 'RecycleBinUUID'); UuidNode get recycleBinUUID => UuidNode(this, 'RecycleBinUUID');
DateTimeUtcNode get settingsChanged =>
DateTimeUtcNode(this, 'SettingsChanged');
DateTimeUtcNode get recycleBinChanged => DateTimeUtcNode get recycleBinChanged =>
DateTimeUtcNode(this, 'RecycleBinChanged'); DateTimeUtcNode(this, 'RecycleBinChanged');

6
lib/src/kdbx_xml.dart

@ -56,6 +56,12 @@ abstract class KdbxSubNode<T> {
T get(); T get();
void set(T value); void set(T value);
void remove() {
for (final el in node.node.findElements(name)) {
el.parentElement.children.remove(el);
}
}
} }
abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> { abstract class KdbxSubTextNode<T> extends KdbxSubNode<T> {

4
test/kdbx_test.dart

@ -83,7 +83,7 @@ void main() {
test('read mod date time', () async { test('read mod date time', () async {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
final first = file.body.rootGroup.entries.first; final first = file.body.rootGroup.entries.first;
expect(file.header.versionMajor, 3); expect(file.header.version.major, 3);
expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry'); expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry');
final modTime = first.times.lastModificationTime.get(); final modTime = first.times.lastModificationTime.get();
expect(modTime, DateTime.utc(2020, 5, 6, 7, 31, 48)); expect(modTime, DateTime.utc(2020, 5, 6, 7, 31, 48));
@ -93,7 +93,7 @@ void main() {
final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx'); final file = await TestUtil.readKdbxFile('test/keepass2test.kdbx');
{ {
final first = file.body.rootGroup.entries.first; final first = file.body.rootGroup.entries.first;
expect(file.header.versionMajor, 3); expect(file.header.version.major, 3);
expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry'); expect(first.getString(KdbxKey('Title')).getText(), 'Sample Entry');
first.times.lastModificationTime.set(newModDate); first.times.lastModificationTime.set(newModDate);
} }

Loading…
Cancel
Save