|
|
|
@ -6,6 +6,7 @@ import 'package:convert/convert.dart' as convert;
|
|
|
|
|
import 'package:crypto/crypto.dart' as crypto; |
|
|
|
|
import 'package:kdbx/src/crypto/protected_salt_generator.dart'; |
|
|
|
|
import 'package:kdbx/src/crypto/protected_value.dart'; |
|
|
|
|
import 'package:kdbx/src/internal/async_utils.dart'; |
|
|
|
|
import 'package:kdbx/src/internal/byte_utils.dart'; |
|
|
|
|
import 'package:kdbx/src/internal/crypto_utils.dart'; |
|
|
|
|
import 'package:kdbx/src/kdbx_group.dart'; |
|
|
|
@ -34,8 +35,10 @@ class Credentials {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class KdbxFile { |
|
|
|
|
KdbxFile(this.credentials, this.header, this.body); |
|
|
|
|
class KdbxFile with Changeable<KdbxFile> { |
|
|
|
|
KdbxFile(this.credentials, this.header, this.body) { |
|
|
|
|
_subscribeToChildren(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static final protectedValues = Expando<ProtectedValue>(); |
|
|
|
|
|
|
|
|
@ -43,23 +46,57 @@ class KdbxFile {
|
|
|
|
|
return protectedValues[node]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void setProtectedValueForNode(xml.XmlElement node, ProtectedValue value) { |
|
|
|
|
static void setProtectedValueForNode( |
|
|
|
|
xml.XmlElement node, ProtectedValue value) { |
|
|
|
|
protectedValues[node] = value; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final StreamSubscriptions _subscriptions = StreamSubscriptions(); |
|
|
|
|
|
|
|
|
|
final Credentials credentials; |
|
|
|
|
final KdbxHeader header; |
|
|
|
|
final KdbxBody body; |
|
|
|
|
|
|
|
|
|
Uint8List save() { |
|
|
|
|
assert(header.versionMajor == 3); |
|
|
|
|
final output = BytesBuilder(); |
|
|
|
|
final writer = WriterHelper(output); |
|
|
|
|
header.generateSalts(); |
|
|
|
|
header.write(writer); |
|
|
|
|
body.write(writer, this); |
|
|
|
|
|
|
|
|
|
final streamKey = |
|
|
|
|
header.fields[HeaderFields.ProtectedStreamKey].bytes.asUint8List(); |
|
|
|
|
final gen = ProtectedSaltGenerator(streamKey); |
|
|
|
|
|
|
|
|
|
body.meta.headerHash.set( |
|
|
|
|
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) |
|
|
|
|
.buffer); |
|
|
|
|
body.writeV3(writer, this, gen); |
|
|
|
|
return output.toBytes(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Iterable<KdbxObject> get _allObjects => body.rootGroup |
|
|
|
|
.getAllGroups() |
|
|
|
|
.cast<KdbxObject>() |
|
|
|
|
.followedBy(body.rootGroup.getAllEntries()); |
|
|
|
|
|
|
|
|
|
void _subscribeToChildren() { |
|
|
|
|
final allObjects = _allObjects; |
|
|
|
|
for (final obj in allObjects) { |
|
|
|
|
_subscriptions.handle(obj.changes.listen((event) { |
|
|
|
|
if (event.isDirty) { |
|
|
|
|
isDirty = true; |
|
|
|
|
if (event.object is KdbxGroup) { |
|
|
|
|
Future(() { |
|
|
|
|
// resubscribe, just in case some child groups/entries have changed. |
|
|
|
|
_subscriptions.cancelSubscriptions(); |
|
|
|
|
_subscribeToChildren(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
})); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class KdbxBody extends KdbxNode { |
|
|
|
@ -77,35 +114,32 @@ class KdbxBody extends KdbxNode {
|
|
|
|
|
final KdbxMeta meta; |
|
|
|
|
final KdbxGroup rootGroup; |
|
|
|
|
|
|
|
|
|
void write(WriterHelper writer, KdbxFile kdbxFile) { |
|
|
|
|
assert(kdbxFile.header.versionMajor == 3); |
|
|
|
|
final streamKey = kdbxFile |
|
|
|
|
.header.fields[HeaderFields.ProtectedStreamKey].bytes |
|
|
|
|
.asUint8List(); |
|
|
|
|
final gen = ProtectedSaltGenerator(streamKey); |
|
|
|
|
|
|
|
|
|
_writeV3(writer, kdbxFile, gen); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void _writeV3(WriterHelper writer, KdbxFile kdbxFile, |
|
|
|
|
void writeV3(WriterHelper writer, KdbxFile kdbxFile, |
|
|
|
|
ProtectedSaltGenerator saltGenerator) { |
|
|
|
|
meta.headerHash.set( |
|
|
|
|
(crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) |
|
|
|
|
.buffer); |
|
|
|
|
final xml = generateXml(saltGenerator); |
|
|
|
|
final xmlBytes = utf8.encode(xml.toXmlString()); |
|
|
|
|
final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ? |
|
|
|
|
GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List; |
|
|
|
|
final Uint8List compressedBytes = |
|
|
|
|
(kdbxFile.header.compression == Compression.gzip |
|
|
|
|
? GZipCodec().encode(xmlBytes) |
|
|
|
|
: xmlBytes) as Uint8List; |
|
|
|
|
|
|
|
|
|
final encrypted = _encryptV3(kdbxFile, compressedBytes); |
|
|
|
|
writer.writeBytes(encrypted); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Uint8List _encryptV3(KdbxFile kdbxFile, Uint8List compressedBytes) { |
|
|
|
|
final byteWriter = WriterHelper(); |
|
|
|
|
byteWriter.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List()); |
|
|
|
|
byteWriter.writeBytes(kdbxFile |
|
|
|
|
.header.fields[HeaderFields.StreamStartBytes].bytes |
|
|
|
|
.asUint8List()); |
|
|
|
|
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter); |
|
|
|
|
final bytes = byteWriter.output.toBytes(); |
|
|
|
|
|
|
|
|
|
final masterKey = KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials); |
|
|
|
|
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List()); |
|
|
|
|
// writer.writeBytes(kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes.asUint8List()); |
|
|
|
|
writer.writeBytes(encrypted); |
|
|
|
|
final masterKey = |
|
|
|
|
KdbxFormat._generateMasterKeyV3(kdbxFile.header, kdbxFile.credentials); |
|
|
|
|
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, |
|
|
|
|
kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes.asUint8List()); |
|
|
|
|
return encrypted; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { |
|
|
|
@ -124,12 +158,16 @@ class KdbxBody extends KdbxNode {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
final builder = xml.XmlBuilder(); |
|
|
|
|
builder.processing('xml', 'version="1.0" encoding="utf-8" standalone="yes"'); |
|
|
|
|
builder.element('KeePassFile', nest: [ |
|
|
|
|
builder.processing( |
|
|
|
|
'xml', 'version="1.0" encoding="utf-8" standalone="yes"'); |
|
|
|
|
builder.element( |
|
|
|
|
'KeePassFile', |
|
|
|
|
nest: [ |
|
|
|
|
meta.toXml(), |
|
|
|
|
() => builder.element('Root', nest: rootGroupNode),],); |
|
|
|
|
() => builder.element('Root', nest: rootGroupNode), |
|
|
|
|
], |
|
|
|
|
); |
|
|
|
|
// final doc = xml.XmlDocument(); |
|
|
|
|
// doc.children.add(xml.XmlProcessing( |
|
|
|
|
// 'xml', 'version="1.0" encoding="utf-8" standalone="yes"')); |
|
|
|
@ -224,7 +262,8 @@ class KdbxFormat {
|
|
|
|
|
final decryptCipher = CBCBlockCipher(AESFastEngine()); |
|
|
|
|
decryptCipher.init(false, |
|
|
|
|
ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); |
|
|
|
|
final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); |
|
|
|
|
final paddedDecrypted = |
|
|
|
|
AesHelper.processBlocks(decryptCipher, encryptedPayload); |
|
|
|
|
final decrypted = AesHelper.unpad(paddedDecrypted); |
|
|
|
|
|
|
|
|
|
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; |
|
|
|
@ -265,11 +304,12 @@ class KdbxFormat {
|
|
|
|
|
return masterKey; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static Uint8List _encryptDataAes(Uint8List masterKey, Uint8List payload, Uint8List encryptionIv) { |
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
|
encryptCipher.init( |
|
|
|
|
true, ParametersWithIV(KeyParameter(masterKey), encryptionIv)); |
|
|
|
|
return AesHelper.processBlocks( |
|
|
|
|
encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|