diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6c8f9..a101742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+1 + +- Minor fixes for kdbx 4.x + ## 0.3.0 - Initial support for kdbx 4.x diff --git a/lib/kdbx.dart b/lib/kdbx.dart index b5a9c2d..20d45d6 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -1,6 +1,7 @@ /// dart library for reading keepass file format (kdbx). library kdbx; +export 'src/crypto/key_encrypter_kdf.dart' show Argon2; export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_consts.dart'; @@ -9,7 +10,6 @@ export 'src/kdbx_dao.dart' show KdbxDao; export 'src/kdbx_entry.dart'; export 'src/kdbx_format.dart'; export 'src/kdbx_group.dart'; -export 'src/crypto/key_encrypter_kdf.dart' show Argon2; export 'src/kdbx_header.dart' show KdbxException, diff --git a/lib/src/crypto/key_encrypter_kdf.dart b/lib/src/crypto/key_encrypter_kdf.dart index 9a98e6a..e294bb2 100644 --- a/lib/src/crypto/key_encrypter_kdf.dart +++ b/lib/src/crypto/key_encrypter_kdf.dart @@ -67,7 +67,7 @@ class KeyEncrypterKdf { 'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes, }; static KdbxUuid kdfUuidForType(KdfType type) { - String uuid = + final uuid = kdfUuids.entries.firstWhere((element) => element.value == type).key; return KdbxUuid(uuid); } @@ -99,7 +99,8 @@ class KeyEncrypterKdf { return argon2.argon2( key, KdfField.salt.read(kdfParameters), - 65536, //KdfField.memory.read(kdfParameters), +// 65536, //KdfField.memory.read(kdfParameters), + KdfField.memory.read(kdfParameters) ~/ 1024, KdfField.iterations.read(kdfParameters), 32, KdfField.parallelism.read(kdfParameters), diff --git a/lib/src/crypto/protected_salt_generator.dart b/lib/src/crypto/protected_salt_generator.dart index b63f5bb..e788d2b 100644 --- a/lib/src/crypto/protected_salt_generator.dart +++ b/lib/src/crypto/protected_salt_generator.dart @@ -1,10 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:logging/logging.dart'; import 'package:crypto/crypto.dart'; import 'package:cryptography/cryptography.dart' as cryptography; import 'package:pointycastle/export.dart'; +final _logger = Logger('protected_salt_generator'); + class ProtectedSaltGenerator { factory ProtectedSaltGenerator(Uint8List key) { final hash = sha256.convert(key).bytes as Uint8List; @@ -24,6 +27,10 @@ class ProtectedSaltGenerator { String decryptBase64(String protectedValue) { final bytes = base64.decode(protectedValue); + if (bytes.isEmpty) { + _logger.warning('decoded base64 data has length 0'); + return null; + } final result = _cipher.process(bytes); final decrypted = utf8.decode(result); return decrypted; @@ -36,34 +43,54 @@ class ProtectedSaltGenerator { } class ChachaProtectedSaltGenerator implements ProtectedSaltGenerator { - ChachaProtectedSaltGenerator._(this._secretKey, this._nonce); + ChachaProtectedSaltGenerator._(this._secretKey, this._nonce, this._state); factory ChachaProtectedSaltGenerator.create(Uint8List key) { final hash = sha512.convert(key); final secretKey = hash.bytes.sublist(0, 32); final nonce = hash.bytes.sublist(32, 32 + 12); + return ChachaProtectedSaltGenerator._( - cryptography.SecretKey(secretKey), cryptography.SecretKey(nonce)); + cryptography.SecretKey(secretKey), + cryptography.SecretKey(nonce), + cryptography.chacha20.newState(cryptography.SecretKey(secretKey), + nonce: cryptography.SecretKey(nonce))); } final cryptography.SecretKey _secretKey; final cryptography.SecretKey _nonce; + final cryptography.KeyStreamCipherState _state; @override StreamCipher get _cipher => throw UnimplementedError(); @override String decryptBase64(String protectedValue) { - final result = cryptography.chacha20 - .decrypt(base64.decode(protectedValue), _secretKey, nonce: _nonce); - return utf8.decode(result); + final bytes = base64.decode(protectedValue); + if (bytes.isEmpty) { + _logger.warning('decoded base64 data has length 0'); + return null; + } + final result = _state.convert(bytes); +// try { + _logger.fine('decoding protected value.'); + final ret = utf8.decode(result); + _logger.fine('Successfully decoded stuff.'); + return ret; +// } on FormatException catch (e, stackTrace) { +// final ret = utf8.decode(result, allowMalformed: true); +// _logger.severe( +// 'Error while decoding utf8. ignoring malformed. result: {$ret}', +// e, +// stackTrace); +// return ret; +// } } @override String encryptToBase64(String plainValue) { final input = utf8.encode(plainValue) as Uint8List; - final encrypted = - cryptography.chacha20.encrypt(input, _secretKey, nonce: _nonce); + final encrypted = _state.convert(input); return base64.encode(encrypted); } } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index cd41715..58293ca 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; @@ -282,6 +281,8 @@ class KdbxBody extends KdbxNode { return result; } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) { _logger.fine('We need chacha20'); + // TODO can we combine this with _encryptV3? + throw UnsupportedError('Unsupported cipher chacha20 for kdbx 4.x'); } else { throw UnsupportedError('Unsupported cipherId $cipherId'); } @@ -405,16 +406,16 @@ class KdbxFormat { final headerHmac = _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)}'); - if (!ByteUtils.eq(hash, actualHash)) { + _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}'); + _logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}'); + if (!ByteUtils.eq(headerHmac.bytes, expectedHmac)) { throw KdbxInvalidKeyException(); } // final hmacTransformer = crypto.Hmac(crypto.sha256, hmacKey.bytes); // final blockreader.readBytes(32); - final bodyStuff = hmacBlockTransformer(reader); - _logger.fine('body decrypt: ${ByteUtils.toHexList(bodyStuff)}'); - final decrypted = decrypt(header, bodyStuff, keys.cipherKey); + final bodyContent = hmacBlockTransformer(reader); + _logger.fine('body decrypt: ${ByteUtils.toHexList(bodyContent)}'); + final decrypted = decrypt(header, bodyContent, keys.cipherKey); _logger.finer('compression: ${header.compression}'); if (header.compression == Compression.gzip) { final content = GZipCodec().decode(decrypted) as Uint8List; @@ -431,7 +432,7 @@ class KdbxFormat { Uint8List hmacBlockTransformer(ReaderHelper reader) { Uint8List blockHash; int blockLength; - List ret = []; + final ret = []; while (true) { blockHash = reader.readBytes(32); blockLength = reader.readUint32(); diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 38c2435..690b427 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:clock/clock.dart'; import 'package:kdbx/kdbx.dart'; +import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/kdbx_consts.dart'; import 'package:meta/meta.dart'; import 'package:xml/xml.dart'; @@ -154,7 +155,23 @@ class DateTimeUtcNode extends KdbxSubTextNode { } @override - DateTime decode(String value) => DateTime.parse(value); + DateTime decode(String value) { + if (value == null) { + return null; + } + if (value.contains(':')) { + return DateTime.parse(value); + } + // kdbx 4.x uses base64 encoded date. + final decoded = base64.decode(value); + const EpochSeconds = 62135596800; + + final secondsFrom00 = ReaderHelper(decoded).readUint64(); + + return DateTime.fromMillisecondsSinceEpoch( + (secondsFrom00 - EpochSeconds) * 1000, + isUtc: true); + } @override String encode(DateTime value) { diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart index 283c179..baf4306 100644 --- a/test/kdbx4_test.dart +++ b/test/kdbx4_test.dart @@ -13,6 +13,8 @@ import 'package:test/test.dart'; final _logger = Logger('kdbx4_test'); +// ignore_for_file: non_constant_identifier_names + //typedef HashStuff = Pointer Function(Pointer str); typedef Argon2HashNative = Pointer Function( Pointer key, @@ -100,7 +102,6 @@ void main() { PrintAppender().attachToLogger(Logger.root); final kdbxFormat = KdbxFormat(Argon2Test()); group('Reading', () { - final argon2 = Argon2Test(); test('bubb', () async { final key = utf8.encode('asdf') as Uint8List; final salt = Uint8List(8); @@ -126,6 +127,7 @@ void main() { final saved = kdbx.save(); final loadedKdbx = kdbxFormat.read(saved, credentials); + _logger.fine('Successfully loaded kdbx $loadedKdbx'); File('test_v4.kdbx').writeAsBytesSync(saved); }); });