diff --git a/.idea/dictionaries/herbert.xml b/.idea/dictionaries/herbert.xml
index 93abe3f..7e2bf04 100644
--- a/.idea/dictionaries/herbert.xml
+++ b/.idea/dictionaries/herbert.xml
@@ -3,6 +3,7 @@
consts
derivator
+ encrypter
kdbx
diff --git a/bin/kdbx.dart b/bin/kdbx.dart
index 971ce70..cbab6c7 100644
--- a/bin/kdbx.dart
+++ b/bin/kdbx.dart
@@ -74,8 +74,8 @@ abstract class KdbxFileCommand extends Command {
final bytes = await File(inputFile).readAsBytes();
final password = prompts.get('Password for $inputFile',
conceal: true, validate: (str) => str.isNotEmpty);
- final file = KdbxFormat.read(
- bytes, Credentials(ProtectedValue.fromString(password)));
+ final file = KdbxFormat(null)
+ .read(bytes, Credentials(ProtectedValue.fromString(password)));
return runWithFile(file);
}
diff --git a/example/kdbx_example.dart b/example/kdbx_example.dart
index 0453429..d4902e5 100644
--- a/example/kdbx_example.dart
+++ b/example/kdbx_example.dart
@@ -1,5 +1,5 @@
import 'package:kdbx/kdbx.dart';
void main() {
- KdbxFormat.read(null, null);
+ KdbxFormat().read(null, null);
}
diff --git a/lib/src/crypto/key_encrypter_kdf.dart b/lib/src/crypto/key_encrypter_kdf.dart
new file mode 100644
index 0000000..dc902f0
--- /dev/null
+++ b/lib/src/crypto/key_encrypter_kdf.dart
@@ -0,0 +1,113 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:kdbx/kdbx.dart';
+import 'package:kdbx/src/internal/byte_utils.dart';
+import 'package:kdbx/src/kdbx_var_dictionary.dart';
+import 'package:logging/logging.dart';
+
+final _logger = Logger('key_encrypter_kdf');
+
+enum KdfType {
+ Argon2,
+ Aes,
+}
+
+class KdfField {
+ KdfField(this.field, this.type);
+
+ final String field;
+ final ValueType type;
+
+ static final salt = KdfField('S', ValueType.typeBytes);
+ static final parallelism = KdfField('P', ValueType.typeUInt32);
+ static final memory = KdfField('M', ValueType.typeUInt64);
+ static final iterations = KdfField('I', ValueType.typeUInt64);
+ static final version = KdfField('V', ValueType.typeUInt32);
+ static final secretKey = KdfField('K', ValueType.typeBytes);
+ static final assocData = KdfField('A', ValueType.typeBytes);
+ static final rounds = KdfField('R', ValueType.typeInt64);
+
+ static final fields = [
+ salt,
+ parallelism,
+ memory,
+ iterations,
+ version,
+ secretKey,
+ assocData,
+ rounds
+ ];
+
+ static void debugAll(VarDictionary dict) {
+ _logger
+ .fine('VarDictionary{\n${fields.map((f) => f.debug(dict)).join('\n')}');
+ }
+
+ T read(VarDictionary dict) => dict.get(type, field);
+ String debug(VarDictionary dict) {
+ final value = dict.get(type, field);
+ final strValue = type == ValueType.typeBytes
+ ? ByteUtils.toHexList(value as Uint8List)
+ : value;
+ return '$field=$strValue';
+ }
+}
+
+class KeyEncrypterKdf {
+ KeyEncrypterKdf(this.argon2);
+
+ static const kdfUuids = {
+ '72Nt34wpREuR96mkA+MKDA==': KdfType.Argon2,
+ 'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes,
+ };
+
+ final Argon2 argon2;
+
+ Uint8List encrypt(Uint8List key, VarDictionary kdfParameters) {
+ final uuid = kdfParameters.get(ValueType.typeBytes, '\$UUID');
+ if (uuid == null) {
+ throw KdbxCorruptedFileException('No Kdf UUID');
+ }
+ final kdfUuid = base64.encode(uuid);
+ switch (kdfUuids[kdfUuid]) {
+ case KdfType.Argon2:
+ _logger.fine('Must be using argon2');
+ return encryptArgon2(key, kdfParameters);
+ break;
+ case KdfType.Aes:
+ _logger.fine('Must be using aes');
+ break;
+ }
+ throw UnsupportedError('unsupported encrypt stuff.');
+ }
+
+ Uint8List encryptArgon2(Uint8List key, VarDictionary kdfParameters) {
+ _logger.fine('argon2():');
+ _logger.fine('key: ${ByteUtils.toHexList(key)}');
+ KdfField.debugAll(kdfParameters);
+ return argon2.argon2(
+ key,
+ KdfField.salt.read(kdfParameters),
+ 65536, //KdfField.memory.read(kdfParameters),
+ KdfField.iterations.read(kdfParameters),
+ 32,
+ KdfField.parallelism.read(kdfParameters),
+ 0,
+ KdfField.version.read(kdfParameters),
+ );
+ }
+}
+
+abstract class Argon2 {
+ Uint8List argon2(
+ Uint8List key,
+ Uint8List salt,
+ int memory,
+ int iterations,
+ int length,
+ int parallelism,
+ int type,
+ int version,
+ );
+}
diff --git a/lib/src/crypto/protected_salt_generator.dart b/lib/src/crypto/protected_salt_generator.dart
index 891da36..b63f5bb 100644
--- a/lib/src/crypto/protected_salt_generator.dart
+++ b/lib/src/crypto/protected_salt_generator.dart
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
+import 'package:cryptography/cryptography.dart' as cryptography;
import 'package:pointycastle/export.dart';
class ProtectedSaltGenerator {
@@ -11,6 +12,9 @@ class ProtectedSaltGenerator {
..init(false, ParametersWithIV(KeyParameter(hash), salsaNonce));
return ProtectedSaltGenerator._(cipher);
}
+ factory ProtectedSaltGenerator.chacha20(Uint8List key) {
+ return ChachaProtectedSaltGenerator.create(key); // Chacha20();
+ }
ProtectedSaltGenerator._(this._cipher);
@@ -30,3 +34,36 @@ class ProtectedSaltGenerator {
return base64.encode(encrypted);
}
}
+
+class ChachaProtectedSaltGenerator implements ProtectedSaltGenerator {
+ ChachaProtectedSaltGenerator._(this._secretKey, this._nonce);
+
+ 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));
+ }
+
+ final cryptography.SecretKey _secretKey;
+ final cryptography.SecretKey _nonce;
+
+ @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);
+ }
+
+ @override
+ String encryptToBase64(String plainValue) {
+ final input = utf8.encode(plainValue) as Uint8List;
+ final encrypted =
+ cryptography.chacha20.encrypt(input, _secretKey, nonce: _nonce);
+ return base64.encode(encrypted);
+ }
+}
diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart
index dfa3aff..560cf50 100644
--- a/lib/src/internal/byte_utils.dart
+++ b/lib/src/internal/byte_utils.dart
@@ -1,3 +1,4 @@
+import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
@@ -20,7 +21,7 @@ class ByteUtils {
return true;
}
- static String toHex(int val) => '0x${val.toRadixString(16)}';
+ static String toHex(int val) => '0x${val.toRadixString(16).padLeft(2, '0')}';
static String toHexList(List list) =>
list?.map((val) => toHex(val))?.join(' ') ?? '(null)';
@@ -73,8 +74,13 @@ class ReaderHelper {
int readUint32() => _nextByteBuffer(4).getUint32(0, Endian.little);
int readUint64() => _nextByteBuffer(8).getUint64(0, Endian.little);
+ int readInt32() => _nextByteBuffer(4).getInt32(0, Endian.little);
+ int readInt64() => _nextByteBuffer(8).getInt64(0, Endian.little);
+
Uint8List readBytes(int size) => _nextBytes(size);
+ String readString(int size) => const Utf8Decoder().convert(readBytes(size));
+
Uint8List readBytesUpTo(int maxSize) =>
_nextBytes(min(maxSize, lengthInBytes - pos));
@@ -84,6 +90,8 @@ class ReaderHelper {
static int singleUint64(Uint8List bytes) => ReaderHelper(bytes).readUint64();
}
+typedef LengthWriter = void Function(int length);
+
class WriterHelper {
WriterHelper([BytesBuilder output]) : output = output ?? BytesBuilder();
@@ -91,25 +99,40 @@ class WriterHelper {
void _write(ByteData byteData) => output.add(byteData.buffer.asUint8List());
- void writeBytes(Uint8List bytes) {
+ void writeBytes(Uint8List bytes, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(4);
output.add(bytes);
// output.asUint8List().addAll(bytes);
}
- void writeUint32(int value) {
+ void writeUint32(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(4);
_write(ByteData(4)..setUint32(0, value, Endian.little));
// output.asUint32List().add(value);
}
- void writeUint64(int value) {
+ void writeUint64(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(8);
_write(ByteData(8)..setUint64(0, value, Endian.little));
}
- void writeUint16(int value) {
+ void writeUint16(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(2);
_write(ByteData(2)..setUint16(0, value, Endian.little));
}
- void writeUint8(int value) {
+ void writeInt32(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(4);
+ _write(ByteData(4)..setInt32(0, value, Endian.little));
+ }
+
+ void writeInt64(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(8);
+ _write(ByteData(8)..setInt64(0, value, Endian.little));
+ }
+
+ void writeUint8(int value, [LengthWriter lengthWriter]) {
+ lengthWriter?.call(1);
output.addByte(value);
}
@@ -117,4 +140,11 @@ class WriterHelper {
(WriterHelper()..writeUint32(val)).output.toBytes();
static Uint8List singleUint64Bytes(int val) =>
(WriterHelper()..writeUint64(val)).output.toBytes();
+
+ int writeString(String value, [LengthWriter lengthWriter]) {
+ final bytes = const Utf8Encoder().convert(value);
+ lengthWriter?.call(bytes.length);
+ writeBytes(bytes);
+ return bytes.length;
+ }
}
diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart
index ccc9a20..90f8497 100644
--- a/lib/src/kdbx_format.dart
+++ b/lib/src/kdbx_format.dart
@@ -1,14 +1,17 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/kdbx.dart';
+import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/byte_utils.dart';
+import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/internal/crypto_utils.dart';
import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart';
@@ -278,7 +281,11 @@ class KdbxBody extends KdbxNode {
}
class KdbxFormat {
- static KdbxFile create(
+ KdbxFormat([this.argon2]);
+
+ final Argon2 argon2;
+
+ KdbxFile create(
Credentials credentials,
String name, {
String generator,
@@ -293,19 +300,22 @@ class KdbxFormat {
return KdbxFile(credentials, header, body);
}
- static KdbxFile read(Uint8List input, Credentials credentials) {
+ KdbxFile read(Uint8List input, Credentials credentials) {
final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader);
- if (header.versionMajor != 3) {
+ if (header.versionMajor == 3) {
+ return _loadV3(header, reader, credentials);
+ } else if (header.versionMajor == 4) {
+ return _loadV4(header, reader, credentials);
+ } else {
_logger.finer('Unsupported version for $header');
throw KdbxUnsupportedException('Unsupported kdbx version '
'${header.versionMajor}.${header.versionMinor}.'
- ' Only 3.x is supported.');
+ ' Only 3.x and 4.x is supported.');
}
- return _loadV3(header, reader, credentials);
}
- static KdbxFile _loadV3(
+ KdbxFile _loadV3(
KdbxHeader header, ReaderHelper reader, Credentials credentials) {
// _getMasterKeyV3(header, credentials);
final masterKey = _generateMasterKeyV3(header, credentials);
@@ -324,14 +334,138 @@ class KdbxFormat {
}
}
- static KdbxBody _loadXml(KdbxHeader header, String xmlString) {
+ KdbxFile _loadV4(
+ KdbxHeader header, ReaderHelper reader, Credentials credentials) {
+ final headerBytes = reader.byteData.sublist(0, header.endPos);
+ final hash = crypto.sha256.convert(headerBytes).bytes;
+ final actualHash = reader.readBytes(hash.length);
+ if (!ByteUtils.eq(hash, actualHash)) {
+ _logger.fine(
+ 'Does not match ${ByteUtils.toHexList(hash)} vs ${ByteUtils.toHexList(actualHash)}');
+ throw KdbxCorruptedFileException('Header hash does not match.');
+ }
+ _logger
+ .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}');
+ _logger.finest('Header hash matches.');
+ final key = _computeKeysV4(header, credentials);
+ final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
+ if (masterSeed.length != 32) {
+ throw const FormatException('Master seed must be 32 bytes.');
+ }
+// final keyWithSeed = Uint8List(65);
+// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
+// keyWithSeed.replaceRange(
+// masterSeed.length, masterSeed.length + key.length, key);
+// keyWithSeed[64] = 1;
+ _logger.fine('masterSeed: ${ByteUtils.toHexList(masterSeed)}');
+ final keyWithSeed = masterSeed + key + Uint8List.fromList([1]);
+ assert(keyWithSeed.length == 65);
+ final cipher = crypto.sha256.convert(keyWithSeed.sublist(0, 64));
+ final hmacKey = crypto.sha512.convert(keyWithSeed);
+ _logger.fine('hmacKey: ${ByteUtils.toHexList(hmacKey.bytes)}');
+ final headerHmac =
+ _getHeaderHmac(header, reader, hmacKey.bytes as Uint8List);
+ 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)) {
+ 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, cipher.bytes as Uint8List);
+ _logger.finer('compression: ${header.compression}');
+ if (header.compression == Compression.gzip) {
+ final content = GZipCodec().decode(decrypted) as Uint8List;
+ final contentReader = ReaderHelper(content);
+ final fieldIterable =
+ KdbxHeader.readField(contentReader, 4, InnerHeaderFields.values);
+ final headerFields = Map.fromEntries(
+ fieldIterable.map((field) => MapEntry(field.field, field)));
+ _logger.fine('inner header fields: $headerFields');
+ header.fields.addAll(headerFields);
+ final xml = utf8.decode(contentReader.readRemaining());
+ _logger.fine('content: $xml');
+ return KdbxFile(credentials, header, _loadXml(header, xml));
+ }
+ return null;
+ }
+
+ Uint8List hmacBlockTransformer(ReaderHelper reader) {
+ Uint8List blockHash;
+ int blockLength;
+ List ret = [];
+ while (true) {
+ blockHash = reader.readBytes(32);
+ blockLength = reader.readUint32();
+ if (blockLength < 1) {
+ return Uint8List.fromList(ret);
+ }
+ ret.addAll(reader.readBytes(blockLength));
+ }
+ }
+
+ Uint8List decrypt(
+ KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) {
+ final cipherId = base64.encode(header.fields[HeaderFields.CipherID].bytes);
+ if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.aes].uuid) {
+ _logger.fine('We need AES');
+ final result = _decryptContentV4(header, cipherKey, encrypted);
+ _logger.fine('Result: ${ByteUtils.toHexList(result)}');
+ return result;
+ } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
+ _logger.fine('We need chacha20');
+ } else {
+ throw UnsupportedError('Unsupported cipherId $cipherId');
+ }
+ }
+
+// Uint8List _transformDataV4Aes() {
+// }
+
+ crypto.Digest _getHeaderHmac(
+ KdbxHeader header, ReaderHelper reader, Uint8List key) {
+ final writer = WriterHelper()
+ ..writeUint32(0xffffffff)
+ ..writeUint32(0xffffffff)
+ ..writeBytes(key);
+ final hmacKey = crypto.sha512.convert(writer.output.toBytes()).bytes;
+ final src = reader.byteData.sublist(0, header.endPos);
+ final hmacKeyStuff = crypto.Hmac(crypto.sha256, hmacKey);
+ _logger.fine('keySha: ${ByteUtils.toHexList(hmacKey)}');
+ _logger.fine('src: ${ByteUtils.toHexList(src)}');
+ return hmacKeyStuff.convert(src);
+ }
+
+ Uint8List _computeKeysV4(KdbxHeader header, Credentials credentials) {
+ final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
+ final kdfParameters = header.readKdfParameters;
+ assert(masterSeed.length == 32);
+ final credentialHash = credentials.getHash();
+ _logger.fine('MasterSeed: ${ByteUtils.toHexList(masterSeed)}');
+ _logger.fine('credentialHash: ${ByteUtils.toHexList(credentialHash)}');
+ final ret = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
+ _logger.fine('keyv4: ${ByteUtils.toHexList(ret)}');
+ return ret;
+ }
+
+ ProtectedSaltGenerator _createProtectedSaltGenerator(KdbxHeader header) {
final protectedValueEncryption = header.innerRandomStreamEncryption;
- if (protectedValueEncryption != ProtectedValueEncryption.salsa20) {
+ final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
+ if (protectedValueEncryption == ProtectedValueEncryption.salsa20) {
+ return ProtectedSaltGenerator(streamKey);
+ } else if (protectedValueEncryption == ProtectedValueEncryption.chaCha20) {
+ return ProtectedSaltGenerator.chacha20(streamKey);
+ } else {
throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption');
}
- final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
- final gen = ProtectedSaltGenerator(streamKey);
+ }
+
+ KdbxBody _loadXml(KdbxHeader header, String xmlString) {
+ final gen = _createProtectedSaltGenerator(header);
final document = xml.parse(xmlString);
@@ -350,7 +484,7 @@ class KdbxFormat {
return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup);
}
- static Uint8List _decryptContent(
+ Uint8List _decryptContent(
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
@@ -383,6 +517,19 @@ class KdbxFormat {
return content;
}
+ Uint8List _decryptContentV4(
+ KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
+ final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
+ final decryptCipher = CBCBlockCipher(AESFastEngine());
+ decryptCipher.init(
+ false, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
+ final paddedDecrypted =
+ AesHelper.processBlocks(decryptCipher, encryptedPayload);
+
+ final decrypted = AesHelper.unpad(paddedDecrypted);
+ return decrypted;
+ }
+
static Uint8List _generateMasterKeyV3(
KdbxHeader header, Credentials credentials) {
final rounds = ReaderHelper.singleUint64(
diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart
index e3f68cc..6730eca 100644
--- a/lib/src/kdbx_header.dart
+++ b/lib/src/kdbx_header.dart
@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/internal/consts.dart';
+import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
@@ -23,7 +24,7 @@ enum Compression {
}
/// how protected values are encrypted in the xml.
-enum ProtectedValueEncryption { plainText, arc4variant, salsa20 }
+enum ProtectedValueEncryption { plainText, arc4variant, salsa20, chaCha20 }
enum HeaderFields {
EndOfHeader,
@@ -41,6 +42,13 @@ enum HeaderFields {
PublicCustomData,
}
+enum InnerHeaderFields {
+ EndOfHeader,
+ InnerRandomStreamID,
+ InnerRandomStreamKey,
+ Binary,
+}
+
class HeaderField {
HeaderField(this.field, this.bytes);
@@ -57,6 +65,7 @@ class KdbxHeader {
@required this.versionMinor,
@required this.versionMajor,
@required this.fields,
+ @required this.endPos,
});
KdbxHeader.create()
@@ -66,6 +75,7 @@ class KdbxHeader {
versionMinor: 1,
versionMajor: 3,
fields: _defaultFieldValues(),
+ endPos: null,
);
static List _requiredFields(int majorVersion) {
@@ -76,7 +86,8 @@ class KdbxHeader {
HeaderFields.CipherID,
HeaderFields.CompressionFlags,
HeaderFields.MasterSeed,
- HeaderFields.EncryptionIV
+ HeaderFields.EncryptionIV,
+ HeaderFields.InnerRandomStreamID,
];
if (majorVersion < 4) {
return baseHeaders +
@@ -85,7 +96,7 @@ class KdbxHeader {
HeaderFields.TransformRounds,
HeaderFields.ProtectedStreamKey,
HeaderFields.StreamStartBytes,
- HeaderFields.InnerRandomStreamID
+// HeaderFields.InnerRandomStreamID
];
} else {
// TODO kdbx 4 support
@@ -189,26 +200,37 @@ class KdbxHeader {
_logger.finer('Reading version: $versionMajor.$versionMinor');
final headerFields = Map.fromEntries(readField(reader, versionMajor)
.map((field) => MapEntry(field.field, field)));
+
return KdbxHeader(
sig1: sig1,
sig2: sig2,
versionMinor: versionMinor,
versionMajor: versionMajor,
fields: headerFields,
+ endPos: reader.pos,
);
}
- static Iterable readField(
- ReaderHelper reader, int versionMajor) sync* {
+ static Iterable readField(ReaderHelper reader, int versionMajor,
+ [List fields = HeaderFields.values]) sync* {
while (true) {
final headerId = reader.readUint8();
final int bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
_logger.finer(
- 'Read header ${HeaderFields.values[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
+ 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) {
- yield HeaderField(HeaderFields.values[headerId], bodyBytes);
+ final dynamic field = fields[headerId];
+ if (field is HeaderFields) {
+ yield HeaderField(field, bodyBytes);
+ } else {
+ if (field == InnerHeaderFields.InnerRandomStreamID) {
+ yield HeaderField(HeaderFields.InnerRandomStreamID, bodyBytes);
+ } else if (field == InnerHeaderFields.InnerRandomStreamKey) {
+ yield HeaderField(HeaderFields.ProtectedStreamKey, bodyBytes);
+ }
+ }
} else {
break;
}
@@ -221,6 +243,9 @@ class KdbxHeader {
final int versionMajor;
final Map fields;
+ /// end position of the header, if we have been reading from a stream.
+ final int endPos;
+
Compression get compression {
switch (ReaderHelper.singleUint32(
fields[HeaderFields.CompressionFlags].bytes)) {
@@ -237,6 +262,9 @@ class KdbxHeader {
ProtectedValueEncryption.values[ReaderHelper.singleUint32(
fields[HeaderFields.InnerRandomStreamID].bytes)];
+ VarDictionary get readKdfParameters => VarDictionary.read(
+ ReaderHelper(fields[HeaderFields.KdfParameters].bytes));
+
@override
String toString() {
return 'KdbxHeader{sig1: $sig1, sig2: $sig2, versionMajor: $versionMajor, versionMinor: $versionMinor}';
diff --git a/lib/src/kdbx_var_dictionary.dart b/lib/src/kdbx_var_dictionary.dart
new file mode 100644
index 0000000..b1ff948
--- /dev/null
+++ b/lib/src/kdbx_var_dictionary.dart
@@ -0,0 +1,127 @@
+import 'package:kdbx/src/internal/byte_utils.dart';
+import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
+
+final _logger = Logger('kdbx_var_dictionary');
+
+typedef Decoder = T Function(ReaderHelper reader, int length);
+typedef Encoder = void Function(WriterHelper writer, T value);
+
+extension on WriterHelper {
+ LengthWriter _lengthWriter() => (int length) => writeInt32(length);
+}
+
+@immutable
+class ValueType {
+ const ValueType(this.code, this.decoder, [this.encoder]);
+ final int code;
+ final Decoder decoder;
+ final Encoder encoder;
+
+ static final typeUInt32 = ValueType(
+ 0x04,
+ (reader, _) => reader.readUint32(),
+ (writer, value) => writer.writeUint32(value, writer._lengthWriter()),
+ );
+ static final typeUInt64 = ValueType(
+ 0x05,
+ (reader, _) => reader.readUint64(),
+ (writer, value) => writer.writeUint64(value, writer._lengthWriter()),
+ );
+ static final typeBool = ValueType(
+ 0x08,
+ (reader, _) => reader.readUint8() != 0,
+ (writer, value) => writer.writeUint8(value ? 1 : 0, writer._lengthWriter()),
+ );
+ static final typeInt32 = ValueType(
+ 0x0C,
+ (reader, _) => reader.readInt32(),
+ (writer, value) => writer.writeInt32(value, writer._lengthWriter()),
+ );
+ static final typeInt64 = ValueType(
+ 0x0D,
+ (reader, _) => reader.readInt64(),
+ (writer, value) => writer.writeInt64(value, writer._lengthWriter()),
+ );
+ static final typeString = ValueType(
+ 0x18,
+ (reader, length) => reader.readString(length),
+ (writer, value) => writer.writeString(value, writer._lengthWriter()),
+ );
+ static final typeBytes = ValueType(
+ 0x42,
+ (reader, length) => reader.readBytes(length),
+ (writer, value) => writer.writeBytes(value, writer._lengthWriter()),
+ );
+
+ static ValueType typeByCode(int code) =>
+ values.firstWhere((t) => t.code == code);
+
+ static final values = [
+ typeUInt32,
+ typeUInt64,
+ typeBool,
+ typeInt32,
+ typeInt64,
+ typeString,
+ typeBytes,
+ ];
+}
+
+class VarDictionaryItem {
+ VarDictionaryItem(this._key, this._valueType, this._value);
+
+ final String _key;
+ final ValueType _valueType;
+ final T _value;
+
+ String toDebugString() {
+ return 'VarDictionaryItem{key=$_key, valueType=$_valueType, value=${_value.runtimeType}}';
+ }
+}
+
+class VarDictionary {
+ VarDictionary(List> items)
+ : assert(items != null),
+ _items = items,
+ _dict = Map.fromEntries(items.map((item) => MapEntry(item._key, item)));
+
+ factory VarDictionary.read(ReaderHelper reader) {
+ final items = [];
+ final versionMinor = reader.readUint8();
+ final versionMajor = reader.readUint8();
+ _logger.finest('Reading VarDictionary $versionMajor.$versionMinor');
+ assert(versionMajor == 1);
+
+ while (true) {
+ final item = _readItem(reader);
+ if (item == null) {
+ break;
+ }
+ items.add(item);
+ }
+ return VarDictionary(items);
+ }
+
+ final List> _items;
+ final Map> _dict;
+
+ T get(ValueType type, String key) => _dict[key]?._value as T;
+
+ static VarDictionaryItem _readItem(ReaderHelper reader) {
+ final type = reader.readUint8();
+ if (type == 0) {
+ return null;
+ }
+ final keyLength = reader.readUint32();
+ final key = reader.readString(keyLength);
+ final valueLength = reader.readInt32();
+ final valueType = ValueType.typeByCode(type);
+ return VarDictionaryItem(
+ key, valueType, valueType.decoder(reader, valueLength));
+ }
+
+ String toDebugString() {
+ return 'VarDictionary{${_items.map((item) => item.toDebugString())}';
+ }
+}
diff --git a/libargon2_ffi.dylib b/libargon2_ffi.dylib
new file mode 100755
index 0000000..bf06d71
Binary files /dev/null and b/libargon2_ffi.dylib differ
diff --git a/pubspec.yaml b/pubspec.yaml
index 646d3c0..830b3db 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -13,6 +13,7 @@ dependencies:
logging: '>=0.11.3+2 <1.0.0'
crypto: '>=2.0.0 <3.0.0'
pointycastle: '>=1.0.1 <2.0.0'
+ cryptography: ^0.1.2
xml: '>=3.7.0 <4.0.0'
uuid: '>=2.0.0 <3.0.0'
meta: '>=1.0.0 <2.0.0'
@@ -25,8 +26,11 @@ dependencies:
args: '>1.5.0 <2.0.0'
prompts: '>=1.3.0 <2.0.0'
logging_appenders: '>=0.1.0 <1.0.0'
+ ffi_helper: ^1.4.0
dev_dependencies:
pedantic: '>=1.7.0 <2.0.0'
test: '>=1.6.0 <2.0.0'
+ ffi: ^0.1.3
+
diff --git a/test/kdbx4_test.dart b/test/kdbx4_test.dart
new file mode 100644
index 0000000..d7c1af8
--- /dev/null
+++ b/test/kdbx4_test.dart
@@ -0,0 +1,117 @@
+import 'dart:convert';
+import 'dart:ffi';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:ffi/ffi.dart';
+import 'package:ffi_helper/ffi_helper.dart';
+import 'package:kdbx/kdbx.dart';
+import 'package:kdbx/src/crypto/key_encrypter_kdf.dart';
+import 'package:logging/logging.dart';
+import 'package:logging_appenders/logging_appenders.dart';
+import 'package:test/test.dart';
+
+final _logger = Logger('kdbx4_test');
+
+//typedef HashStuff = Pointer Function(Pointer str);
+typedef Argon2HashNative = Pointer Function(
+ Pointer key,
+ IntPtr keyLen,
+ Pointer salt,
+ Uint64 saltlen,
+ Uint32 m_cost, // memory cost
+ Uint32 t_cost, // time cost (number iterations)
+ Uint32 parallelism,
+ IntPtr hashlen,
+ Uint8 type,
+ Uint32 version,
+);
+typedef Argon2Hash = Pointer Function(
+ Pointer key,
+ int keyLen,
+ Pointer salt,
+ int saltlen,
+ int m_cost, // memory cost
+ int t_cost, // time cost (number iterations)
+ int parallelism,
+ int hashlen,
+ int type,
+ int version,
+);
+
+class Argon2Test implements Argon2 {
+ Argon2Test() {
+// final argon2lib = DynamicLibrary.open('libargon2.1.dylib');
+ final argon2lib = DynamicLibrary.open('libargon2_ffi.dylib');
+ _argon2hash = argon2lib
+ .lookup>('hp_argon2_hash')
+ .asFunction();
+ }
+ Argon2Hash _argon2hash;
+
+ @override
+ Uint8List argon2(
+ Uint8List key,
+ Uint8List salt,
+ int memory,
+ int iterations,
+ int length,
+ int parallelism,
+ int type,
+ int version,
+ ) {
+// print('hash: ${hashStuff('abc')}');
+ final keyArray = Uint8Array.fromTypedList(key);
+// final saltArray = Uint8Array.fromTypedList(salt);
+ final saltArray = allocate(count: salt.length);
+ final saltList = saltArray.asTypedList(length);
+ saltList.setAll(0, salt);
+ const int memoryCost = 1 << 16;
+
+// _logger.fine('saltArray: ${ByteUtils.toHexList(saltArray.view)}');
+
+ final result = _argon2hash(
+ keyArray.rawPtr,
+ keyArray.length,
+ saltArray,
+ salt.length,
+ memoryCost,
+ iterations,
+ parallelism,
+ length,
+ type,
+ version,
+ );
+
+ keyArray.free();
+// saltArray.free();
+ free(saltArray);
+ final resultString = Utf8.fromUtf8(result);
+ return base64.decode(resultString);
+ }
+
+// String hashStuff(String password) =>
+// Utf8.fromUtf8(_hashStuff(Utf8.toUtf8(password)));
+}
+
+void main() {
+ Logger.root.level = Level.ALL;
+ 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);
+// final result = Argon2Test().argon2(key, salt, 1 << 16, 5, 16, 1, 0x13, 1);
+// _logger.fine('hashing: $result');
+ final data = await File('test/keepassxcpasswords.kdbx').readAsBytes();
+ final file =
+ kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
+ final firstEntry = file.body.rootGroup.entries.first;
+ final pwd = firstEntry.getString(KdbxKey('Password')).getText();
+ _logger.info('password: $pwd');
+ expect(pwd, 'MyPassword');
+ });
+ });
+}
diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart
index d34a145..ce08173 100644
--- a/test/kdbx_test.dart
+++ b/test/kdbx_test.dart
@@ -1,3 +1,4 @@
+import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
@@ -20,12 +21,13 @@ class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
void main() {
Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root);
+ final kdbxForamt = KdbxFormat();
group('Reading', () {
setUp(() {});
test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes();
- KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar')));
+ kdbxForamt.read(data, Credentials(ProtectedValue.fromString('FooBar')));
});
});
@@ -36,14 +38,14 @@ void main() {
final cred = Credentials.composite(
ProtectedValue.fromString('asdf'), keyFileBytes);
final data = await File('test/password-and-keyfile.kdbx').readAsBytes();
- final file = KdbxFormat.read(data, cred);
+ final file = kdbxForamt.read(data, cred);
expect(file.body.rootGroup.entries, hasLength(2));
});
});
group('Creating', () {
test('Simple create', () {
- final kdbx = KdbxFormat.create(
+ final kdbx = kdbxForamt.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
expect(kdbx, isNotNull);
expect(kdbx.body.rootGroup, isNotNull);
@@ -54,7 +56,7 @@ void main() {
.toXmlString(pretty: true));
});
test('Create Entry', () {
- final kdbx = KdbxFormat.create(
+ final kdbx = kdbxForamt.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup);
@@ -71,7 +73,7 @@ void main() {
test('Simple save and load', () {
final credentials = Credentials(ProtectedValue.fromString('FooBar'));
final Uint8List saved = (() {
- final kdbx = KdbxFormat.create(credentials, 'CreateTest');
+ final kdbx = kdbxForamt.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry);
@@ -82,7 +84,7 @@ void main() {
// print(ByteUtils.toHexList(saved));
- final kdbx = KdbxFormat.read(saved, credentials);
+ final kdbx = kdbxForamt.read(saved, credentials);
expect(
kdbx.body.rootGroup.entries.first
.getString(KdbxKey('Password'))
@@ -92,12 +94,10 @@ void main() {
});
});
- group('Unsupported version', () {
+ group('kdbx 4.x', () {
test('Fails with exception', () async {
final data = await File('test/keepassxcpasswords.kdbx').readAsBytes();
- expect(() {
- KdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
- }, throwsA(const TypeMatcher()));
+ kdbxForamt.read(data, Credentials(ProtectedValue.fromString('asdf')));
});
});
}
diff --git a/test/keepassxcpasswords.kdbx b/test/keepassxcpasswords.kdbx
index 69ad677..2a50297 100644
Binary files a/test/keepassxcpasswords.kdbx and b/test/keepassxcpasswords.kdbx differ