Browse Source

fixed kdbx 4 support, protected fields, argon2 arguments, etc.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
2659585da9
  1. 4
      CHANGELOG.md
  2. 2
      lib/kdbx.dart
  3. 5
      lib/src/crypto/key_encrypter_kdf.dart
  4. 41
      lib/src/crypto/protected_salt_generator.dart
  5. 17
      lib/src/kdbx_format.dart
  6. 19
      lib/src/kdbx_xml.dart
  7. 4
      test/kdbx4_test.dart

4
CHANGELOG.md

@ -1,3 +1,7 @@
## 0.3.0+1
- Minor fixes for kdbx 4.x
## 0.3.0 ## 0.3.0
- Initial support for kdbx 4.x - Initial support for kdbx 4.x

2
lib/kdbx.dart

@ -1,6 +1,7 @@
/// dart library for reading keepass file format (kdbx). /// dart library for reading keepass file format (kdbx).
library kdbx; library kdbx;
export 'src/crypto/key_encrypter_kdf.dart' show Argon2;
export 'src/crypto/protected_value.dart' export 'src/crypto/protected_value.dart'
show ProtectedValue, StringValue, PlainValue; show ProtectedValue, StringValue, PlainValue;
export 'src/kdbx_consts.dart'; export 'src/kdbx_consts.dart';
@ -9,7 +10,6 @@ export 'src/kdbx_dao.dart' show KdbxDao;
export 'src/kdbx_entry.dart'; export 'src/kdbx_entry.dart';
export 'src/kdbx_format.dart'; export 'src/kdbx_format.dart';
export 'src/kdbx_group.dart'; export 'src/kdbx_group.dart';
export 'src/crypto/key_encrypter_kdf.dart' show Argon2;
export 'src/kdbx_header.dart' export 'src/kdbx_header.dart'
show show
KdbxException, KdbxException,

5
lib/src/crypto/key_encrypter_kdf.dart

