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>derivator</w>
<w>encrypter</w>
<w>hmac</w>
<w>kdbx</w>
</words>
</dictionary>

10
lib/src/crypto/key_encrypter_kdf.dart

@ -19,6 +19,7 @@ class KdfField<T> {
final String field;
final ValueType<T> type;
static final uuid = KdfField('\$UUID', ValueType.typeBytes);
static final salt = KdfField('S', ValueType.typeBytes);
static final parallelism = KdfField('P', ValueType.typeUInt32);
static final memory = KdfField('M', ValueType.typeUInt64);
@ -45,6 +46,10 @@ class KdfField<T> {
}
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) {
final value = dict.get(type, field);
final strValue = type == ValueType.typeBytes
@ -61,6 +66,11 @@ class KeyEncrypterKdf {
'72Nt34wpREuR96mkA+MKDA==': KdfType.Argon2,
'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;

159
lib/src/kdbx_format.dart

@ -119,7 +119,7 @@ class HashCredentials implements Credentials {
}
class KdbxFile {
KdbxFile(this.credentials, this.header, this.body) {
KdbxFile(this.kdbxFormat, this.credentials, this.header, this.body) {
for (final obj in _allObjects) {
obj.file = this;
}
@ -136,6 +136,7 @@ class KdbxFile {
protectedValues[node] = value;
}
final KdbxFormat kdbxFormat;
final Credentials credentials;
final KdbxHeader header;
final KdbxBody body;
@ -147,19 +148,30 @@ class KdbxFile {
_dirtyObjectsChanged.stream;
Uint8List save() {
assert(header.versionMajor == 3);
final output = BytesBuilder();
final writer = WriterHelper(output);
header.generateSalts();
header.write(writer);
final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey);
body.meta.headerHash.set(
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List)
.buffer);
body.writeV3(writer, this, gen);
final headerHash =
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List);
if (header.versionMajor <= 3) {
final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey);
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();
_dirtyObjectsChanged.add(dirtyObjects);
return output.toBytes();
@ -226,6 +238,24 @@ class KdbxBody extends KdbxNode {
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) {
final byteWriter = WriterHelper();
byteWriter.writeBytes(
@ -240,6 +270,23 @@ class KdbxBody extends KdbxNode {
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) {
final rootGroupNode = rootGroup.toXml();
// 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 {
KdbxFormat([this.argon2]);
@ -297,7 +351,7 @@ class KdbxFormat {
);
final rootGroup = KdbxGroup.create(parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup);
return KdbxFile(credentials, header, body);
return KdbxFile(this, credentials, header, body);
}
KdbxFile read(Uint8List input, Credentials credentials) {
@ -327,10 +381,10 @@ class KdbxFormat {
if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml);
return KdbxFile(credentials, header, _loadXml(header, string));
return KdbxFile(this, credentials, header, _loadXml(header, string));
} else {
return KdbxFile(
credentials, header, _loadXml(header, utf8.decode(blocks)));
this, credentials, header, _loadXml(header, utf8.decode(blocks)));
}
}
@ -347,27 +401,12 @@ class KdbxFormat {
_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 keys = _computeKeysV4(header, credentials);
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);
_logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
_logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}');
// _logger.fine('Expected: ${ByteUtils.toHexList(expectedHmac)}');
// _logger.fine('Actual : ${ByteUtils.toHexList(headerHmac.bytes)}');
if (!ByteUtils.eq(hash, actualHash)) {
throw KdbxInvalidKeyException();
}
@ -375,17 +414,16 @@ class KdbxFormat {
// final blockreader.readBytes(32);
final bodyStuff = hmacBlockTransformer(reader);
_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}');
if (header.compression == Compression.gzip) {
final content = GZipCodec().decode(decrypted) as Uint8List;
final contentReader = ReaderHelper(content);
final headerFields = KdbxHeader.readInnerHeaderFields(contentReader, 4);
_logger.fine('inner header fields: $headerFields');
// _logger.fine('inner header fields: $headerFields');
header.innerFields.addAll(headerFields);
final xml = utf8.decode(contentReader.readRemaining());
_logger.fine('content: $xml');
return KdbxFile(credentials, header, _loadXml(header, xml));
return KdbxFile(this, credentials, header, _loadXml(header, xml));
}
return null;
}
@ -414,6 +452,7 @@ class KdbxFormat {
return result;
} else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
_logger.fine('We need chacha20');
throw UnsupportedError('chacha20 not yet supported $cipherId');
} else {
throw UnsupportedError('Unsupported cipherId $cipherId');
}
@ -422,30 +461,45 @@ class KdbxFormat {
// Uint8List _transformDataV4Aes() {
// }
crypto.Digest _getHeaderHmac(
KdbxHeader header, ReaderHelper reader, Uint8List key) {
crypto.Digest _getHeaderHmac(Uint8List headerBytes, 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 src = headerBytes;
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) {
_KeysV4 _computeKeysV4(KdbxHeader header, Credentials credentials) {
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
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();
_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;
final key = KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
_logger.fine('keyv4: ${ByteUtils.toHexList(key)}');
// 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) {
@ -515,11 +569,11 @@ class KdbxFormat {
}
Uint8List _decryptContentV4(
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(
false, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload);
@ -527,6 +581,19 @@ class KdbxFormat {
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(
KdbxHeader header, Credentials credentials) {
final rounds = ReaderHelper.singleUint64(

109
lib/src/kdbx_header.dart

@ -1,11 +1,14 @@
import 'dart:convert';
import 'dart:typed_data';
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/consts.dart';
import 'package:kdbx/src/kdbx_var_dictionary.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:kdbx/src/utils/scope_functions.dart';
final _logger = Logger('kdbx.header');
@ -13,6 +16,11 @@ class Consts {
static const FileMagic = 0x9AA2D903;
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 {
@ -81,7 +89,8 @@ class KdbxHeader {
@required this.versionMajor,
@required this.fields,
@required this.endPos,
});
Map<InnerHeaderFields, InnerHeaderField> innerFields,
}) : innerFields = innerFields ?? {};
KdbxHeader.create()
: this(
@ -93,6 +102,17 @@ class KdbxHeader {
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) {
if (majorVersion < 3) {
throw KdbxUnsupportedException('Unsupported version: $majorVersion');
@ -113,12 +133,22 @@ class KdbxHeader {
// HeaderFields.InnerRandomStreamID
];
} else {
// TODO kdbx 4 support
throw KdbxUnsupportedException('We do not support kdbx 4.x right now');
return baseHeaders + [HeaderFields.KdfParameters]; // ignore: dead_code
return baseHeaders + [HeaderFields.KdfParameters];
}
}
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() {
for (HeaderFields required in _requiredFields(versionMajor)) {
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) {
fields[field] = HeaderField(field, bytes);
}
@ -145,9 +187,22 @@ class KdbxHeader {
_setHeaderField(
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32));
_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 {
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);
}
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) {
final value = fields[field];
if (value == null) {
@ -201,6 +276,25 @@ class KdbxHeader {
.indexOf(ProtectedValueEncryption.salsa20))),
].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) {
// reading signature
final sig1 = reader.readUint32();
@ -281,7 +375,7 @@ class KdbxHeader {
final int versionMinor;
final int versionMajor;
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.
final int endPos;
@ -313,6 +407,9 @@ class KdbxHeader {
VarDictionary get readKdfParameters => VarDictionary.read(
ReaderHelper(fields[HeaderFields.KdfParameters].bytes));
void writeKdfParameters(VarDictionary kdfParameters) =>
_setHeaderField(HeaderFields.KdfParameters, kdfParameters.write());
@override
String toString() {
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:logging/logging.dart';
import 'package:meta/meta.dart';
@ -103,10 +105,23 @@ class VarDictionary {
return VarDictionary(items);
}
static const DEFAULT_VERSION = 0x0100;
final List<VarDictionaryItem<dynamic>> _items;
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;
void set<T>(ValueType<T> type, String key, T value) =>
_dict[key] = VarDictionaryItem<T>(key, type, value);
static VarDictionaryItem<dynamic> _readItem(ReaderHelper reader) {
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')));
final firstEntry = file.body.rootGroup.entries.first;
final pwd = firstEntry.getString(KdbxKey('Password')).getText();
_logger.info('password: $pwd');
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