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';