@ -67,7 +67,7 @@ class KeyEncrypterKdf {
'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes, 'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes,
}; };
static KdbxUuid kdfUuidForType(KdfType type) { static KdbxUuid kdfUuidForType(KdfType type) {
String uuid = final uuid =
kdfUuids.entries.firstWhere((element) => element.value == type).key; kdfUuids.entries.firstWhere((element) => element.value == type).key;
return KdbxUuid(uuid); return KdbxUuid(uuid);
} }
@ -99,7 +99,8 @@ class KeyEncrypterKdf {
return argon2.argon2( return argon2.argon2(
key, key,
KdfField.salt.read(kdfParameters), KdfField.salt.read(kdfParameters),
65536, //KdfField.memory.read(kdfParameters), // 65536, //KdfField.memory.read(kdfParameters),
KdfField.memory.read(kdfParameters) ~/ 1024,
KdfField.iterations.read(kdfParameters), KdfField.iterations.read(kdfParameters),
32, 32,
KdfField.parallelism.read(kdfParameters), KdfField.parallelism.read(kdfParameters),

41
lib/src/crypto/protected_salt_generator.dart

@ -1,10 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:logging/logging.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cryptography/cryptography.dart' as cryptography; import 'package:cryptography/cryptography.dart' as cryptography;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
final _logger = Logger('protected_salt_generator');
class ProtectedSaltGenerator { class ProtectedSaltGenerator {
factory ProtectedSaltGenerator(Uint8List key) { factory ProtectedSaltGenerator(Uint8List key) {
final hash = sha256.convert(key).bytes as Uint8List; final hash = sha256.convert(key).bytes as Uint8List;
@ -24,6 +27,10 @@ class ProtectedSaltGenerator {
String decryptBase64(String protectedValue) { String decryptBase64(String protectedValue) {
final bytes = base64.decode(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 result = _cipher.process(bytes);
final decrypted = utf8.decode(result); final decrypted = utf8.decode(result);
return decrypted; return decrypted;
@ -36,34 +43,54 @@ class ProtectedSaltGenerator {
} }
class ChachaProtectedSaltGenerator implements ProtectedSaltGenerator { class ChachaProtectedSaltGenerator implements ProtectedSaltGenerator {
ChachaProtectedSaltGenerator._(this._secretKey, this._nonce); ChachaProtectedSaltGenerator._(this._secretKey, this._nonce, this._state);
factory ChachaProtectedSaltGenerator.create(Uint8List key) { factory ChachaProtectedSaltGenerator.create(Uint8List key) {
final hash = sha512.convert(key); final hash = sha512.convert(key);
final secretKey = hash.bytes.sublist(0, 32); final secretKey = hash.bytes.sublist(0, 32);
final nonce = hash.bytes.sublist(32, 32 + 12); final nonce = hash.bytes.sublist(32, 32 + 12);
return ChachaProtectedSaltGenerator._( 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 _secretKey;
final cryptography.SecretKey _nonce; final cryptography.SecretKey _nonce;
final cryptography.KeyStreamCipherState _state;
@override @override
StreamCipher get _cipher => throw UnimplementedError(); StreamCipher get _cipher => throw UnimplementedError();
@override @override
String decryptBase64(String protectedValue) { String decryptBase64(String protectedValue) {
final result = cryptography.chacha20 final bytes = base64.decode(protectedValue);
.decrypt(base64.decode(protectedValue), _secretKey, nonce: _nonce); if (bytes.isEmpty) {
return utf8.decode(result); _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 @override
String encryptToBase64(String plainValue) { String encryptToBase64(String plainValue) {
final input = utf8.encode(plainValue) as Uint8List; final input = utf8.encode(plainValue) as Uint8List;
final encrypted = final encrypted = _state.convert(input);
cryptography.chacha20.encrypt(input, _secretKey, nonce: _nonce);
return base64.encode(encrypted); return base64.encode(encrypted);
} }
} }

17
lib/src/kdbx_format.dart

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@ -282,6 +281,8 @@ class KdbxBody extends KdbxNode {
return result; return result;
} else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) { } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
_logger.fine('We need chacha20'); _logger.fine('We need chacha20');
// TODO can we combine this with _encryptV3?
throw UnsupportedError('Unsupported cipher chacha20 for kdbx 4.x');
} else { } else {
throw UnsupportedError('Unsupported cipherId $cipherId'); throw UnsupportedError('Unsupported cipherId $cipherId');
} }
@ -405,16 +406,16 @@ class KdbxFormat {
final headerHmac = final headerHmac =
_getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey); _getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey);
final expectedHmac = reader.readBytes(headerHmac.bytes.length); final expectedHmac = reader.readBytes(headerHmac.bytes.length);
// _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}'); _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
// _logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}'); _logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}');
if (!ByteUtils.eq(hash, actualHash)) { if (!ByteUtils.eq(headerHmac.bytes, expectedHmac)) {
throw KdbxInvalidKeyException(); throw KdbxInvalidKeyException();
} }
// final hmacTransformer = crypto.Hmac(crypto.sha256, hmacKey.bytes); // final hmacTransformer = crypto.Hmac(crypto.sha256, hmacKey.bytes);
// final blockreader.readBytes(32); // final blockreader.readBytes(32);
final bodyStuff = hmacBlockTransformer(reader); final bodyContent = hmacBlockTransformer(reader);
_logger.fine('body decrypt: ${ByteUtils.toHexList(bodyStuff)}'); _logger.fine('body decrypt: ${ByteUtils.toHexList(bodyContent)}');
final decrypted = decrypt(header, bodyStuff, keys.cipherKey); final decrypted = decrypt(header, bodyContent, keys.cipherKey);
_logger.finer('compression: ${header.compression}'); _logger.finer('compression: ${header.compression}');
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final content = GZipCodec().decode(decrypted) as Uint8List; final content = GZipCodec().decode(decrypted) as Uint8List;
@ -431,7 +432,7 @@ class KdbxFormat {
Uint8List hmacBlockTransformer(ReaderHelper reader) { Uint8List hmacBlockTransformer(ReaderHelper reader) {
Uint8List blockHash; Uint8List blockHash;
int blockLength; int blockLength;
List<int> ret = <int>[]; final ret = <int>[];
while (true) { while (true) {
blockHash = reader.readBytes(32); blockHash = reader.readBytes(32);
blockLength = reader.readUint32(); blockLength = reader.readUint32();

19
lib/src/kdbx_xml.dart

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/kdbx_consts.dart'; import 'package:kdbx/src/kdbx_consts.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
@ -154,7 +155,23 @@ class DateTimeUtcNode extends KdbxSubTextNode<DateTime> {
} }
@override @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 @override
String encode(DateTime value) { String encode(DateTime value) {

4
test/kdbx4_test.dart

@ -13,6 +13,8 @@ import 'package:test/test.dart';
final _logger = Logger('kdbx4_test'); final _logger = Logger('kdbx4_test');
// ignore_for_file: non_constant_identifier_names
//typedef HashStuff = Pointer<Utf8> Function(Pointer<Utf8> str); //typedef HashStuff = Pointer<Utf8> Function(Pointer<Utf8> str);
typedef Argon2HashNative = Pointer<Utf8> Function( typedef Argon2HashNative = Pointer<Utf8> Function(
Pointer<Uint8> key, Pointer<Uint8> key,
@ -100,7 +102,6 @@ void main() {
PrintAppender().attachToLogger(Logger.root); PrintAppender().attachToLogger(Logger.root);
final kdbxFormat = KdbxFormat(Argon2Test()); final kdbxFormat = KdbxFormat(Argon2Test());
group('Reading', () { group('Reading', () {
final argon2 = Argon2Test();
test('bubb', () async { test('bubb', () async {
final key = utf8.encode('asdf') as Uint8List; final key = utf8.encode('asdf') as Uint8List;
final salt = Uint8List(8); final salt = Uint8List(8);
@ -126,6 +127,7 @@ void main() {
final saved = kdbx.save(); final saved = kdbx.save();
final loadedKdbx = kdbxFormat.read(saved, credentials); final loadedKdbx = kdbxFormat.read(saved, credentials);
_logger.fine('Successfully loaded kdbx $loadedKdbx');
File('test_v4.kdbx').writeAsBytesSync(saved); File('test_v4.kdbx').writeAsBytesSync(saved);
}); });
}); });

Loading…
Cancel
Save