Browse Source

first version implementing kdbx4 support.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
8fa7947529
  1. 1
      .idea/dictionaries/herbert.xml
  2. 4
      bin/kdbx.dart
  3. 2
      example/kdbx_example.dart
  4. 113
      lib/src/crypto/key_encrypter_kdf.dart
  5. 37
      lib/src/crypto/protected_salt_generator.dart
  6. 42
      lib/src/internal/byte_utils.dart
  7. 169
      lib/src/kdbx_format.dart
  8. 42
      lib/src/kdbx_header.dart
  9. 127
      lib/src/kdbx_var_dictionary.dart
  10. BIN
      libargon2_ffi.dylib
  11. 4
      pubspec.yaml
  12. 117
      test/kdbx4_test.dart
  13. 20
      test/kdbx_test.dart
  14. BIN
      test/keepassxcpasswords.kdbx

1
.idea/dictionaries/herbert.xml

@ -3,6 +3,7 @@
<words> <words>
<w>consts</w> <w>consts</w>
<w>derivator</w> <w>derivator</w>
<w>encrypter</w>
<w>kdbx</w> <w>kdbx</w>
</words> </words>
</dictionary> </dictionary>

4
bin/kdbx.dart

@ -74,8 +74,8 @@ abstract class KdbxFileCommand extends Command<void> {
final bytes = await File(inputFile).readAsBytes(); final bytes = await File(inputFile).readAsBytes();
final password = prompts.get('Password for $inputFile', final password = prompts.get('Password for $inputFile',
conceal: true, validate: (str) => str.isNotEmpty); conceal: true, validate: (str) => str.isNotEmpty);
final file = KdbxFormat.read( final file = KdbxFormat(null)
bytes, Credentials(ProtectedValue.fromString(password))); .read(bytes, Credentials(ProtectedValue.fromString(password)));
return runWithFile(file); return runWithFile(file);
} }

2
example/kdbx_example.dart

@ -1,5 +1,5 @@
import 'package:kdbx/kdbx.dart'; import 'package:kdbx/kdbx.dart';
void main() { void main() {
KdbxFormat.read(null, null); KdbxFormat().read(null, null);
} }

113
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<T> {
KdfField(this.field, this.type);
final String field;
final ValueType<T> 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 = <String, KdfType>{
'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,
);
}

37
lib/src/crypto/protected_salt_generator.dart

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:cryptography/cryptography.dart' as cryptography;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
class ProtectedSaltGenerator { class ProtectedSaltGenerator {
@ -11,6 +12,9 @@ class ProtectedSaltGenerator {
..init(false, ParametersWithIV(KeyParameter(hash), salsaNonce)); ..init(false, ParametersWithIV(KeyParameter(hash), salsaNonce));
return ProtectedSaltGenerator._(cipher); return ProtectedSaltGenerator._(cipher);
} }
factory ProtectedSaltGenerator.chacha20(Uint8List key) {
return ChachaProtectedSaltGenerator.create(key); // Chacha20();
}
ProtectedSaltGenerator._(this._cipher); ProtectedSaltGenerator._(this._cipher);
@ -30,3 +34,36 @@ class ProtectedSaltGenerator {
return base64.encode(encrypted); 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);
}
}

