From 50aaab71bdb2de628a369cf14278e6ea4a9590b0 Mon Sep 17 00:00:00 2001
From: Herbert Poul <herbert@codeux.design>
Date: Sat, 22 Feb 2020 08:24:14 +0100
Subject: [PATCH] kdbx 4.x write support

---
 .idea/dictionaries/herbert.xml        |   1 +
 lib/src/crypto/key_encrypter_kdf.dart |  10 ++
 lib/src/kdbx_format.dart              | 159 ++++++++++++++++++--------
 lib/src/kdbx_header.dart              | 109 +++++++++++++++++-
 lib/src/kdbx_var_dictionary.dart      |  15 +++
 lib/src/utils/scope_functions.dart    |  17 +++
 libargon2_ffi.dylib                   | Bin 72052 -> 67956 bytes
 test/kdbx4_test.dart                  |  16 ++-
 8 files changed, 274 insertions(+), 53 deletions(-)
 create mode 100644 lib/src/utils/scope_functions.dart

diff --git a/.idea/dictionaries/herbert.xml b/.idea/dictionaries/herbert.xml
index 7e2bf04..c96d2ea 100644
--- a/.idea/dictionaries/herbert.xml
+++ b/.idea/dictionaries/herbert.xml
@@ -4,6 +4,7 @@
       <w>consts</w>
       <w>derivator</w>
       <w>encrypter</w>
+      <w>hmac</w>
       <w>kdbx</w>
     </words>
   </dictionary>
