KeepassX format implementation in pure dart.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

717 lines
25 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:argon2_ffi_base/argon2_ffi_base.dart';
import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto;
import 'package:cryptography/cryptography.dart' as cryptography;
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_binary.dart';
import 'package:kdbx/src/kdbx_file.dart';
import 'package:kdbx/src/kdbx_group.dart';
import 'package:kdbx/src/kdbx_header.dart';
import 'package:kdbx/src/kdbx_meta.dart';
import 'package:kdbx/src/kdbx_object.dart';
import 'package:kdbx/src/kdbx_xml.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pointycastle/export.dart';
import 'package:xml/xml.dart' as xml;
final _logger = Logger('kdbx.format');
abstract class Credentials {
factory Credentials(ProtectedValue password) =>
Credentials.composite(password, null); //PasswordCredentials(password);
factory Credentials.composite(ProtectedValue password, Uint8List keyFile) =>
KeyFileComposite(
password: password == null ? null : PasswordCredentials(password),
keyFile: keyFile == null ? null : KeyFileCredentials(keyFile),
);
factory Credentials.fromHash(Uint8List hash) => HashCredentials(hash);
Uint8List getHash();
}
class KeyFileComposite implements Credentials {
KeyFileComposite({@required this.password, @required this.keyFile});
PasswordCredentials password;
KeyFileCredentials keyFile;
@override
Uint8List getHash() {
final buffer = [...?password?.getBinary(), ...?keyFile?.getBinary()];
return crypto.sha256.convert(buffer).bytes as Uint8List;
// final output = convert.AccumulatorSink<crypto.Digest>();
// final input = crypto.sha256.startChunkedConversion(output);
//// input.add(password.getHash());
// input.add(buffer);
// input.close();
// return output.events.single.bytes as Uint8List;
}
}
/// Context used during reading and writing.
class KdbxReadWriteContext {
KdbxReadWriteContext({
@required List<KdbxBinary> binaries,
@required this.header,
}) : assert(binaries != null),
assert(header != null),
_binaries = binaries;
static final kdbxContext = Expando<KdbxReadWriteContext>();
static KdbxReadWriteContext kdbxContextForNode(xml.XmlParent node) {
final ret = kdbxContext[node.document];
if (ret == null) {
throw StateError('Unable to locate kdbx context for document.');
}
return ret;
}
static void setKdbxContextForNode(
xml.XmlParent node, KdbxReadWriteContext ctx) {
kdbxContext[node.document] = ctx;
}
@protected
final List<KdbxBinary> _binaries;
Iterable<KdbxBinary> get binariesIterable => _binaries;
final KdbxHeader header;
int get versionMajor => header.versionMajor;
KdbxBinary binaryById(int id) {
if (id >= _binaries.length) {
return null;
}
return _binaries[id];
}
void addBinary(KdbxBinary binary) {
_binaries.add(binary);
}
/// finds the ID of the given binary.
/// if it can't be found, [KdbxCorruptedFileException] is thrown.
int findBinaryId(KdbxBinary binary) {
assert(binary != null);
assert(!binary.isInline);
final id = _binaries.indexOf(binary);
if (id < 0) {
throw KdbxCorruptedFileException('Unable to find binary.'
' (${binary.value.length},${binary.isInline})');
}
return id;
}
/// removes the given binary. Does not check if it is still referenced
/// in any [KdbxEntry]!!
void removeBinary(KdbxBinary binary) {
if (!_binaries.remove(binary)) {
throw KdbxCorruptedFileException(
'Tried to remove binary which is not in this file.');
}
}
}
abstract class CredentialsPart {
Uint8List getBinary();
}
class KeyFileCredentials implements CredentialsPart {
factory KeyFileCredentials(Uint8List keyFileContents) {
try {
final keyFileAsString = utf8.decode(keyFileContents);
if (_hexValuePattern.hasMatch(keyFileAsString)) {
return KeyFileCredentials._(ProtectedValue.fromBinary(
convert.hex.decode(keyFileAsString) as Uint8List));
}
final xmlContent = xml.parse(keyFileAsString);
final key = xmlContent.findAllElements('Key').single;
final dataString = key.findElements('Data').single;
final dataBytes = base64.decode(dataString.text);
_logger.finer('Decoded base64 of keyfile.');
return KeyFileCredentials._(ProtectedValue.fromBinary(dataBytes));
} catch (e, stackTrace) {
_logger.warning(
'Unable to parse key file as hex or XML, use as is.', e, stackTrace);
final bytes = crypto.sha256.convert(keyFileContents).bytes as Uint8List;
return KeyFileCredentials._(ProtectedValue.fromBinary(bytes));
}
}
KeyFileCredentials._(this._keyFileValue);
static final RegExp _hexValuePattern =
RegExp(r'^[a-f\d]{64}', caseSensitive: false);
final ProtectedValue _keyFileValue;
@override
Uint8List getBinary() {
return _keyFileValue.binaryValue;
// return crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List;
}
}
class PasswordCredentials implements CredentialsPart {
PasswordCredentials(this._password);
final ProtectedValue _password;
@override
Uint8List getBinary() {
return _password.hash;
}
}
class HashCredentials implements Credentials {
HashCredentials(this.hash);
final Uint8List hash;
@override
Uint8List getHash() => hash;
}
class KdbxBody extends KdbxNode {
KdbxBody.create(this.meta, this.rootGroup) : super.create('KeePassFile') {
node.children.add(meta.node);
final rootNode = xml.XmlElement(xml.XmlName('Root'));
node.children.add(rootNode);
rootNode.children.add(rootGroup.node);
}
KdbxBody.read(
xml.XmlElement node,
this.meta,
this.rootGroup,
) : super.read(node);
// final xml.XmlDocument xmlDocument;
final KdbxMeta meta;
final KdbxGroup rootGroup;
Future<void> writeV3(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator) async {
final xml = generateXml(saltGenerator);
final xmlBytes = utf8.encode(xml.toXmlString());
final compressedBytes = (kdbxFile.header.compression == Compression.gzip
? GZipCodec().encode(xmlBytes)
: xmlBytes) as Uint8List;
final encrypted = await _encryptV3(kdbxFile, compressedBytes);
writer.writeBytes(encrypted);
}
void writeV4(WriterHelper writer, KdbxFile kdbxFile,
ProtectedSaltGenerator saltGenerator, _KeysV4 keys) {
final bodyWriter = WriterHelper();
final xml = generateXml(saltGenerator);
kdbxFile.header.innerHeader.updateBinaries(kdbxFile.ctx.binariesIterable);
kdbxFile.header.writeInnerHeader(bodyWriter);
bodyWriter.writeBytes(utf8.encode(xml.toXmlString()) as Uint8List);
final compressedBytes = (kdbxFile.header.compression == Compression.gzip
? GZipCodec().encode(bodyWriter.output.toBytes())
: bodyWriter.output.toBytes()) as Uint8List;
final encrypted = _encryptV4(
kdbxFile,
compressedBytes,
keys.cipherKey,
);
final transformed = kdbxFile.kdbxFormat
.hmacBlockTransformerEncrypt(keys.hmacKey, encrypted);
writer.writeBytes(transformed);
}
Future<Uint8List> _encryptV3(
KdbxFile kdbxFile, Uint8List compressedBytes) async {
final byteWriter = WriterHelper();
byteWriter.writeBytes(
kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes);
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter);
final bytes = byteWriter.output.toBytes();
final masterKey = await KdbxFormat._generateMasterKeyV3(
kdbxFile.header, kdbxFile.credentials);
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes,
kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes);
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');
return kdbxFile.kdbxFormat
.transformContentV4ChaCha20(header, compressedBytes, cipherKey);
} else {
throw UnsupportedError('Unsupported cipherId $cipherId');
}
}
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
final rootGroupNode = rootGroup.toXml();
// update protected values...
for (final el in rootGroupNode.findAllElements(KdbxXml.NODE_VALUE).where(
(el) =>
el.getAttribute(KdbxXml.ATTR_PROTECTED)?.toLowerCase() == 'true')) {
final pv = KdbxFile.protectedValues[el];
if (pv != null) {
final newValue = saltGenerator.encryptToBase64(pv.getText());
el.children.clear();
el.children.add(xml.XmlText(newValue));
} else {
// assert((() {
// _logger.severe('Unable to find protected value for $el ${el.parent.parent} (children: ${el.children})');
// return false;
// })());
// this is always an error, not just during debug.
throw StateError('Unable to find protected value for $el ${el.parent}');
}
}
final builder = xml.XmlBuilder();
builder.processing(
'xml', 'version="1.0" encoding="utf-8" standalone="yes"');
builder.element(
'KeePassFile',
nest: [
meta.toXml(),
() => builder.element('Root', nest: rootGroupNode),
],
);
// final doc = xml.XmlDocument();
// doc.children.add(xml.XmlProcessing(
// 'xml', 'version="1.0" encoding="utf-8" standalone="yes"'));
final node = builder.build() as xml.XmlDocument;
return node;
}
}
class _KeysV4 {
_KeysV4(this.hmacKey, this.cipherKey);
final Uint8List hmacKey;
final Uint8List cipherKey;
}
class KdbxFormat {
KdbxFormat([this.argon2]);
final Argon2 argon2;
KdbxFile create(
Credentials credentials,
String name, {
String generator,
KdbxHeader header,
}) {
header ??= KdbxHeader.create();
final ctx = KdbxReadWriteContext(binaries: [], header: header);
final meta = KdbxMeta.create(
databaseName: name,
ctx: ctx,
generator: generator,
);
final rootGroup = KdbxGroup.create(ctx: ctx, parent: null, name: name);
final body = KdbxBody.create(meta, rootGroup);
return KdbxFile(
ctx,
this,
credentials,
header,
body,
);
}
Future<KdbxFile> read(Uint8List input, Credentials credentials) async {
final reader = ReaderHelper(input);
final header = KdbxHeader.read(reader);
if (header.versionMajor == 3) {
return await _loadV3(header, reader, credentials);
} else if (header.versionMajor == 4) {
return await _loadV4(header, reader, credentials);
} else {
_logger.finer('Unsupported version for $header');
throw KdbxUnsupportedException('Unsupported kdbx version '
'${header.versionMajor}.${header.versionMinor}.'
' Only 3.x and 4.x is supported.');
}
}
Future<Uint8List> save(KdbxFile file) async {
final body = file.body;
final header = file.header;
final output = BytesBuilder();
final writer = WriterHelper(output);
header.generateSalts();
header.write(writer);
final headerHash =
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List);
if (file.header.versionMajor <= 3) {
final streamKey =
file.header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey);
body.meta.headerHash.set(headerHash.buffer);
await body.writeV3(writer, file, gen);
} else if (header.versionMajor <= 4) {
final headerBytes = writer.output.toBytes();
writer.writeBytes(headerHash);
final gen = _createProtectedSaltGenerator(header);
final keys = await _computeKeysV4(header, file.credentials);
final headerHmac = _getHeaderHmac(headerBytes, keys.hmacKey);
writer.writeBytes(headerHmac.bytes as Uint8List);
body.writeV4(writer, file, gen, keys);
} else {
throw UnsupportedError('Unsupported version ${header.versionMajor}');
}
file.onSaved();
return output.toBytes();
}
Future<KdbxFile> _loadV3(
KdbxHeader header, ReaderHelper reader, Credentials credentials) async {
// _getMasterKeyV3(header, credentials);
final masterKey = await _generateMasterKeyV3(header, credentials);
final encryptedPayload = reader.readRemaining();
final content = _decryptContent(header, masterKey, encryptedPayload);
final blocks = HashedBlockReader.readBlocks(ReaderHelper(content));
_logger.finer('compression: ${header.compression}');
final ctx = KdbxReadWriteContext(binaries: [], header: header);
if (header.compression == Compression.gzip) {
final xml = GZipCodec().decode(blocks);
final string = utf8.decode(xml);
return KdbxFile(
ctx, this, credentials, header, _loadXml(ctx, header, string));
} else {
return KdbxFile(ctx, this, credentials, header,
_loadXml(ctx, header, utf8.decode(blocks)));
}
}
Future<KdbxFile> _loadV4(
KdbxHeader header, ReaderHelper reader, Credentials credentials) async {
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 keys = await _computeKeysV4(header, credentials);
final headerHmac =
_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)}');
if (!ByteUtils.eq(headerHmac.bytes, expectedHmac)) {
throw KdbxInvalidKeyException();
}
// final hmacTransformer = crypto.Hmac(crypto.sha256, hmacKey.bytes);
// final blockreader.readBytes(32);
final bodyContent = hmacBlockTransformer(keys.hmacKey, reader);
final decrypted = decrypt(header, bodyContent, keys.cipherKey);
_logger.finer('compression: ${header.compression}');
if (header.compression == Compression.gzip) {
final content = GZipCodec().decode(decrypted) as Uint8List;
final contentReader = ReaderHelper(content);
final innerHeader = KdbxHeader.readInnerHeaderFields(contentReader, 4);
// _logger.fine('inner header fields: $headerFields');
// header.innerFields.addAll(headerFields);
header.innerHeader.updateFrom(innerHeader);
final xml = utf8.decode(contentReader.readRemaining());
final context = KdbxReadWriteContext(binaries: [], header: header);
return KdbxFile(
context, this, credentials, header, _loadXml(context, header, xml));
}
throw StateError('Kdbx4 without compression is not yet supported.');
}
Uint8List hmacBlockTransformerEncrypt(Uint8List hmacKey, Uint8List data) {
final writer = WriterHelper();
final reader = ReaderHelper(data);
const blockSize = 1024 * 1024;
var blockIndex = 0;
while (true) {
final blockData = reader.readBytesUpTo(blockSize);
final calculatedHash = _hmacHashForBlock(hmacKey, blockIndex, blockData);
writer.writeBytes(calculatedHash);
writer.writeUint32(blockData.length);
if (blockData.isEmpty) {
// writer.writeUint32(0);
return writer.output.toBytes();
}
writer.writeBytes(blockData);
blockIndex++;
}
}
Uint8List _hmacKeyForBlockIndex(Uint8List hmacKey, int blockIndex) {
final blockKeySrc = WriterHelper()
..writeUint64(blockIndex)
..writeBytes(hmacKey);
return crypto.sha512.convert(blockKeySrc.output.toBytes()).bytes
as Uint8List;
}
Uint8List _hmacHashForBlock(
Uint8List hmacKey, int blockIndex, Uint8List blockData) {
final blockKey = _hmacKeyForBlockIndex(hmacKey, blockIndex);
final tmp = WriterHelper();
tmp.writeUint64(blockIndex);
tmp.writeInt32(blockData.length);
tmp.writeBytes(blockData);
// _logger.fine('blockHash: ${ByteUtils.toHexList(tmp.output.toBytes())}');
// _logger.fine('blockKey: ${ByteUtils.toHexList(blockKey.bytes)}');
final hmac = crypto.Hmac(crypto.sha256, blockKey);
final calculatedHash = hmac.convert(tmp.output.toBytes());
return calculatedHash.bytes as Uint8List;
}
Uint8List hmacBlockTransformer(Uint8List hmacKey, ReaderHelper reader) {
final ret = <int>[];
var blockIndex = 0;
while (true) {
final blockHash = reader.readBytes(32);
final blockLength = reader.readUint32();
final blockBytes = reader.readBytes(blockLength);
final calculatedHash = _hmacHashForBlock(hmacKey, blockIndex, blockBytes);
// _logger
// .fine('CalculatedHash: ${ByteUtils.toHexList(calculatedHash.bytes)}');
if (!ByteUtils.eq(blockHash, calculatedHash)) {
throw KdbxCorruptedFileException('Invalid hash block.');
}
if (blockLength < 1) {
return Uint8List.fromList(ret);
}
blockIndex++;
ret.addAll(blockBytes);
}
}
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);
return result;
} else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) {
_logger.fine('We need chacha20');
// throw UnsupportedError('chacha20 not yet supported $cipherId');
return transformContentV4ChaCha20(header, encrypted, cipherKey);
} else {
throw UnsupportedError('Unsupported cipherId $cipherId');
}
}
Uint8List transformContentV4ChaCha20(
KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final key = cryptography.SecretKey(cipherKey);
final nonce = cryptography.SecretKey(encryptionIv);
return cryptography.chacha20.decrypt(encrypted, key, nonce: nonce);
}
// Uint8List _transformDataV4Aes() {
// }
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 = headerBytes;
final hmacKeyStuff = crypto.Hmac(crypto.sha256, hmacKey);
return hmacKeyStuff.convert(src);
}
Future<_KeysV4> _computeKeysV4(
KdbxHeader header, Credentials credentials) async {
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
final kdfParameters = header.readKdfParameters;
if (masterSeed.length != 32) {
throw const FormatException('Master seed must be 32 bytes.');
}
final credentialHash = credentials.getHash();
final key =
await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters);
// final keyWithSeed = Uint8List(65);
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed);
// keyWithSeed.replaceRange(
// masterSeed.length, masterSeed.length + key.length, key);
// keyWithSeed[64] = 1;
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);
return _KeysV4(hmacKey.bytes as Uint8List, cipher.bytes as Uint8List);
}
ProtectedSaltGenerator _createProtectedSaltGenerator(KdbxHeader header) {
final protectedValueEncryption = header.innerRandomStreamEncryption;
final streamKey = header.protectedStreamKey;
if (protectedValueEncryption == ProtectedValueEncryption.salsa20) {
return ProtectedSaltGenerator(streamKey);
} else if (protectedValueEncryption == ProtectedValueEncryption.chaCha20) {
return ProtectedSaltGenerator.chacha20(streamKey);
} else {
throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption');
}
}
KdbxBody _loadXml(
KdbxReadWriteContext ctx, KdbxHeader header, String xmlString) {
final gen = _createProtectedSaltGenerator(header);
final document = xml.parse(xmlString);
KdbxReadWriteContext.setKdbxContextForNode(document, ctx);
for (final el in document
.findAllElements(KdbxXml.NODE_VALUE)
.where((el) => el.getAttributeBool(KdbxXml.ATTR_PROTECTED))) {
final pw = gen.decryptBase64(el.text.trim());
if (pw == null) {
continue;
}
KdbxFile.protectedValues[el] = ProtectedValue.fromString(pw);
}
final keePassFile = document.findElements('KeePassFile').single;
final meta = keePassFile.findElements('Meta').single;
final root = keePassFile.findElements('Root').single;
final kdbxMeta = KdbxMeta.read(meta, ctx);
if (kdbxMeta.binaries?.isNotEmpty == true) {
ctx._binaries.addAll(kdbxMeta.binaries);
} else if (header.innerHeader.binaries.isNotEmpty) {
ctx._binaries.addAll(header.innerHeader.binaries
.map((e) => KdbxBinary.readBinaryInnerHeader(e)));
}
final rootGroup =
KdbxGroup.read(ctx, null, root.findElements('Group').single);
_logger.fine('successfully read Meta.');
return KdbxBody.read(keePassFile, kdbxMeta, rootGroup);
}
Uint8List _decryptContent(
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 streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) {
_logger.warning(
'decrypted content was shorter than expected stream start block.');
throw KdbxInvalidKeyException();
}
if (!ByteUtils.eq(
streamStart, paddedDecrypted.sublist(0, streamStart.lengthInBytes))) {
throw KdbxInvalidKeyException();
}
final decrypted = AesHelper.unpad(paddedDecrypted);
// ignore: unnecessary_cast
final content = decrypted.sublist(streamStart.lengthInBytes) as Uint8List;
return content;
}
Uint8List _decryptContentV4(
KdbxHeader header, Uint8List cipherKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(
false, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload);
final decrypted = AesHelper.unpad(paddedDecrypted);
return decrypted;
}
/// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?)
Uint8List _encryptContentV4Aes(
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final encryptCypher = CBCBlockCipher(AESFastEngine());
encryptCypher.init(
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv));
final paddedBytes = AesHelper.pad(bytes, encryptCypher.blockSize);
return AesHelper.processBlocks(encryptCypher, paddedBytes);
}
static Future<Uint8List> _generateMasterKeyV3(
KdbxHeader header, Credentials credentials) async {
final rounds = ReaderHelper.singleUint64(
header.fields[HeaderFields.TransformRounds].bytes);
final seed = header.fields[HeaderFields.TransformSeed].bytes;
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
_logger.finer(
'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds].bytes)})');
final transformedKey = await KeyEncrypterKdf.encryptAesAsync(
EncryptAesArgs(seed, credentials.getHash(), rounds));
final masterKey = crypto.sha256
.convert(Uint8List.fromList(masterSeed + transformedKey))
.bytes as Uint8List;
return masterKey;
}
static Uint8List _encryptDataAes(
Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) {
final encryptCipher = CBCBlockCipher(AESFastEngine());
encryptCipher.init(
true, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
return AesHelper.processBlocks(
encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize));
}
}