Browse Source

kdbx 4.x write support

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
50aaab71bd
  1. 1
      .idea/dictionaries/herbert.xml
  2. 10
      lib/src/crypto/key_encrypter_kdf.dart
  3. 159
      lib/src/kdbx_format.dart
  4. 109
      lib/src/kdbx_header.dart
  5. 15
      lib/src/kdbx_var_dictionary.dart
  6. 17
      lib/src/utils/scope_functions.dart
  7. BIN
      libargon2_ffi.dylib
  8. 16
      test/kdbx4_test.dart

1
.idea/dictionaries/herbert.xml

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

10
lib/src/crypto/key_encrypter_kdf.dart

@ -19,6 +19,7 @@ class KdfField<T> {
final String field; final String field;
final ValueType<T> type; final ValueType<T> type;
static final uuid = KdfField('\$UUID', ValueType.typeBytes);
static final salt = KdfField('S', ValueType.typeBytes); static final salt = KdfField('S', ValueType.typeBytes);
static final parallelism = KdfField('P', ValueType.typeUInt32); static final parallelism = KdfField('P', ValueType.typeUInt32);
static final memory = KdfField('M', ValueType.typeUInt64); static final memory = KdfField('M', ValueType.typeUInt64);
@ -45,6 +46,10 @@ class KdfField<T> {
} }
T read(VarDictionary dict) => dict.get(type, field); T read(VarDictionary dict) => dict.get(type, field);
void write(VarDictionary dict, T value) => dict.set(type, field, value);
VarDictionaryItem<T> item(T value) =>
VarDictionaryItem<T>(field, type, value);
String debug(VarDictionary dict) { String debug(VarDictionary dict) {
final value = dict.get(type, field); final value = dict.get(type, field);
final strValue = type == ValueType.typeBytes final strValue = type == ValueType.typeBytes
@ -61,6 +66,11 @@ class KeyEncrypterKdf {
'72Nt34wpREuR96mkA+MKDA==': KdfType.Argon2, '72Nt34wpREuR96mkA+MKDA==': KdfType.Argon2,
'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes, 'ydnzmmKKRGC/dA0IwYpP6g==': KdfType.Aes,
}; };
static KdbxUuid kdfUuidForType(KdfType type) {
String uuid =
kdfUuids.entries.firstWhere((element) => element.value == type).key;
return KdbxUuid(uuid);
}
final Argon2 argon2; final Argon2 argon2;

159
lib/src/kdbx_format.dart

@ -119,7 +119,7 @@ class HashCredentials implements Credentials {
} }
class KdbxFile { class KdbxFile {
KdbxFile(this.credentials, this.header, this.body) { KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) {
for (final obj in _allObjects) { for (final obj in _allObjects) {
obj.file = this; obj.file = this;
} }
@ -136,6 +136,7 @@ class KdbxFile {
protectedValues[node] = value; protectedValues[node] = value;
} }
final KdbxFormat kdbxFormat;
final Credentials credentials; final Credentials credentials;
final KdbxHeader header; final KdbxHeader header;
final KdbxBody body; final KdbxBody body;
@ -147,19 +148,30 @@ class KdbxFile {
_dirtyObjectsChanged.stream; _dirtyObjectsChanged.stream;
Uint8List save() { Uint8List save() {
assert(header.versionMajor == 3);
final output = BytesBuilder(); final output = BytesBuilder();
final writer = WriterHelper(output); final writer = WriterHelper(output);
header.generateSalts(); header.generateSalts();
header.write(writer); header.write(writer);
final headerHash =
final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes; (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List);
final gen = ProtectedSaltGenerator(streamKey);
if (header.versionMajor <= 3) {
body.meta.headerHash.set( final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) final gen = ProtectedSaltGenerator(streamKey);
.buffer);
body.writeV3(writer, this, gen); body.meta.headerHash.set(headerHash.buffer);
body.writeV3(writer, this, gen);
} else if (header.versionMajor <= 4) {
final headerBytes = writer.output.toBytes();
writer.writeBytes(headerHash);
final gen = kdbxFormat._createProtectedSaltGenerator(header);
final keys = kdbxFormat._computeKeysV4(header, credentials);
final headerHmac = kdbxFormat._getHeaderHmac(headerBytes, keys.hmacKey);
writer.writeBytes(headerHmac.bytes as Uint8List);
body.writeV4(writer, this, gen, keys);
} else {
throw UnsupportedError('Unsupported version ${header.versionMajor}');
}
dirtyObjects.clear(); dirtyObjects.clear();
_dirtyObjectsChanged.add(dirtyObjects); _dirtyObjectsChanged.add(dirtyObjects);
return output.toBytes(); return output.toBytes();
@ -226,6 +238,24 @@ class KdbxBody extends KdbxNode {
writer.writeBytes(encrypted); writer.writeBytes(encrypted);
} }
void writeV4(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator, _KeysV4 keys) {
final bodyWriter = WriterHelper();
final xml = generateXml(saltGenerator);
kdbxFile.header.writeInnerHeader(bodyWriter);
bodyWriter.writeBytes(utf8.encode(xml.toXmlString()) as Uint8List);
final Uint8List compressedBytes =
(kdbxFile.header.compression == Compression.gzip
? GZipCodec().encode(bodyWriter.output.toBytes())
: bodyWriter.output.toBytes()) as Uint8List;
final encrypted = _encryptV4(
kdbxFile,
compressedBytes,
keys.cipherKey,
);
writer.writeBytes(encrypted);
}
Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) { Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) {
final byteWriter = WriterHelper(); final byteWriter = WriterHelper();
byteWriter.writeBytes( byteWriter.writeBytes(
@ -240,6 +270,23 @@ class KdbxBody extends KdbxNode {
return encrypted; return encrypted;
} }
Uint8List _encryptV4(
KdbxFile kdbxFile, Uint8List compressedBytes, Uint8List cipherKey) {
final header = kdbxFile.header;
final cipherId = base64.encode(header.fields[HeaderFields.CipherID].bytes);
if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.aes].uuid) {
_logger.fine('We need AES');
final result = kdbxFile.kdbxFormat
._encryptContentV4Aes(header, cipherKey, compressedBytes);
_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');
}
}
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
final rootGroupNode = rootGroup.toXml(); final rootGroupNode = rootGroup.toXml();
// update protected values... // update protected values...
@ -280,6 +327,13 @@ class KdbxBody extends KdbxNode {
} }
} }
class _KeysV4 {
_KeysV4(this.hmacKey, this.cipherKey);
final Uint8List hmacKey;
final Uint8List cipherKey;
}
class KdbxFormat { class KdbxFormat {
KdbxFormat([this.argon2]); KdbxFormat([this.argon2]);
@ -297,7 +351,7 @@ class KdbxFormat {
); );
final rootGroup = KdbxGroup.create(parent: null, name: name); final rootGroup = KdbxGroup.create(parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup); final body = KdbxBody.create(meta, rootGroup);
return KdbxFile(credentials, header, body); return KdbxFile(this, credentials, header, body);
} }
KdbxFile read(Uint8List input, Credentials credentials) { KdbxFile read(Uint8List input, Credentials credentials) {
@ -327,10 +381,10 @@ class KdbxFormat {
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks); final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml); final string = utf8.decode(xml);
return KdbxFile(credentials, header, _loadXml(header, string)); return KdbxFile(this, credentials, header, _loadXml(header, string));
} else { } else {
return KdbxFile( return KdbxFile(
credentials, header, _loadXml(header, utf8.decode(blocks))); this, credentials, header, _loadXml(header, utf8.decode(blocks)));
} }
} }
@ -347,27 +401,12 @@ class KdbxFormat {
_logger _logger
.finest('KdfParameters: ${header.readKdfParameters.toDebugString()}'); .finest('KdfParameters: ${header.readKdfParameters.toDebugString()}');
_logger.finest('Header hash matches.'); _logger.finest('Header hash matches.');
final key = _computeKeysV4(header, credentials); final keys = _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 = final headerHmac =
_getHeaderHmac(header, reader, hmacKey.bytes as Uint8List); _getHeaderHmac(reader.byteData.sublist(0, header.endPos), keys.hmacKey);
final expectedHmac = reader.readBytes(headerHmac.bytes.length); final expectedHmac = reader.readBytes(headerHmac.bytes.length);
_logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}'); // _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
_logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}'); // _logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}');
if (!ByteUtils.eq(hash, actualHash)) { if (!ByteUtils.eq(hash, actualHash)) {
throw KdbxInvalidKeyException(); throw KdbxInvalidKeyException();
} }
@ -375,17 +414,16 @@ class KdbxFormat {
// final blockreader.readBytes(32); // final blockreader.readBytes(32);
final bodyStuff = hmacBlockTransformer(reader); final bodyStuff = hmacBlockTransformer(reader);
_logger.fine('body decrypt: ${ByteUtils.toHexList(bodyStuff)}'); _logger.fine('body decrypt: ${ByteUtils.toHexList(bodyStuff)}');
final decrypted = decrypt(header, bodyStuff, cipher.bytes as Uint8List); final decrypted = decrypt(header, bodyStuff, keys.cipherKey);
_logger.finer('compression: ${header.compression}'); _logger.finer('compression: ${header.compression}');
if (header.compression == Compression.gzip) { if (header.compression == Compression.gzip) {
final content = GZipCodec().decode(decrypted) as Uint8List; final content = GZipCodec().decode(decrypted) as Uint8List;
final contentReader = ReaderHelper(content); final contentReader = ReaderHelper(content);
final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4); final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4);
_logger.fine('inner header fields: $headerFields'); // _logger.fine('inner header fields: $headerFields');
header.innerFields.addAll(headerFields); header.innerFields.addAll(headerFields);
final xml = utf8.decode(contentReader.readRemaining()); final xml = utf8.decode(contentReader.readRemaining());
_logger.fine('content: $xml'); return KdbxFile(this, credentials, header, _loadXml(header, xml));
return KdbxFile(credentials, header, _loadXml(header, xml));
} }
return null; return null;
} }
@ -414,6 +452,7 @@ class KdbxFormat {
return result; return result;
} else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) { } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
_logger.fine('We need chacha20'); _logger.fine('We need chacha20');
throw UnsupportedError('chacha20 not yet supported $cipherId');
} else { } else {
throw UnsupportedError('Unsupported cipherId $cipherId'); throw UnsupportedError('Unsupported cipherId $cipherId');
} }
@ -422,30 +461,45 @@ class KdbxFormat {
// Uint8List _transformDataV4Aes() { // Uint8List _transformDataV4Aes() {
// } // }
crypto.Digest _getHeaderHmac( crypto.Digest _getHeaderHmac(Uint8List headerBytes, Uint8List key) {
KdbxHeader header, ReaderHelper reader, Uint8List key) {
final writer = WriterHelper() final writer = WriterHelper()
..writeUint32(0xffffffff) ..writeUint32(0xffffffff)
..writeUint32(0xffffffff) ..writeUint32(0xffffffff)
..writeBytes(key); ..writeBytes(key);
final hmacKey = crypto.sha512.convert(writer.output.toBytes()).bytes; final hmacKey = crypto.sha512.convert(writer.output.toBytes()).bytes;
final src = reader.byteData.sublist(0, header.endPos); final src = headerBytes;
final hmacKeyStuff = crypto.Hmac(crypto.sha256, hmacKey); final hmacKeyStuff = crypto.Hmac(crypto.sha256, hmacKey);
_logger.fine('keySha: ${ByteUtils.toHexList(hmacKey)}'); _logger.fine('keySha: ${ByteUtils.toHexList(hmacKey)}');
_logger.fine('src: ${ByteUtils.toHexList(src)}'); _logger.fine('src: ${ByteUtils.toHexList(src)}');
return hmacKeyStuff.convert(src); return hmacKeyStuff.convert(src);
} }
Uint8List _computeKeysV4(KdbxHeader header, Credentials credentials) { _KeysV4 _computeKeysV4(KdbxHeader header, Credentials credentials) {
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
final kdfParameters = header.readKdfParameters; final kdfParameters = header.readKdfParameters;
assert(masterSeed.length == 32); if (masterSeed.length != 32) {
throw const FormatException('Master seed must be 32 bytes.');
}
final credentialHash = credentials.getHash(); final credentialHash = credentials.getHash();
_logger.fine('MasterSeed: ${ByteUtils.toHexList(masterSeed)}'); _logger.fine('MasterSeed: ${ByteUtils.toHexList(masterSeed)}');
_logger.fine('credentialHash: ${ByteUtils.toHexList(credentialHash)}'); _logger.fine('credentialHash: ${ByteUtils.toHexList(credentialHash)}');
final ret = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
_logger.fine('keyv4: ${ByteUtils.toHexList(ret)}'); _logger.fine('keyv4: ${ByteUtils.toHexList(key)}');
return ret;
// 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)}');
return _KeysV4(hmacKey.bytes as Uint8List, cipher.bytes as Uint8List);
} }
ProtectedSaltGenerator _createProtectedSaltGenerator(KdbxHeader header) { ProtectedSaltGenerator _createProtectedSaltGenerator(KdbxHeader header) {
@ -515,11 +569,11 @@ class KdbxFormat {
} }
Uint8List _decryptContentV4( Uint8List _decryptContentV4(
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) { KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine()); final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init( decryptCipher.init(
false, ParametersWithIV(KeyParameter(masterKey), encryptionIv)); false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted = final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload); AesHelper.processBlocks(decryptCipher, encryptedPayload);
@ -527,6 +581,19 @@ class KdbxFormat {
return decrypted; return decrypted;
} }
/// TODO combine this with [_decryptContentV4]
Uint8List _encryptContentV4Aes(
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted = AesHelper.processBlocks(decryptCipher, bytes);
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(

109
lib/src/kdbx_header.dart

@ -1,11 +1,14 @@
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/crypto/key_encrypter_kdf.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/consts.dart';
import 'package:kdbx/src/kdbx_var_dictionary.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';
import 'package:kdbx/src/utils/scope_functions.dart';
final _logger = Logger('kdbx.header'); final _logger = Logger('kdbx.header');
@ -13,6 +16,11 @@ class Consts {
static const FileMagic = 0x9AA2D903; static const FileMagic = 0x9AA2D903;
static const Sig2Kdbx = 0xB54BFB67; static const Sig2Kdbx = 0xB54BFB67;
static const DefaultKdfSaltLength = 32;
static const DefaultKdfParallelism = 1;
static const DefaultKdfIterations = 2;
static const DefaultKdfMemory = 1024 * 1024;
static const DefaultKdfVersion = 0x13;
} }
enum Compression { enum Compression {
@ -81,7 +89,8 @@ class KdbxHeader {
@required this.versionMajor, @required this.versionMajor,
@required this.fields, @required this.fields,
@required this.endPos, @required this.endPos,
}); Map<InnerHeaderFields, InnerHeaderField> innerFields,
}) : innerFields = innerFields ?? {};
KdbxHeader.create() KdbxHeader.create()
: this( : this(
@ -93,6 +102,17 @@ class KdbxHeader {
endPos: null, endPos: null,
); );
KdbxHeader.createV4()
: this(
sig1: Consts.FileMagic,
sig2: Consts.Sig2Kdbx,
versionMinor: 1,
versionMajor: 4,
fields: _defaultFieldValuesV4(),
innerFields: _defaultInnerFieldValuesV4(),
endPos: null,
);
static List<HeaderFields> _requiredFields(int majorVersion) { static List<HeaderFields> _requiredFields(int majorVersion) {
if (majorVersion < 3) { if (majorVersion < 3) {
throw KdbxUnsupportedException('Unsupported version: $majorVersion'); throw KdbxUnsupportedException('Unsupported version: $majorVersion');
@ -113,12 +133,22 @@ class KdbxHeader {
// HeaderFields.InnerRandomStreamID // HeaderFields.InnerRandomStreamID
]; ];
} else { } else {
// TODO kdbx 4 support return baseHeaders + [HeaderFields.KdfParameters];
throw KdbxUnsupportedException('We do not support kdbx 4.x right now');
return baseHeaders + [HeaderFields.KdfParameters]; // ignore: dead_code
} }
} }
static VarDictionary _createKdfDefaultParameters() {
return VarDictionary([
KdfField.uuid
.item(KeyEncrypterKdf.kdfUuidForType(KdfType.Argon2).toBytes()),
KdfField.salt.item(ByteUtils.randomBytes(Consts.DefaultKdfSaltLength)),
KdfField.parallelism.item(Consts.DefaultKdfParallelism),
KdfField.iterations.item(Consts.DefaultKdfIterations),
KdfField.memory.item(Consts.DefaultKdfMemory),
KdfField.version.item(Consts.DefaultKdfVersion),
]);
}
void _validate() { void _validate() {
for (HeaderFields required in _requiredFields(versionMajor)) { for (HeaderFields required in _requiredFields(versionMajor)) {
if (fields[required] == null) { if (fields[required] == null) {
@ -127,6 +157,18 @@ class KdbxHeader {
} }
} }
void _validateInner() {
final requiredFields = [
InnerHeaderFields.InnerRandomStreamID,
InnerHeaderFields.InnerRandomStreamKey
];
for (final field in requiredFields) {
if (innerFields[field] == null) {
throw KdbxCorruptedFileException('Missing inner header $field');
}
}
}
void _setHeaderField(HeaderFields field, Uint8List bytes) { void _setHeaderField(HeaderFields field, Uint8List bytes) {
fields[field] = HeaderField(field, bytes); fields[field] = HeaderField(field, bytes);
} }
@ -145,9 +187,22 @@ class KdbxHeader {
_setHeaderField( _setHeaderField(
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32)); HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32));
_setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16)); _setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16));
} else if (versionMajor < 5) {
_setInnerHeaderField(
InnerHeaderFields.InnerRandomStreamKey, ByteUtils.randomBytes(64));
final kdfParameters = readKdfParameters;
KdfField.salt.write(
kdfParameters, ByteUtils.randomBytes(Consts.DefaultKdfSaltLength));
// var ivLength = this.dataCipherUuid.toString() === Consts.CipherId.ChaCha20 ? 12 : 16;
// this.encryptionIV = Random.getBytes(ivLength);
final cipherId = base64.encode(fields[HeaderFields.CipherID].bytes);
final ivLength =
cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid ? 12 : 16;
_setHeaderField(
HeaderFields.EncryptionIV, ByteUtils.randomBytes(ivLength));
} else { } else {
throw KdbxUnsupportedException( throw KdbxUnsupportedException(
'We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)'); 'We do not support Kdbx 3.x and 4.x right now. ($versionMajor.$versionMinor)');
} }
} }
@ -168,6 +223,26 @@ class KdbxHeader {
_writeField(writer, HeaderFields.EndOfHeader); _writeField(writer, HeaderFields.EndOfHeader);
} }
void writeInnerHeader(WriterHelper writer) {
_validateInner();
for (final field in InnerHeaderFields.values
.where((f) => f != InnerHeaderFields.EndOfHeader)) {
_writeInnerField(writer, field);
}
_writeInnerField(writer, InnerHeaderFields.EndOfHeader);
}
void _writeInnerField(WriterHelper writer, InnerHeaderFields field) {
final value = innerFields[field];
if (value == null) {
return;
}
_logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes);
}
void _writeField(WriterHelper writer, HeaderFields field) { void _writeField(WriterHelper writer, HeaderFields field) {
final value = fields[field]; final value = fields[field];
if (value == null) { if (value == null) {
@ -201,6 +276,25 @@ class KdbxHeader {
.indexOf(ProtectedValueEncryption.salsa20))), .indexOf(ProtectedValueEncryption.salsa20))),
].map((f) => MapEntry(f.field, f))); ].map((f) => MapEntry(f.field, f)));
static Map<HeaderFields, HeaderField> _defaultFieldValuesV4() =>
_defaultFieldValues()
..remove(HeaderFields.TransformRounds)
..remove(HeaderFields.InnerRandomStreamID)
..remove(HeaderFields.ProtectedStreamKey)
..also((fields) {
fields[HeaderFields.KdfParameters] = HeaderField(
HeaderFields.KdfParameters,
_createKdfDefaultParameters().write());
});
static Map<InnerHeaderFields, InnerHeaderField>
_defaultInnerFieldValuesV4() => Map.fromEntries([
InnerHeaderField(
InnerHeaderFields.InnerRandomStreamID,
WriterHelper.singleUint32Bytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.chaCha20)))
].map((f) => MapEntry(f.field, f)));
static KdbxHeader read(ReaderHelper reader) { static KdbxHeader read(ReaderHelper reader) {
// reading signature // reading signature
final sig1 = reader.readUint32(); final sig1 = reader.readUint32();
@ -281,7 +375,7 @@ class KdbxHeader {
final int versionMinor; final int versionMinor;
final int versionMajor; final int versionMajor;
final Map<HeaderFields, HeaderField> fields; final Map<HeaderFields, HeaderField> fields;
final Map<InnerHeaderFields, InnerHeaderField> innerFields = {}; final Map<InnerHeaderFields, InnerHeaderField> innerFields;
/// end position of the header, if we have been reading from a stream. /// end position of the header, if we have been reading from a stream.
final int endPos; final int endPos;
@ -313,6 +407,9 @@ class KdbxHeader {
VarDictionary get readKdfParameters => VarDictionary.read( VarDictionary get readKdfParameters => VarDictionary.read(
ReaderHelper(fields[HeaderFields.KdfParameters].bytes)); ReaderHelper(fields[HeaderFields.KdfParameters].bytes));
void writeKdfParameters(VarDictionary kdfParameters) =>
_setHeaderField(HeaderFields.KdfParameters, kdfParameters.write());
@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}';

15
lib/src/kdbx_var_dictionary.dart

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:kdbx/src/internal/byte_utils.dart'; import 'package:kdbx/src/internal/byte_utils.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@ -103,10 +105,23 @@ class VarDictionary {
return VarDictionary(items); return VarDictionary(items);
} }
static const DEFAULT_VERSION = 0x0100;
final List<VarDictionaryItem<dynamic>> _items; final List<VarDictionaryItem<dynamic>> _items;
final Map<String, VarDictionaryItem<dynamic>> _dict; final Map<String, VarDictionaryItem<dynamic>> _dict;
Uint8List write() {
final writer = WriterHelper();
writer.writeUint16(DEFAULT_VERSION);
for (final item in _items) {
item._valueType.encoder(writer, item._value);
}
writer.writeUint8(0);
return writer.output.toBytes();
}
T get<T>(ValueType<T> type, String key) => _dict[key]?._value as T; T get<T>(ValueType<T> type, String key) => _dict[key]?._value as T;
void set<T>(ValueType<T> type, String key, T value) =>
_dict[key] = VarDictionaryItem<T>(key, type, value);
static VarDictionaryItem<dynamic> _readItem(ReaderHelper reader) { static VarDictionaryItem<dynamic> _readItem(ReaderHelper reader) {
final type = reader.readUint8(); final type = reader.readUint8();

17
lib/src/utils/scope_functions.dart

@ -0,0 +1,17 @@
/// https://github.com/YusukeIwaki/dart-kotlin_flavor/blob/74593dada94bdd8ca78946ad005d3a2624dc833f/lib/scope_functions.dart
/// MIT license: https://github.com/YusukeIwaki/dart-kotlin_flavor/blob/74593dada94bdd8ca78946ad005d3a2624dc833f/LICENSE
ReturnType run<ReturnType>(ReturnType operation()) {
return operation();
}
extension ScopeFunctionsForObject<T extends Object> on T {
ReturnType let<ReturnType>(ReturnType operationFor(T self)) {
return operationFor(this);
}
T also(void operationFor(T self)) {
operationFor(this);
return this;
}
}

BIN
libargon2_ffi.dylib

Binary file not shown.

16
test/kdbx4_test.dart

@ -110,8 +110,22 @@ void main() {
kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf'))); kdbxFormat.read(data, Credentials(ProtectedValue.fromString('asdf')));
final firstEntry = file.body.rootGroup.entries.first; final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKey('Password')).getText(); final pwd = firstEntry.getString(KdbxKey('Password')).getText();
_logger.info('password: $pwd');
expect(pwd, 'MyPassword'); expect(pwd, 'MyPassword');
}); });
}); });
group('Writing', () {
test('Create and save', () {
final credentials = Credentials(ProtectedValue.fromString('asdf'));
final kdbx = kdbxFormat.create(credentials, 'Test Keystore');
final rootGroup = kdbx.body.rootGroup;
final entry = KdbxEntry.create(kdbx, rootGroup);
rootGroup.addEntry(entry);
entry.setString(
KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum'));
final saved = kdbx.save();
final loadedKdbx = kdbxFormat.read(saved, credentials);
File('test_v4.kdbx').writeAsBytesSync(saved);
});
});
} }

Loading…
Cancel
Save