42
lib/src/internal/byte_utils.dart

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
@ -20,7 +21,7 @@ class ByteUtils {
return true; 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<int> list) => static String toHexList(List<int> list) =>
list?.map((val) => toHex(val))?.join(' ') ?? '(null)'; list?.map((val) => toHex(val))?.join(' ') ?? '(null)';
@ -73,8 +74,13 @@ class ReaderHelper {
int readUint32() => _nextByteBuffer(4).getUint32(0, Endian.little); int readUint32() => _nextByteBuffer(4).getUint32(0, Endian.little);
int readUint64() => _nextByteBuffer(8).getUint64(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); Uint8List readBytes(int size) => _nextBytes(size);
String readString(int size) => const Utf8Decoder().convert(readBytes(size));
Uint8List readBytesUpTo(int maxSize) => Uint8List readBytesUpTo(int maxSize) =>
_nextBytes(min(maxSize, lengthInBytes - pos)); _nextBytes(min(maxSize, lengthInBytes - pos));
@ -84,6 +90,8 @@ class ReaderHelper {
static int singleUint64(Uint8List bytes) => ReaderHelper(bytes).readUint64(); static int singleUint64(Uint8List bytes) => ReaderHelper(bytes).readUint64();
} }
typedef LengthWriter = void Function(int length);
class WriterHelper { class WriterHelper {
WriterHelper([BytesBuilder output]) : output = output ?? BytesBuilder(); WriterHelper([BytesBuilder output]) : output = output ?? BytesBuilder();
@ -91,25 +99,40 @@ class WriterHelper {
void _write(ByteData byteData) => output.add(byteData.buffer.asUint8List()); 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.add(bytes);
// output.asUint8List().addAll(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)); _write(ByteData(4)..setUint32(0, value, Endian.little));
// output.asUint32List().add(value); // 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)); _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)); _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); output.addByte(value);
} }
@ -117,4 +140,11 @@ class WriterHelper {
(WriterHelper()..writeUint32(val)).output.toBytes(); (WriterHelper()..writeUint32(val)).output.toBytes();
static Uint8List singleUint64Bytes(int val) => static Uint8List singleUint64Bytes(int val) =>
(WriterHelper()..writeUint64(val)).output.toBytes(); (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;
}
} }

169
lib/src/kdbx_format.dart