diff --git a/lib/src/crypto/key_encrypter_kdf.dart b/lib/src/crypto/key_encrypter_kdf.dart
index dc902f0..9a98e6a 100644
--- a/lib/src/crypto/key_encrypter_kdf.dart
+++ b/lib/src/crypto/key_encrypter_kdf.dart
@@ -19,6 +19,7 @@ class KdfField<T> {
   final String field;
   final ValueType<T> type;
 
+  static final uuid = KdfField('\$UUID', ValueType.typeBytes);
   static final salt = KdfField('S', ValueType.typeBytes);
   static final parallelism = KdfField('P', ValueType.typeUInt32);
   static final memory = KdfField('M', ValueType.typeUInt64);
@@ -45,6 +46,10 @@ class KdfField<T> {
   }
 
   T read(VarDictionary dict) => dict.get(type, field);
+  void write(VarDictionary dict, T value) => dict.set(type, field, value);
+  VarDictionaryItem<T> item(T value) =>
+      VarDictionaryItem<T>(field, type, value);
+
   String debug(VarDictionary dict) {
     final value = dict.get(type, field);
     final strValue = type == ValueType.typeBytes
@@ -61,6 +66,11 @@ class KeyEncrypterKdf {
     '72Nt34wpREuR96mkA+MKDA==': KdfType.Argon2,
     'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes,
   };
+  static KdbxUuid kdfUuidForType(KdfType type) {
+    String uuid =
+        kdfUuids.entries.firstWhere((element) => element.value == type).key;
+    return KdbxUuid(uuid);
+  }
 
   final Argon2 argon2;
 
diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart
index 2d62e24..cd41715 100644
--- a/lib/src/kdbx_format.dart
+++ b/lib/src/kdbx_format.dart
@@ -119,7 +119,7 @@ class HashCredentials implements Credentials {
 }
 
 class KdbxFile {
-  KdbxFile(this.credentials, this.header, this.body) {
+  KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) {
     for (final obj in _allObjects) {
       obj.file = this;
     }
@@ -136,6 +136,7 @@ class KdbxFile {
     protectedValues[node] = value;
   }
 
+  final KdbxFormat kdbxFormat;
   final Credentials credentials;
   final KdbxHeader header;
   final KdbxBody body;
@@ -147,19 +148,30 @@ class KdbxFile {
       _dirtyObjectsChanged.stream;
 
   Uint8List save() {
-    assert(header.versionMajor == 3);
     final output = BytesBuilder();
     final writer = WriterHelper(output);
     header.generateSalts();
     header.write(writer);
-
-    final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
-    final gen = ProtectedSaltGenerator(streamKey);
-
-    body.meta.headerHash.set(
-        (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List)
-            .buffer);
-    body.writeV3(writer, this, gen);
+    final headerHash =
+        (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List);
+
+    if (header.versionMajor <= 3) {
+      final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
+      final gen = ProtectedSaltGenerator(streamKey);
+
+      body.meta.headerHash.set(headerHash.buffer);
+      body.writeV3(writer, this, gen);
+    } else if (header.versionMajor <= 4) {
+      final headerBytes = writer.output.toBytes();
+      writer.writeBytes(headerHash);
+      final gen = kdbxFormat._createProtectedSaltGenerator(header);
+      final keys = kdbxFormat._computeKeysV4(header, credentials);
+      final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey);
+      writer.writeBytes(headerHmac.bytes as Uint8List);
+      body.writeV4(writer, this, gen, keys);
+    } else {
+      throw UnsupportedError('Unsupported version ${header.versionMajor}');
+    }
     dirtyObjects.clear();
     _dirtyObjectsChanged.add(dirtyObjects);
     return output.toBytes();
@@ -226,6 +238,24 @@ class KdbxBody extends KdbxNode {
     writer.writeBytes(encrypted);
   }
 
+  void writeV4(WriterHelper writer, KdbxFile kdbxFile,
+      ProtectedSaltGenerator saltGenerator, _KeysV4 keys) {
+    final bodyWriter = WriterHelper();
+    final xml = generateXml(saltGenerator);
+    kdbxFile.header.writeInnerHeader(bodyWriter);
+    bodyWriter.writeBytes(utf8.encode(xml.toXmlString()) as Uint8List);
+    final Uint8List compressedBytes =
+        (kdbxFile.header.compression == Compression.gzip
+            ? GZipCodec().encode(bodyWriter.output.toBytes())
+            : bodyWriter.output.toBytes()) as Uint8List;
+    final encrypted = _encryptV4(
+      kdbxFile,
+      compressedBytes,
+      keys.cipherKey,
+    );
+    writer.writeBytes(encrypted);
+  }
+
   Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) {
     final byteWriter = WriterHelper();
     byteWriter.writeBytes(
@@ -240,6 +270,23 @@ class KdbxBody extends KdbxNode {
     return encrypted;
   }
 
+  Uint8List _encryptV4(
+      KdbxFile kdbxFile, Uint8List compressedBytes, Uint8List cipherKey) {
+    final header = kdbxFile.header;
+    final cipherId = base64.encode(header.fields[HeaderFields.CipherID].bytes);
+    if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.aes].uuid) {
+      _logger.fine('We need AES');
+      final result = kdbxFile.kdbxFormat
+          ._encryptContentV4Aes(header, cipherKey, compressedBytes);
+      _logger.fine('Result: ${ByteUtils.toHexList(result)}');
+      return result;
+    } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
+      _logger.fine('We need chacha20');
+    } else {
+      throw UnsupportedError('Unsupported cipherId $cipherId');
+    }
+  }
+
   xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
     final rootGroupNode = rootGroup.toXml();
     // update protected values...
@@ -280,6 +327,13 @@ class KdbxBody extends KdbxNode {
   }
 }
 
+class _KeysV4 {
+  _KeysV4(this.hmacKey, this.cipherKey);
+
+  final Uint8List hmacKey;
+  final Uint8List cipherKey;
+}
+
 class KdbxFormat {
   KdbxFormat([this.argon2]);
 
@@ -297,7 +351,7 @@ class KdbxFormat {
     );
     final rootGroup = KdbxGroup.create(parent: null, name: name);
     final body = KdbxBody.create(meta, rootGroup);
-    return KdbxFile(credentials, header, body);
+    return KdbxFile(this, credentials, header, body);
   }
 
   KdbxFile read(Uint8List input, Credentials credentials) {
@@ -327,10 +381,10 @@ class KdbxFormat {
     if (header.compression == Compression.gzip) {
       final xml = GZipCodec().decode(blocks);
       final string = utf8.decode(xml);
-      return KdbxFile(credentials, header, _loadXml(header, string));
+      return KdbxFile(this, credentials, header, _loadXml(header, string));
     } else {
       return KdbxFile(
-          credentials, header, _loadXml(header, utf8.decode(blocks)));
+          this, credentials, header, _loadXml(header, utf8.decode(blocks)));
     }
   }
 
@@ -347,27 +401,12 @@ class KdbxFormat {
     _logger
         .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}');
     _logger.finest('Header hash matches.');
-    final key = _computeKeysV4(header, credentials);
-    final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
-    if (masterSeed.length != 32) {
-      throw const FormatException('Master seed must be 32 bytes.');
-    }
-//    final keyWithSeed = Uint8List(65);
-//    keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
-//    keyWithSeed.replaceRange(
-//        masterSeed.length, masterSeed.length + key.length, key);
-//    keyWithSeed[64] = 1;
-    _logger.fine('masterSeed: ${ByteUtils.toHexList(masterSeed)}');
-    final keyWithSeed = masterSeed + key + Uint8List.fromList([1]);
-    assert(keyWithSeed.length == 65);
-    final cipher = crypto.sha256.convert(keyWithSeed.sublist(0, 64));
-    final hmacKey = crypto.sha512.convert(keyWithSeed);
-    _logger.fine('hmacKey: ${ByteUtils.toHexList(hmacKey.bytes)}');
+    final keys = _computeKeysV4(header, credentials);
     final headerHmac =
-        _getHeaderHmac(header, reader, hmacKey.bytes as Uint8List);
+        _getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey);
     final expectedHmac = reader.readBytes(headerHmac.bytes.length);
-    _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
-    _logger.fine('Actual  : ${ByteUtils.toHexList(headerHmac.bytes)}');
+//    _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
+//    _logger.fine('Actual  : ${ByteUtils.toHexList(headerHmac.bytes)}');
     if (!ByteUtils.eq(hash, actualHash)) {
       throw KdbxInvalidKeyException();
     }
@@ -375,17 +414,16 @@ class KdbxFormat {
 //    final blockreader.readBytes(32);
     final bodyStuff = hmacBlockTransformer(reader);
     _logger.fine('body decrypt: ${ByteUtils.toHexList(bodyStuff)}');
-    final decrypted = decrypt(header, bodyStuff, cipher.bytes as Uint8List);
+    final decrypted = decrypt(header, bodyStuff, keys.cipherKey);
     _logger.finer('compression: ${header.compression}');
     if (header.compression == Compression.gzip) {
       final content = GZipCodec().decode(decrypted) as Uint8List;
       final contentReader = ReaderHelper(content);
       final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4);
-      _logger.fine('inner header fields: $headerFields');
+//      _logger.fine('inner header fields: $headerFields');
       header.innerFields.addAll(headerFields);
       final xml = utf8.decode(contentReader.readRemaining());
-      _logger.fine('content: $xml');
-      return KdbxFile(credentials, header, _loadXml(header, xml));
+      return KdbxFile(this, credentials, header, _loadXml(header, xml));
     }
     return null;
   }
