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 0329487..e352347 100644 Binary files a/test/FooBar.kdbx and b/test/FooBar.kdbx differ 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';