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