@@ -414,6 +452,7 @@ class KdbxFormat {
       return result;
     } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
       _logger.fine('We need chacha20');
+      throw UnsupportedError('chacha20 not yet supported $cipherId');
     } else {
       throw UnsupportedError('Unsupported cipherId $cipherId');
     }
@@ -422,30 +461,45 @@ class KdbxFormat {
 //  Uint8List _transformDataV4Aes() {
 //  }
 
-  crypto.Digest _getHeaderHmac(
-      KdbxHeader header, ReaderHelper reader, Uint8List key) {
+  crypto.Digest _getHeaderHmac(Uint8List headerBytes, Uint8List key) {
     final writer = WriterHelper()
       ..writeUint32(0xffffffff)
       ..writeUint32(0xffffffff)
       ..writeBytes(key);
     final hmacKey = crypto.sha512.convert(writer.output.toBytes()).bytes;
-    final src = reader.byteData.sublist(0, header.endPos);
+    final src = headerBytes;
     final hmacKeyStuff = crypto.Hmac(crypto.sha256, hmacKey);
     _logger.fine('keySha: ${ByteUtils.toHexList(hmacKey)}');
     _logger.fine('src: ${ByteUtils.toHexList(src)}');
     return hmacKeyStuff.convert(src);
   }
 
-  Uint8List _computeKeysV4(KdbxHeader header, Credentials credentials) {
+  _KeysV4 _computeKeysV4(KdbxHeader header, Credentials credentials) {
     final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
     final kdfParameters = header.readKdfParameters;
-    assert(masterSeed.length == 32);
+    if (masterSeed.length != 32) {
+      throw const FormatException('Master seed must be 32 bytes.');
+    }
+
     final credentialHash = credentials.getHash();
     _logger.fine('MasterSeed: ${ByteUtils.toHexList(masterSeed)}');
     _logger.fine('credentialHash: ${ByteUtils.toHexList(credentialHash)}');
-    final ret = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
-    _logger.fine('keyv4: ${ByteUtils.toHexList(ret)}');
-    return ret;
+    final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
+    _logger.fine('keyv4: ${ByteUtils.toHexList(key)}');
+
+//    final keyWithSeed = Uint8List(65);
+//    keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
+//    keyWithSeed.replaceRange(
+//        masterSeed.length, masterSeed.length + key.length, key);
+//    keyWithSeed[64] = 1;
+    _logger.fine('masterSeed: ${ByteUtils.toHexList(masterSeed)}');
+    final keyWithSeed = masterSeed + key + Uint8List.fromList([1]);
+    assert(keyWithSeed.length == 65);
+    final cipher = crypto.sha256.convert(keyWithSeed.sublist(0, 64));
+    final hmacKey = crypto.sha512.convert(keyWithSeed);
+    _logger.fine('hmacKey: ${ByteUtils.toHexList(hmacKey.bytes)}');
+
+    return _KeysV4(hmacKey.bytes as Uint8List, cipher.bytes as Uint8List);
   }
 
   ProtectedSaltGenerator _createProtectedSaltGenerator(KdbxHeader header) {
@@ -515,11 +569,11 @@ class KdbxFormat {
   }
 
   Uint8List _decryptContentV4(
-      KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
+      KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) {
     final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
     final decryptCipher = CBCBlockCipher(AESFastEngine());
     decryptCipher.init(
-        false, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
+        false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
     final paddedDecrypted =
         AesHelper.processBlocks(decryptCipher, encryptedPayload);
 
@@ -527,6 +581,19 @@ class KdbxFormat {
     return decrypted;
   }
 
+  /// TODO combine this with [_decryptContentV4]
+  Uint8List _encryptContentV4Aes(
+      KdbxHeader header, Uint8List cipherKey, Uint8List bytes) {
+    final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
+    final decryptCipher = CBCBlockCipher(AESFastEngine());
+    decryptCipher.init(
+        true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
+    final paddedDecrypted = AesHelper.processBlocks(decryptCipher, bytes);
+
+    final decrypted = AesHelper.unpad(paddedDecrypted);
+    return decrypted;
+  }
+
   static Uint8List _generateMasterKeyV3(
       KdbxHeader header, Credentials credentials) {
     final rounds = ReaderHelper.singleUint64(
diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart
index aa07b04..88c8033 100644
--- a/lib/src/kdbx_header.dart
+++ b/lib/src/kdbx_header.dart
@@ -1,11 +1,14 @@
+import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:crypto/crypto.dart' as crypto;
+import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
 import 'package:kdbx/src/internal/byte_utils.dart';
 import 'package:kdbx/src/internal/consts.dart';
 import 'package:kdbx/src/kdbx_var_dictionary.dart';
 import 'package:logging/logging.dart';
 import 'package:meta/meta.dart';
+import 'package:kdbx/src/utils/scope_functions.dart';
 
 final _logger = Logger('kdbx.header');
 
@@ -13,6 +16,11 @@ class Consts {
   static const FileMagic = 0x9AA2D903;
 
   static const Sig2Kdbx = 0xB54BFB67;
+  static const DefaultKdfSaltLength = 32;
+  static const DefaultKdfParallelism = 1;
+  static const DefaultKdfIterations = 2;
+  static const DefaultKdfMemory = 1024 * 1024;
+  static const DefaultKdfVersion = 0x13;
 }
 
 enum Compression {
@@ -81,7 +89,8 @@ class KdbxHeader {
     @required this.versionMajor,
     @required this.fields,
     @required this.endPos,
-  });
+    Map<InnerHeaderFields, InnerHeaderField> innerFields,
+  }) : innerFields = innerFields ?? {};
 
   KdbxHeader.create()
       : this(
@@ -93,6 +102,17 @@ class KdbxHeader {
           endPos: null,
         );
 
+  KdbxHeader.createV4()
+      : this(
+          sig1: Consts.FileMagic,
+          sig2: Consts.Sig2Kdbx,
+          versionMinor: 1,
+          versionMajor: 4,
+          fields: _defaultFieldValuesV4(),
+          innerFields: _defaultInnerFieldValuesV4(),
+          endPos: null,
+        );
+
   static List<HeaderFields> _requiredFields(int majorVersion) {
     if (majorVersion < 3) {
       throw KdbxUnsupportedException('Unsupported version: $majorVersion');
@@ -113,12 +133,22 @@ class KdbxHeader {
 //            HeaderFields.InnerRandomStreamID
           ];
     } else {
-      // TODO kdbx 4 support
-      throw KdbxUnsupportedException('We do not support kdbx 4.x right now');
-      return baseHeaders + [HeaderFields.KdfParameters]; // ignore: dead_code
+      return baseHeaders + [HeaderFields.KdfParameters];
     }
   }
 
+  static VarDictionary _createKdfDefaultParameters() {
+    return VarDictionary([
+      KdfField.uuid
+          .item(KeyEncrypterKdf.kdfUuidForType(KdfType.Argon2).toBytes()),
+      KdfField.salt.item(ByteUtils.randomBytes(Consts.DefaultKdfSaltLength)),
+      KdfField.parallelism.item(Consts.DefaultKdfParallelism),
+      KdfField.iterations.item(Consts.DefaultKdfIterations),
+      KdfField.memory.item(Consts.DefaultKdfMemory),
+      KdfField.version.item(Consts.DefaultKdfVersion),
+    ]);
+  }
+
   void _validate() {
     for (HeaderFields required in _requiredFields(versionMajor)) {
       if (fields[required] == null) {
@@ -127,6 +157,18 @@ class KdbxHeader {
     }
   }
 
+  void _validateInner() {
+    final requiredFields = [
+      InnerHeaderFields.InnerRandomStreamID,
+      InnerHeaderFields.InnerRandomStreamKey
+    ];
+    for (final field in requiredFields) {
+      if (innerFields[field] == null) {
+        throw KdbxCorruptedFileException('Missing inner header $field');
+      }
+    }
+  }
+
   void _setHeaderField(HeaderFields field, Uint8List bytes) {
     fields[field] = HeaderField(field, bytes);
   }
@@ -145,9 +187,22 @@ class KdbxHeader {
       _setHeaderField(
           HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32));
       _setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16));
+    } else if (versionMajor < 5) {
+      _setInnerHeaderField(
+          InnerHeaderFields.InnerRandomStreamKey, ByteUtils.randomBytes(64));
+      final kdfParameters = readKdfParameters;
+      KdfField.salt.write(
+          kdfParameters, ByteUtils.randomBytes(Consts.DefaultKdfSaltLength));
+      //         var ivLength = this.dataCipherUuid.toString() === Consts.CipherId.ChaCha20 ? 12 : 16;
+      //        this.encryptionIV = Random.getBytes(ivLength);
+      final cipherId = base64.encode(fields[HeaderFields.CipherID].bytes);
+      final ivLength =
+          cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid ? 12 : 16;
+      _setHeaderField(
+          HeaderFields.EncryptionIV, ByteUtils.randomBytes(ivLength));
     } else {
       throw KdbxUnsupportedException(
-          'We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)');
+          'We do not support Kdbx 3.x and 4.x right now. ($versionMajor.$versionMinor)');
     }
   }
 
@@ -168,6 +223,26 @@ class KdbxHeader {
     _writeField(writer, HeaderFields.EndOfHeader);
   }
 
+  void writeInnerHeader(WriterHelper writer) {
+    _validateInner();
+    for (final field in InnerHeaderFields.values
+        .where((f) => f != InnerHeaderFields.EndOfHeader)) {
+      _writeInnerField(writer, field);
+    }
+    _writeInnerField(writer, InnerHeaderFields.EndOfHeader);
+  }
+
+  void _writeInnerField(WriterHelper writer, InnerHeaderFields field) {
+    final value = innerFields[field];
+    if (value == null) {
+      return;
+    }
+    _logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
+    writer.writeUint8(field.index);
+    _writeFieldSize(writer, value.bytes.lengthInBytes);
+    writer.writeBytes(value.bytes);
+  }
+
   void _writeField(WriterHelper writer, HeaderFields field) {
     final value = fields[field];
     if (value == null) {
@@ -201,6 +276,25 @@ class KdbxHeader {
                 .indexOf(ProtectedValueEncryption.salsa20))),
       ].map((f) => MapEntry(f.field, f)));
 
+  static Map<HeaderFields, HeaderField> _defaultFieldValuesV4() =>
+      _defaultFieldValues()
+        ..remove(HeaderFields.TransformRounds)
+        ..remove(HeaderFields.InnerRandomStreamID)
+        ..remove(HeaderFields.ProtectedStreamKey)
+        ..also((fields) {
+          fields[HeaderFields.KdfParameters] = HeaderField(
+              HeaderFields.KdfParameters,
+              _createKdfDefaultParameters().write());
+        });
+
+  static Map<InnerHeaderFields, InnerHeaderField>
+      _defaultInnerFieldValuesV4() => Map.fromEntries([
+            InnerHeaderField(
+                InnerHeaderFields.InnerRandomStreamID,
+                WriterHelper.singleUint32Bytes(ProtectedValueEncryption.values
+                    .indexOf(ProtectedValueEncryption.chaCha20)))
+          ].map((f) => MapEntry(f.field, f)));
+
   static KdbxHeader read(ReaderHelper reader) {
     // reading signature
     final sig1 = reader.readUint32();
@@ -281,7 +375,7 @@ class KdbxHeader {
   final int versionMinor;
   final int versionMajor;
   final Map<HeaderFields, HeaderField> fields;
-  final Map<InnerHeaderFields, InnerHeaderField> innerFields = {};
+  final Map<InnerHeaderFields, InnerHeaderField> innerFields;
 
   /// end position of the header, if we have been reading from a stream.
   final int endPos;
@@ -313,6 +407,9 @@ class KdbxHeader {
   VarDictionary get readKdfParameters => VarDictionary.read(
       ReaderHelper(fields[HeaderFields.KdfParameters].bytes));
 
+  void writeKdfParameters(VarDictionary kdfParameters) =>
+      _setHeaderField(HeaderFields.KdfParameters, kdfParameters.write());
+
   @override
   String toString() {
     return 'KdbxHeader{sig1: $sig1, sig2: $sig2, versionMajor: $versionMajor, versionMinor: $versionMinor}';
diff --git a/lib/src/kdbx_var_dictionary.dart b/lib/src/kdbx_var_dictionary.dart
index b1ff948..3f01e01 100644
--- a/lib/src/kdbx_var_dictionary.dart
+++ b/lib/src/kdbx_var_dictionary.dart
@@ -1,3 +1,5 @@
+import 'dart:typed_data';
+
 import 'package:kdbx/src/internal/byte_utils.dart';
 import 'package:logging/logging.dart';
 import 'package:meta/meta.dart';
@@ -103,10 +105,23 @@ class VarDictionary {
     return VarDictionary(items);
   }
 
+  static const DEFAULT_VERSION = 0x0100;
   final List<VarDictionaryItem<dynamic>> _items;
   final Map<String, VarDictionaryItem<dynamic>> _dict;
 
+  Uint8List write() {
+    final writer = WriterHelper();
+    writer.writeUint16(DEFAULT_VERSION);
+    for (final item in _items) {
+      item._valueType.encoder(writer, item._value);
+    }
+    writer.writeUint8(0);
+    return writer.output.toBytes();
+  }
+
   T get<T>(ValueType<T> type, String key) => _dict[key]?._value as T;
+  void set<T>(ValueType<T> type, String key, T value) =>
+      _dict[key] = VarDictionaryItem<T>(key, type, value);
 
   static VarDictionaryItem<dynamic> _readItem(ReaderHelper reader) {
     final type = reader.readUint8();
diff --git a/lib/src/utils/scope_functions.dart b/lib/src/utils/scope_functions.dart
new file mode 100644
index 0000000..44f8089
--- /dev/null
+++ b/lib/src/utils/scope_functions.dart
@@ -0,0 +1,17 @@
+/// https://github.com/YusukeIwaki/dart-kotlin_flavor/blob/74593dada94bdd8ca78946ad005d3a2624dc833f/lib/scope_functions.dart
+/// MIT license: https://github.com/YusukeIwaki/dart-kotlin_flavor/blob/74593dada94bdd8ca78946ad005d3a2624dc833f/LICENSE
+
+ReturnType run<ReturnType>(ReturnType operation()) {
+  return operation();
+}
+
+extension ScopeFunctionsForObject<T extends Object> on T {
+  ReturnType let<ReturnType>(ReturnType operationFor(T self)) {
+    return operationFor(this);
+  }
+
+  T also(void operationFor(T self)) {
+    operationFor(this);
+    return this;
+  }
+}
diff --git a/libargon2_ffi.dylib b/libargon2_ffi.dylib
index bf06d71a898dcc9fd8fda96dbe348c532adfbf98..4c38cd62fabfe43743c1e33c1f5a50c96925ca7d 100755
GIT binary patch
delta 7334
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

delta 7655
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

diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart
index d7c1af8..aba9315 100644
--- a/test/kdbx4_test.dart
+++ b/test/kdbx4_test.dart
@@ -110,8 +110,22 @@ void main() {
           kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
       final firstEntry = file.body.rootGroup.entries.first;
       final pwd = firstEntry.getString(KdbxKey('Password')).getText();
-      _logger.info('password: $pwd');
       expect(pwd, 'MyPassword');
     });
   });
+  group('Writing', () {
+    test('Create and save', () {
+      final credentials = Credentials(ProtectedValue.fromString('asdf'));
+      final kdbx = kdbxFormat.create(credentials, 'Test Keystore');
+      final rootGroup = kdbx.body.rootGroup;
+      final entry = KdbxEntry.create(kdbx, rootGroup);
+      rootGroup.addEntry(entry);
+      entry.setString(
+          KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
+      final saved = kdbx.save();
+
+      final loadedKdbx = kdbxFormat.read(saved, credentials);
+      File('test_v4.kdbx').writeAsBytesSync(saved);
+    });
+  });
 }