@ -1,14 +1,17 @@
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';
import 'package:convert/convert.dart' as convert; import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/kdbx.dart'; 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_salt_generator.dart';
import 'package:kdbx/src/crypto/protected_value.dart'; import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:kdbx/src/internal/byte_utils.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/internal/crypto_utils.dart';
import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart'; import 'package:kdbx/src/kdbx_header.dart';
@ -278,7 +281,11 @@ class KdbxBody extends KdbxNode {
} }
class KdbxFormat { class KdbxFormat {
static KdbxFile create( KdbxFormat([this.argon2]);
final Argon2 argon2;
KdbxFile create(
Credentials credentials, Credentials credentials,
String name, { String name, {
String generator, String generator,
@ -293,19 +300,22 @@ class KdbxFormat {
return KdbxFile(credentials, header, body); return KdbxFile(credentials, header, body);
} }
static KdbxFile read(Uint8List input, Credentials credentials) { KdbxFile read(Uint8List input, Credentials credentials) {
final reader = ReaderHelper(input); final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader); 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'); _logger.finer('Unsupported version for $header');
throw KdbxUnsupportedException('Unsupported kdbx version ' throw KdbxUnsupportedException('Unsupported kdbx version '
'${header.versionMajor}.${header.versionMinor}.' '${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) { KdbxHeader header, ReaderHelper reader, Credentials credentials) {
// _getMasterKeyV3(header, credentials); // _getMasterKeyV3(header, credentials);
final masterKey = _generateMasterKeyV3(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<int> ret = <int>[];
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; 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( throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption'); '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); final document = xml.parse(xmlString);
@ -350,7 +484,7 @@ class KdbxFormat {
return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup); return KdbxBody.read(keePassFile, KdbxMeta.read(meta), rootGroup);
} }
static Uint8List _decryptContent( Uint8List _decryptContent(
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) { KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine()); final decryptCipher = CBCBlockCipher(AESFastEngine());
@ -383,6 +517,19 @@ class KdbxFormat {
return content; 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( static Uint8List _generateMasterKeyV3(
KdbxHeader header, Credentials credentials) { KdbxHeader header, Credentials credentials) {
final rounds = ReaderHelper.singleUint64( final rounds = ReaderHelper.singleUint64(

42
lib/src/kdbx_header.dart

@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:kdbx/src/internal/consts.dart'; import 'package:kdbx/src/internal/consts.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -23,7 +24,7 @@ enum Compression {
} }
/// how protected values are encrypted in the xml. /// how protected values are encrypted in the xml.
enum ProtectedValueEncryption { plainText, arc4variant, salsa20 } enum ProtectedValueEncryption { plainText, arc4variant, salsa20, chaCha20 }
enum HeaderFields { enum HeaderFields {
EndOfHeader, EndOfHeader,
@ -41,6 +42,13 @@ enum HeaderFields {
PublicCustomData, PublicCustomData,
} }
enum InnerHeaderFields {
EndOfHeader,
InnerRandomStreamID,
InnerRandomStreamKey,
Binary,
}
class HeaderField { class HeaderField {
HeaderField(this.field, this.bytes); HeaderField(this.field, this.bytes);
@ -57,6 +65,7 @@ class KdbxHeader {
@required this.versionMinor, @required this.versionMinor,
@required this.versionMajor, @required this.versionMajor,
@required this.fields, @required this.fields,
@required this.endPos,
}); });
KdbxHeader.create() KdbxHeader.create()
@ -66,6 +75,7 @@ class KdbxHeader {
versionMinor: 1, versionMinor: 1,
versionMajor: 3, versionMajor: 3,
fields: _defaultFieldValues(), fields: _defaultFieldValues(),
endPos: null,
); );
static List<HeaderFields> _requiredFields(int majorVersion) { static List<HeaderFields> _requiredFields(int majorVersion) {
@ -76,7 +86,8 @@ class KdbxHeader {
HeaderFields.CipherID, HeaderFields.CipherID,
HeaderFields.CompressionFlags, HeaderFields.CompressionFlags,
HeaderFields.MasterSeed, HeaderFields.MasterSeed,
HeaderFields.EncryptionIV HeaderFields.EncryptionIV,
HeaderFields.InnerRandomStreamID,
]; ];
if (majorVersion < 4) { if (majorVersion < 4) {
return baseHeaders + return baseHeaders +
@ -85,7 +96,7 @@ class KdbxHeader {
HeaderFields.TransformRounds, HeaderFields.TransformRounds,
HeaderFields.ProtectedStreamKey, HeaderFields.ProtectedStreamKey,
HeaderFields.StreamStartBytes, HeaderFields.StreamStartBytes,
HeaderFields.InnerRandomStreamID // HeaderFields.InnerRandomStreamID
]; ];
} else { } else {
// TODO kdbx 4 support // TODO kdbx 4 support
@ -189,26 +200,37 @@ class KdbxHeader {
_logger.finer('Reading version: $versionMajor.$versionMinor'); _logger.finer('Reading version: $versionMajor.$versionMinor');
final headerFields = Map.fromEntries(readField(reader, versionMajor) final headerFields = Map.fromEntries(readField(reader, versionMajor)
.map((field) => MapEntry(field.field, field))); .map((field) => MapEntry(field.field, field)));
return KdbxHeader( return KdbxHeader(
sig1: sig1, sig1: sig1,
sig2: sig2, sig2: sig2,
versionMinor: versionMinor, versionMinor: versionMinor,
versionMajor: versionMajor, versionMajor: versionMajor,
fields: headerFields, fields: headerFields,
endPos: reader.pos,
); );
} }
static Iterable<HeaderField> readField( static Iterable<HeaderField> readField(ReaderHelper reader, int versionMajor,
ReaderHelper reader, int versionMajor) sync* { [List<dynamic> fields = HeaderFields.values]) sync* {
while (true) { while (true) {
final headerId = reader.readUint8(); final headerId = reader.readUint8();
final int bodySize = final int bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16(); versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null; final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
_logger.finer( _logger.finer(
'Read header ${HeaderFields.values[headerId]}: ${ByteUtils.toHexList(bodyBytes)}'); 'Read header ${fields[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) { 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 { } else {
break; break;
} }
@ -221,6 +243,9 @@ class KdbxHeader {
final int versionMajor; final int versionMajor;
final Map<HeaderFields, HeaderField> fields; final Map<HeaderFields, HeaderField> fields;
/// end position of the header, if we have been reading from a stream.
final int endPos;
Compression get compression { Compression get compression {
switch (ReaderHelper.singleUint32( switch (ReaderHelper.singleUint32(
fields[HeaderFields.CompressionFlags].bytes)) { fields[HeaderFields.CompressionFlags].bytes)) {
@ -237,6 +262,9 @@ class KdbxHeader {
ProtectedValueEncryption.values[ReaderHelper.singleUint32( ProtectedValueEncryption.values[ReaderHelper.singleUint32(
fields[HeaderFields.InnerRandomStreamID].bytes)]; fields[HeaderFields.InnerRandomStreamID].bytes)];
VarDictionary get readKdfParameters => VarDictionary.read(
ReaderHelper(fields[HeaderFields.KdfParameters].bytes));
@override @override
String toString() { String toString() {
return 'KdbxHeader{sig1: $sig1, sig2: $sig2, versionMajor: $versionMajor, versionMinor: $versionMinor}'; return 'KdbxHeader{sig1: $sig1, sig2: $sig2, versionMajor: $versionMajor, versionMinor: $versionMinor}';

127
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> = T Function(ReaderHelper reader, int length);
typedef Encoder<T> = void Function(WriterHelper writer, T value);
extension on WriterHelper {
LengthWriter _lengthWriter() => (int length) => writeInt32(length);
}
@immutable
class ValueType<T> {
const ValueType(this.code, this.decoder, [this.encoder]);
final int code;
final Decoder<T> decoder;
final Encoder<T> 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<T> {
VarDictionaryItem(this._key, this._valueType, this._value);
final String _key;
final ValueType<T> _valueType;
final T _value;
String toDebugString() {
return 'VarDictionaryItem{key=$_key, valueType=$_valueType, value=${_value.runtimeType}}';
}
}
class VarDictionary {
VarDictionary(List<VarDictionaryItem<dynamic>> items)
: assert(items != null),
_items = items,
_dict = Map.fromEntries(items.map((item) => MapEntry(item._key, item)));
factory VarDictionary.read(ReaderHelper reader) {
final items = <VarDictionaryItem>[];
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<VarDictionaryItem<dynamic>> _items;
final Map<String, VarDictionaryItem<dynamic>> _dict;
T get<T>(ValueType<T> type, String key) => _dict[key]?._value as T;
static VarDictionaryItem<dynamic> _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<dynamic>(
key, valueType, valueType.decoder(reader, valueLength));
}
String toDebugString() {
return 'VarDictionary{${_items.map((item) => item.toDebugString())}';
}
}

BIN
libargon2_ffi.dylib

Binary file not shown.

4
pubspec.yaml

@ -13,6 +13,7 @@ dependencies:
logging: '>=0.11.3+2 <1.0.0' logging: '>=0.11.3+2 <1.0.0'
crypto: '>=2.0.0 <3.0.0' crypto: '>=2.0.0 <3.0.0'
pointycastle: '>=1.0.1 <2.0.0' pointycastle: '>=1.0.1 <2.0.0'
cryptography: ^0.1.2
xml: '>=3.7.0 <4.0.0' xml: '>=3.7.0 <4.0.0'
uuid: '>=2.0.0 <3.0.0' uuid: '>=2.0.0 <3.0.0'
meta: '>=1.0.0 <2.0.0' meta: '>=1.0.0 <2.0.0'
@ -25,8 +26,11 @@ dependencies:
args: '>1.5.0 <2.0.0' args: '>1.5.0 <2.0.0'
prompts: '>=1.3.0 <2.0.0' prompts: '>=1.3.0 <2.0.0'
logging_appenders: '>=0.1.0 <1.0.0' logging_appenders: '>=0.1.0 <1.0.0'
ffi_helper: ^1.4.0
dev_dependencies: dev_dependencies:
pedantic: '>=1.7.0 <2.0.0' pedantic: '>=1.7.0 <2.0.0'
test: '>=1.6.0 <2.0.0' test: '>=1.6.0 <2.0.0'
ffi: ^0.1.3

117
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<Utf8> Function(Pointer<Utf8> str);
typedef Argon2HashNative = Pointer<Utf8> Function(
Pointer<Uint8> key,
IntPtr keyLen,
Pointer<Uint8> 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<Utf8> Function(
Pointer<Uint8> key,
int keyLen,
Pointer<Uint8> 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<NativeFunction<Argon2HashNative>>('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<Uint8>(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');
});
});
}

20
test/kdbx_test.dart

@ -1,3 +1,4 @@
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
@ -20,12 +21,13 @@ class FakeProtectedSaltGenerator implements ProtectedSaltGenerator {
void main() { void main() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
PrintAppender().attachToLogger(Logger.root); PrintAppender().attachToLogger(Logger.root);
final kdbxForamt = KdbxFormat();
group('Reading', () { group('Reading', () {
setUp(() {}); setUp(() {});
test('First Test', () async { test('First Test', () async {
final data = await File('test/FooBar.kdbx').readAsBytes(); 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( final cred = Credentials.composite(
ProtectedValue.fromString('asdf'), keyFileBytes); ProtectedValue.fromString('asdf'), keyFileBytes);
final data = await File('test/password-and-keyfile.kdbx').readAsBytes(); 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)); expect(file.body.rootGroup.entries, hasLength(2));
}); });
}); });
group('Creating', () { group('Creating', () {
test('Simple create', () { test('Simple create', () {
final kdbx = KdbxFormat.create( final kdbx = kdbxForamt.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
expect(kdbx, isNotNull); expect(kdbx, isNotNull);
expect(kdbx.body.rootGroup, isNotNull); expect(kdbx.body.rootGroup, isNotNull);
@ -54,7 +56,7 @@ void main() {
.toXmlString(pretty: true)); .toXmlString(pretty: true));
}); });
test('Create Entry', () { test('Create Entry', () {
final kdbx = KdbxFormat.create( final kdbx = kdbxForamt.create(
Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
@ -71,7 +73,7 @@ void main() {
test('Simple save and load', () { test('Simple save and load', () {
final credentials = Credentials(ProtectedValue.fromString('FooBar')); final credentials = Credentials(ProtectedValue.fromString('FooBar'));
final Uint8List saved = (() { final Uint8List saved = (() {
final kdbx = KdbxFormat.create(credentials, 'CreateTest'); final kdbx = kdbxForamt.create(credentials, 'CreateTest');
final rootGroup = kdbx.body.rootGroup; final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup); final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry); rootGroup.addEntry(entry);
@ -82,7 +84,7 @@ void main() {
// print(ByteUtils.toHexList(saved)); // print(ByteUtils.toHexList(saved));
final kdbx = KdbxFormat.read(saved, credentials); final kdbx = kdbxForamt.read(saved, credentials);
expect( expect(
kdbx.body.rootGroup.entries.first kdbx.body.rootGroup.entries.first
.getString(KdbxKey('Password')) .getString(KdbxKey('Password'))
@ -92,12 +94,10 @@ void main() {
}); });
}); });
group('Unsupported version', () { group('kdbx 4.x', () {
test('Fails with exception', () async { test('Fails with exception', () async {
final data = await File('test/keepassxcpasswords.kdbx').readAsBytes(); final data = await File('test/keepassxcpasswords.kdbx').readAsBytes();
expect(() { kdbxForamt.read(data, Credentials(ProtectedValue.fromString('asdf')));
KdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
}, throwsA(const TypeMatcher<KdbxUnsupportedException>()));
}); });
}); });
} }

BIN
test/keepassxcpasswords.kdbx

Binary file not shown.
Loading…
Cancel
Save