From 50aaab71bdb2de628a369cf14278e6ea4a9590b0 Mon Sep 17 00:00:00 2001 From: Herbert Poul 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 @@ consts derivator encrypter + hmac kdbx 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 { final String field; final ValueType 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 read(VarDictionary dict) => dict.get(type, field); + void write(VarDictionary dict, T value) => dict.set(type, field, value); + VarDictionaryItem item(T value) => + VarDictionaryItem(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 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 _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 _defaultFieldValuesV4() => + _defaultFieldValues() + ..remove(HeaderFields.TransformRounds) + ..remove(HeaderFields.InnerRandomStreamID) + ..remove(HeaderFields.ProtectedStreamKey) + ..also((fields) { + fields[HeaderFields.KdfParameters] = HeaderField( + HeaderFields.KdfParameters, + _createKdfDefaultParameters().write()); + }); + + static Map + _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 fields; - final Map innerFields = {}; + final Map 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> _items; final Map> _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(ValueType type, String key) => _dict[key]?._value as T; + void set(ValueType type, String key, T value) => + _dict[key] = VarDictionaryItem(key, type, value); static VarDictionaryItem _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 operation()) { + return operation(); +} + +extension ScopeFunctionsForObject on T { + ReturnType let(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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 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); + }); + }); }