From fdc6f149dc33156c6cd968db66502dba706b58cf Mon Sep 17 00:00:00 2001 From: Herbert Poul Date: Wed, 21 Aug 2019 12:57:58 +0200 Subject: [PATCH] a bit of refactoring --- .idea/dictionaries/herbert.xml | 1 + README.md | 11 ++ lib/src/internal/byte_utils.dart | 19 ++++ lib/src/internal/crypto_utils.dart | 84 +++++++++++++++ lib/src/kdbx_format.dart | 83 +++++++++++++++ lib/src/kdbx_header.dart | 162 +---------------------------- pubspec.yaml | 1 + test/FooBar.content.xml | 80 ++++++++++++++ test/FooBar.kdbx | Bin 1150 -> 1358 bytes test/kdbx_test.dart | 1 + 10 files changed, 283 insertions(+), 159 deletions(-) create mode 100644 lib/src/internal/byte_utils.dart create mode 100644 lib/src/internal/crypto_utils.dart create mode 100644 lib/src/kdbx_format.dart create mode 100644 test/FooBar.content.xml diff --git a/.idea/dictionaries/herbert.xml b/.idea/dictionaries/herbert.xml index 5d20749..93abe3f 100644 --- a/.idea/dictionaries/herbert.xml +++ b/.idea/dictionaries/herbert.xml @@ -2,6 +2,7 @@ consts + derivator kdbx diff --git a/README.md b/README.md index 00b409d..795f935 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # kdbx.dart +**this is just an experiment right now** + KeepassX format implementation in pure dart. Very much based on https://github.com/keeweb/kdbxweb/ @@ -11,3 +13,12 @@ TODO ## Features and bugs * Only supports v3. + +# TODO + +* For v4 argon2 support would be required. Unfortunately there are no dart + implementations, or bindings yet. (as far as I can find). + * Reference implementation: https://github.com/P-H-C/phc-winner-argon2 + * Rust: https://github.com/bryant/argon2rs/blob/master/src/argon2.rs + * C#: https://github.com/mheyman/Isopoh.Cryptography.Argon2 + diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart new file mode 100644 index 0000000..d35467c --- /dev/null +++ b/lib/src/internal/byte_utils.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +class ByteUtils { + static bool eq(Uint8List a, Uint8List b) { + if (a.length != b.length) { + return false; + } + for (int i = a.length - 1; i >= 0; i--) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + static String toHex(int val) => '0x${val.toRadixString(16)}'; + + static String toHexList(Uint8List list) => list.map((val) => toHex(val)).join(' '); +} diff --git a/lib/src/internal/crypto_utils.dart b/lib/src/internal/crypto_utils.dart new file mode 100644 index 0000000..f0e2fc6 --- /dev/null +++ b/lib/src/internal/crypto_utils.dart @@ -0,0 +1,84 @@ + + +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; + +class CryptoUtils { + +} + +/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b +class AesHelper { + static const CBC_MODE = 'CBC'; + static const CFB_MODE = 'CFB'; + + // AES key size + static const KEY_SIZE = 32; // 32 byte key for AES-256 + static const ITERATION_COUNT = 1000; + + static Uint8List deriveKey( + Uint8List password, { + Uint8List salt, + int iterationCount = ITERATION_COUNT, + int derivedKeyLength = KEY_SIZE, + }) { + final Pbkdf2Parameters params = Pbkdf2Parameters(salt, iterationCount, derivedKeyLength); + final KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 16)); + keyDerivator.init(params); + + return keyDerivator.process(password); + } + + static String decrypt(Uint8List derivedKey, Uint8List cipherIvBytes, {String mode = CBC_MODE}) { +// Uint8List derivedKey = deriveKey(password); + final KeyParameter keyParam = KeyParameter(derivedKey); + final BlockCipher aes = AESFastEngine(); + +// Uint8List cipherIvBytes = base64.decode(ciphertext); + final Uint8List iv = Uint8List(aes.blockSize)..setRange(0, aes.blockSize, cipherIvBytes); + + BlockCipher cipher; + final ParametersWithIV params = ParametersWithIV(keyParam, iv); + switch (mode) { + case CBC_MODE: + cipher = CBCBlockCipher(aes); + break; + case CFB_MODE: + cipher = CFBBlockCipher(aes, aes.blockSize); + break; + default: + throw ArgumentError('incorrect value of the "mode" parameter'); + break; + } + cipher.init(false, params); + + final int cipherLen = cipherIvBytes.length - aes.blockSize; + final Uint8List cipherBytes = Uint8List(cipherLen)..setRange(0, cipherLen, cipherIvBytes, aes.blockSize); + final Uint8List paddedText = processBlocks(cipher, cipherBytes); + final Uint8List textBytes = unpad(paddedText); + + return String.fromCharCodes(textBytes); + } + + static Uint8List unpad(Uint8List src) { + final pad = PKCS7Padding(); + pad.init(null); + + final int padLength = pad.padCount(src); + final int len = src.length - padLength; + + return Uint8List(len)..setRange(0, len, src); + } + + static Uint8List processBlocks(BlockCipher cipher, Uint8List inp) { + final out = Uint8List(inp.lengthInBytes); + + for (var offset = 0; offset < inp.lengthInBytes;) { + final len = cipher.processBlock(inp, offset, out, offset); + offset += len; + } + + return out; + } +} diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart new file mode 100644 index 0000000..9ec1b5c --- /dev/null +++ b/lib/src/kdbx_format.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart' as crypto; +import 'package:kdbx/src/internal/byte_utils.dart'; +import 'package:kdbx/src/internal/crypto_utils.dart'; +import 'package:kdbx/src/kdbx_header.dart'; +import 'package:logging/logging.dart'; +import 'package:pointycastle/export.dart'; + +final _logger = Logger('kdbx.format'); + + +class KdbxFormat { + static Future read(Uint8List input, Credentials credentials) async { + final reader = ReaderHelper(input); + final header = await KdbxHeader.read(reader); + _loadV3(header, reader, credentials); + } + + static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) { +// _getMasterKeyV3(header, credentials); + final masterKey = _generateMasterKeyV3(header, credentials); + final encryptedPayload = reader.readRemaining(); + final content = _decryptContent(header, masterKey, encryptedPayload); + final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); + + _logger.finer('compression: ${header.compression}'); + if (header.compression == Compression.gzip) { + final xml = GZipCodec().decode(blocks); + final string = utf8.decode(xml); + print('xml: $string'); + } + +// final result = utf8.decode(decrypted); +// final aesEngine = AESFastEngine(); +// aesEngine.init(true, KeyParameter(seed)); +// final key = AesHelper.deriveKey(keyComposite.bytes as Uint8List, salt: seed, iterationCount: rounds, derivedKeyLength: 32); +// final masterKey = Uint8List.fromList(key + masterSeed.asUint8List()); +// print('key length: ${key.length} + ${masterSeed.lengthInBytes} = ${masterKey.lengthInBytes} (${masterKey.lengthInBytes} bytes)'); + +// final result = AesHelper.decrypt(masterKey, reader.readRemaining()); +// print('before : ${_toHexList(encryptedPayload)}'); + } + + static Uint8List _decryptContent(KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) { + final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; + final decryptCipher = CBCBlockCipher(AESFastEngine()); + decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); + final decrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); + + final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; + + _logger.finest('streamStart: ${ByteUtils.toHexList(streamStart.asUint8List())}'); + _logger.finest('actual : ${ByteUtils.toHexList(decrypted.sublist(0, streamStart.lengthInBytes))}'); + + if (!ByteUtils.eq(streamStart.asUint8List(), decrypted.sublist(0, streamStart.lengthInBytes))) { + throw KdbxInvalidKeyException(); + } + final content = decrypted.sublist(streamStart.lengthInBytes); + return content; + } + + static Uint8List _generateMasterKeyV3(KdbxHeader header, Credentials credentials) { + final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first; + final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List(); + final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; + _logger.finer('Rounds: $rounds'); + + final cipher = ECBBlockCipher(AESFastEngine())..init(true, KeyParameter(seed)); + final pwHash = credentials.getHash(); + var transformedKey = pwHash; + for (int i = 0; i < rounds; i++) { + transformedKey = AesHelper.processBlocks(cipher, transformedKey); + } + transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List; + final masterKey = + crypto.sha256.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey)).bytes as Uint8List; + return masterKey; + } + +} diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index 56a0982..0c49e05 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'package:crypto/crypto.dart' as crypto; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:logging/logging.dart'; import 'package:pointycastle/export.dart'; @@ -49,10 +50,6 @@ class HeaderField { String get name => field.toString(); } -String _toHex(int val) => '0x${val.toRadixString(16)}'; - -String _toHexList(Uint8List list) => list.map((val) => _toHex(val)).join(' '); - class KdbxHeader { KdbxHeader({this.sig1, this.sig2, this.versionMinor, this.versionMajor, this.fields}); @@ -61,7 +58,7 @@ class KdbxHeader { final sig1 = reader.readUint32(); final sig2 = reader.readUint32(); if (!(sig1 == Consts.FileMagic && sig2 == Consts.Sig2Kdbx)) { - throw UnsupportedError('Unsupported file structure. ${_toHex(sig1)}, ${_toHex(sig2)}'); + throw UnsupportedError('Unsupported file structure. ${ByteUtils.toHex(sig1)}, ${ByteUtils.toHex(sig2)}'); } // reading version @@ -137,86 +134,7 @@ class KdbxUnsupportedException implements KdbxException { final String hint; } -class KdbxFormat { - static Future read(Uint8List input, Credentials credentials) async { - final reader = ReaderHelper(input); - final header = await KdbxHeader.read(reader); - _loadV3(header, reader, credentials); - } - - static void _loadV3(KdbxHeader header, ReaderHelper reader, Credentials credentials) { -// _getMasterKeyV3(header, credentials); - final pwHash = credentials.getHash(); - final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List(); - final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first; - final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; - final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; - _logger.finer('Rounds: $rounds'); - final cipher = ECBBlockCipher(AESFastEngine()); - final encryptedPayload = reader.readRemaining(); - cipher.init(true, KeyParameter(seed)); - - var transformedKey = pwHash; - for (int i = 0; i < rounds; i++) { - transformedKey = AesHelper._processBlocks(cipher, transformedKey); - } - transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List; - final masterKey = - crypto.sha256.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey)).bytes as Uint8List; - final decryptCipher = CBCBlockCipher(AESFastEngine()); - decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); -// final decrypted = decryptCipher.process(encryptedPayload); - final decrypted = AesHelper._processBlocks(decryptCipher, encryptedPayload); - - final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; - print('streamStart: ${_toHexList(streamStart.asUint8List())}'); - print('actual : ${_toHexList(decrypted.sublist(0, streamStart.lengthInBytes))}'); - - if (!_eq(streamStart.asUint8List(), decrypted.sublist(0, streamStart.lengthInBytes))) { - throw KdbxInvalidKeyException(); - } - final content = decrypted.sublist(streamStart.lengthInBytes); - final blocks = HashedBlockReader.readBlocks(ReaderHelper(content)); - - print('compression: ${header.compression}'); - if (header.compression == Compression.gzip) { - final xml = GZipCodec().decode(blocks); - final string = utf8.decode(xml); - print('xml: $string'); - } - -// final result = utf8.decode(decrypted); -// final aesEngine = AESFastEngine(); -// aesEngine.init(true, KeyParameter(seed)); -// final key = AesHelper.deriveKey(keyComposite.bytes as Uint8List, salt: seed, iterationCount: rounds, derivedKeyLength: 32); -// final masterKey = Uint8List.fromList(key + masterSeed.asUint8List()); -// print('key length: ${key.length} + ${masterSeed.lengthInBytes} = ${masterKey.lengthInBytes} (${masterKey.lengthInBytes} bytes)'); - -// final result = AesHelper.decrypt(masterKey, reader.readRemaining()); - print('before : ${_toHexList(encryptedPayload)}'); - } - - static void _getMasterKeyV3(KdbxHeader header, Credentials credentials) { - final pwHash = credentials.getHash(); - final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List(); - final rounds = header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first; - final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; - final key = AesHelper.deriveKey(pwHash, salt: seed, iterationCount: rounds); - } -} - -bool _eq(Uint8List a, Uint8List b) { - if (a.length != b.length) { - return false; - } - for (int i = a.length - 1; i >= 0; i--) { - if (a[i] != b[i]) { - return false; - } - } - return true; -} class HashedBlockReader { static Uint8List readBlocks(ReaderHelper reader) => @@ -229,7 +147,7 @@ class HashedBlockReader { final blockSize = reader.readUint32(); if (blockSize > 0) { final blockData = reader.readBytes(blockSize).asUint8List(); - if (!_eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) { + if (!ByteUtils.eq(crypto.sha256.convert(blockData).bytes as Uint8List, blockHash.asUint8List())) { throw KdbxCorruptedFileException(); } yield blockData; @@ -259,77 +177,3 @@ class ReaderHelper { Uint8List readRemaining() => data.sublist(pos); } -/// https://gist.github.com/proteye/e54eef1713e1fe9123d1eb04c0a5cf9b -class AesHelper { - static const CBC_MODE = 'CBC'; - static const CFB_MODE = 'CFB'; - - // AES key size - static const KEY_SIZE = 32; // 32 byte key for AES-256 - static const ITERATION_COUNT = 1000; - - static Uint8List deriveKey( - Uint8List password, { - Uint8List salt, - int iterationCount = ITERATION_COUNT, - int derivedKeyLength = KEY_SIZE, - }) { - Pbkdf2Parameters params = Pbkdf2Parameters(salt, iterationCount, derivedKeyLength); - KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 16)); - keyDerivator.init(params); - - return keyDerivator.process(password); - } - - static String decrypt(Uint8List derivedKey, Uint8List cipherIvBytes, {String mode = CBC_MODE}) { -// Uint8List derivedKey = deriveKey(password); - KeyParameter keyParam = KeyParameter(derivedKey); - BlockCipher aes = AESFastEngine(); - -// Uint8List cipherIvBytes = base64.decode(ciphertext); - Uint8List iv = Uint8List(aes.blockSize)..setRange(0, aes.blockSize, cipherIvBytes); - - BlockCipher cipher; - ParametersWithIV params = ParametersWithIV(keyParam, iv); - switch (mode) { - case CBC_MODE: - cipher = CBCBlockCipher(aes); - break; - case CFB_MODE: - cipher = CFBBlockCipher(aes, aes.blockSize); - break; - default: - throw ArgumentError('incorrect value of the "mode" parameter'); - break; - } - cipher.init(false, params); - - int cipherLen = cipherIvBytes.length - aes.blockSize; - Uint8List cipherBytes = new Uint8List(cipherLen)..setRange(0, cipherLen, cipherIvBytes, aes.blockSize); - Uint8List paddedText = _processBlocks(cipher, cipherBytes); - Uint8List textBytes = unpad(paddedText); - - return String.fromCharCodes(textBytes); - } - - static Uint8List unpad(Uint8List src) { - final pad = PKCS7Padding(); - pad.init(null); - - int padLength = pad.padCount(src); - int len = src.length - padLength; - - return Uint8List(len)..setRange(0, len, src); - } - - static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) { - var out = Uint8List(inp.lengthInBytes); - - for (var offset = 0; offset < inp.lengthInBytes;) { - var len = cipher.processBlock(inp, offset, out, offset); - offset += len; - } - - return out; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 67fba76..3af2f8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: logging: '>=0.11.3+2 <1.0.0' crypto: '>=2.0.0 <3.0.0' pointycastle: ^1.0.1 + xml: ^3.5.0 dev_dependencies: logging_appenders: '>=0.1.0 <1.0.0' diff --git a/test/FooBar.content.xml b/test/FooBar.content.xml new file mode 100644 index 0000000..839e630 --- /dev/null +++ b/test/FooBar.content.xml @@ -0,0 +1,80 @@ + + + + KdbxWeb + a9XeOPjkxVOzggrVtvoEpBIc07uqIShumzKMU+/lj04= + FooBar + 2019-08-20T13:16:06Z + + 2019-08-20T13:15:47Z + + 2019-08-20T13:15:47Z + 365 + + 2019-08-20T13:16:03Z + -1 + -1 + True + dVSBC/BAx70qcsy6XkrGJA== + 2019-08-20T13:15:47Z + AAAAAAAAAAAAAAAAAAAAAA== + 2019-08-20T13:15:47Z + 10 + 6291456 + + + + False + False + True + False + False + + + + + + + + LAQMkihXTkxhA2D2tE40Fg== + FooBar + + 49 + + 2019-08-20T13:15:47Z + 2019-08-20T13:16:06Z + 2019-08-20T13:16:06Z + 2019-08-20T13:15:47Z + False + 0 + 2019-08-20T13:15:47Z + + True + + null + null + AAAAAAAAAAAAAAAAAAAAAA== + + dVSBC/BAx70qcsy6XkrGJA== + Recycle Bin + + 43 + + 2019-08-20T13:15:47Z + 2019-08-20T13:15:47Z + 2019-08-20T13:15:47Z + 2019-08-20T13:15:47Z + False + 0 + 2019-08-20T13:15:47Z + + True + + False + False + AAAAAAAAAAAAAAAAAAAAAA== + + + + + \ No newline at end of file diff --git a/test/FooBar.kdbx b/test/FooBar.kdbx index 03294877c43b06cc45455b86b11b504f09c218a0..e352347576233a4acd5508470e6590e81193c275 100644 GIT binary patch delta 1334 zcmV-61-9d_Y+Jbrjc7MFSb4SDpAONL81Qtg4 zZFT-pDVo$My3>po@Op2r=>DgkT#ffL76OoL!vot%^?8~+Uz|E*MA}iizmI+ielWN>; z-jw!0(OHwEkAL#85G~=a$IHw4B1SPl`r4FqM$0yNNiI)d!o@0H^w1GGU1yfsk7DTs z&Xg`Ln0>&8aab}_$1Y^YbN$@WZW}g7#a;Y4R^F6`(gBOi`q1QiIKz!$66+=dY92GJXMc=VbP*cVD6(k_`ypY3mymTV zXjnJyJFk|G7pRT1GJT!cU4a`cbo4QF_%glTtW5|Iw<~Aj2_6FH&s@WdDazm}p% z9Ff@l3cY!QU zTa@&zV!8f|fIf@E;Kj8Z$znKEKoopRFORTPY=86|2)jJnG|YW@rVod^hZxMK;bc6> zPrUMC3%3IIl^FT7?a|fA)yW$-JZ_{{Ilb~f4{%-NHaR4=%n$`pD?~E z|9>s5oy_mIFIUtSKz6Y{&a+Af2tn9xIAkOdVNTl;dn^w;bv$iVb_n2oMVAIJ+RYkj z;_Owf`o97^SmHQKA2$|3J_jM9-k+y64RFFZt*CL;>~35xRDGrMC?p1-vUrgabij<@ zr#+7spd*~c4_W*+w}Ew^a>i;c&@jR(fq#6wp@#{%97rX`{D3dlq-2-ppsh7O%hN@X z*~UK0PtB#6)Qt6|Ntz96p!ek_E5%n;j_d+fp4-UGB=XJ^F4Ps5widbp`83f{pX*v{B#}xCM`BhOeREr!piVwBL-rp{cEjP?F&(Wo7&A z4`Xp+g~?h%a*XMi_LsTuw2paTNPkkdDcQB$#F>|+N+Uo`d5og@QO;L?+ zw%6a6L%x1Ut?*4D__Bx}%C`hTspxElA*$0-W=GHH%#4@fey z6B!+<%*$MH(Ugb5=f*Xu!qs7ny`}WvG)!W;UHeGSzxso@3%+D#AFEeoLN9A)9y@{l sTjD{-gX9tGBv&{1D>9y&8|)DMniBP)gFeIZ`{YwasQGEfRhmwY{bT-tcmMzZ delta 1124 zcmV-q1e^QL3jPR?DSvA|XnTj2XEn&Ua|H?IHbqYB;kE+Yi1*ZUwhyJoHzEZf0K{rV z#0h;>XUx}=R zih`8E6*p=KAOJ@ZNc50LYtE<`&V~H*PB+IF@%arzf} z$goBA?Y}5^=19-)9`=CB9jBjk%$Un*5A=k=89C*~|AE81&17QnqH#+rsvHrfTj1?% zC`@tm!*_gSD1R&pd5(8_++A&PVQ@4Z^TGY4r%0Z!3WI~0PML>(OFcmQx#R?3|G1EJ z+JF8(SFT%O&Tu)2--$}=c|sK;vPN4N-K)2YAxOXeP#rA8l$5R5x2`^a&Mp9xL{~5& z?4w?FMUHj^C+(fvaqD%DcpsBhYKRb#i=xyvoaWqvbbnB`^9u%gWI~j#S!hB*5<&=> zxgbh9%-Vn+-p{D$Xy*SU0S5Fr zi!lmC*>vrJpug$z2lLww$nR*!kdi98)b@_?qu_Y$;w0e_6pFd?#sqOpeRm|vDN%u31+u$Yt@rtRl!vzR42<;OfxEZXS{rUN#OlQGB#D7A|;i*(Pf1}$j5^xWPt&8iX=JUWu6PXPI z92huH-`c_m7K%U9XtnX6?)3uof@zog4xn0kRU8Gh^Gt-r88FDsO;KF2+Af%PdW2wD z2>O;M*m~m)#_VoV7ci!eBx8aYniI)iAM=wVN9Ik;1}y7xag5n@f|t4>pHL*bS6x6E zOAym1Geqh%N12{oN)NK|oM8F4q8KaHaY0KV4;Yxl6RHpMdb)pkEw$`$=J|kRpnE{a q|GEIZgO|B*MqYdH}N@tYMWw*wjg diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index 04f4398..2072d98 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:kdbx/kdbx.dart'; import 'package:kdbx/src/crypto/protected_value.dart'; +import 'package:kdbx/src/kdbx_format.dart'; import 'package:kdbx/src/kdbx_header.dart'; import 'package:logging/logging.dart'; import 'package:logging_appenders/logging_appenders.dart';