|
|
@ -5,6 +5,7 @@ import 'dart:typed_data'; |
|
|
|
|
|
|
|
|
|
|
|
import 'package:archive/archive.dart'; |
|
|
|
import 'package:archive/archive.dart'; |
|
|
|
import 'package:argon2_ffi_base/argon2_ffi_base.dart'; |
|
|
|
import 'package:argon2_ffi_base/argon2_ffi_base.dart'; |
|
|
|
|
|
|
|
import 'package:collection/collection.dart' show IterableExtension; |
|
|
|
import 'package:convert/convert.dart' as convert; |
|
|
|
import 'package:convert/convert.dart' as convert; |
|
|
|
import 'package:crypto/crypto.dart' as crypto; |
|
|
|
import 'package:crypto/crypto.dart' as crypto; |
|
|
|
import 'package:kdbx/kdbx.dart'; |
|
|
|
import 'package:kdbx/kdbx.dart'; |
|
|
@ -36,7 +37,7 @@ final _logger = Logger('kdbx.format'); |
|
|
|
abstract class Credentials { |
|
|
|
abstract class Credentials { |
|
|
|
factory Credentials(ProtectedValue password) => |
|
|
|
factory Credentials(ProtectedValue password) => |
|
|
|
Credentials.composite(password, null); //PasswordCredentials(password); |
|
|
|
Credentials.composite(password, null); //PasswordCredentials(password); |
|
|
|
factory Credentials.composite(ProtectedValue password, Uint8List keyFile) => |
|
|
|
factory Credentials.composite(ProtectedValue password, Uint8List? keyFile) => |
|
|
|
KeyFileComposite( |
|
|
|
KeyFileComposite( |
|
|
|
password: password == null ? null : PasswordCredentials(password), |
|
|
|
password: password == null ? null : PasswordCredentials(password), |
|
|
|
keyFile: keyFile == null ? null : KeyFileCredentials(keyFile), |
|
|
|
keyFile: keyFile == null ? null : KeyFileCredentials(keyFile), |
|
|
@ -48,10 +49,10 @@ abstract class Credentials { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class KeyFileComposite implements Credentials { |
|
|
|
class KeyFileComposite implements Credentials { |
|
|
|
KeyFileComposite({@required this.password, @required this.keyFile}); |
|
|
|
KeyFileComposite({required this.password, required this.keyFile}); |
|
|
|
|
|
|
|
|
|
|
|
PasswordCredentials password; |
|
|
|
PasswordCredentials? password; |
|
|
|
KeyFileCredentials keyFile; |
|
|
|
KeyFileCredentials? keyFile; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
Uint8List getHash() { |
|
|
|
Uint8List getHash() { |
|
|
@ -70,8 +71,8 @@ class KeyFileComposite implements Credentials { |
|
|
|
/// Context used during reading and writing. |
|
|
|
/// Context used during reading and writing. |
|
|
|
class KdbxReadWriteContext { |
|
|
|
class KdbxReadWriteContext { |
|
|
|
KdbxReadWriteContext({ |
|
|
|
KdbxReadWriteContext({ |
|
|
|
@required List<KdbxBinary> binaries, |
|
|
|
required List<KdbxBinary> binaries, |
|
|
|
@required this.header, |
|
|
|
required this.header, |
|
|
|
}) : assert(binaries != null), |
|
|
|
}) : assert(binaries != null), |
|
|
|
assert(header != null), |
|
|
|
assert(header != null), |
|
|
|
_binaries = binaries, |
|
|
|
_binaries = binaries, |
|
|
@ -80,7 +81,7 @@ class KdbxReadWriteContext { |
|
|
|
static final kdbxContext = Expando<KdbxReadWriteContext>(); |
|
|
|
static final kdbxContext = Expando<KdbxReadWriteContext>(); |
|
|
|
|
|
|
|
|
|
|
|
static KdbxReadWriteContext kdbxContextForNode(xml.XmlNode node) { |
|
|
|
static KdbxReadWriteContext kdbxContextForNode(xml.XmlNode node) { |
|
|
|
final ret = kdbxContext[node.document]; |
|
|
|
final ret = kdbxContext[node.document!]; |
|
|
|
if (ret == null) { |
|
|
|
if (ret == null) { |
|
|
|
throw StateError('Unable to locate kdbx context for document.'); |
|
|
|
throw StateError('Unable to locate kdbx context for document.'); |
|
|
|
} |
|
|
|
} |
|
|
@ -89,7 +90,7 @@ class KdbxReadWriteContext { |
|
|
|
|
|
|
|
|
|
|
|
static void setKdbxContextForNode( |
|
|
|
static void setKdbxContextForNode( |
|
|
|
xml.XmlNode node, KdbxReadWriteContext ctx) { |
|
|
|
xml.XmlNode node, KdbxReadWriteContext ctx) { |
|
|
|
kdbxContext[node.document] = ctx; |
|
|
|
kdbxContext[node.document!] = ctx; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// TODO make [_binaries] and [_deletedObjects] late init :-) |
|
|
|
// TODO make [_binaries] and [_deletedObjects] late init :-) |
|
|
@ -109,7 +110,7 @@ class KdbxReadWriteContext { |
|
|
|
_deletedObjects.addAll(deletedObjects); |
|
|
|
_deletedObjects.addAll(deletedObjects); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
KdbxBinary binaryById(int id) { |
|
|
|
KdbxBinary? binaryById(int id) { |
|
|
|
if (id >= _binaries.length) { |
|
|
|
if (id >= _binaries.length) { |
|
|
|
return null; |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
@ -120,21 +121,20 @@ class KdbxReadWriteContext { |
|
|
|
_binaries.add(binary); |
|
|
|
_binaries.add(binary); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
KdbxBinary findBinaryByValue(KdbxBinary binary) { |
|
|
|
KdbxBinary? findBinaryByValue(KdbxBinary binary) { |
|
|
|
// TODO create a hashset or map? |
|
|
|
// TODO create a hashset or map? |
|
|
|
return _binaries.firstWhere((element) => element.valueEqual(binary), |
|
|
|
return _binaries.firstWhereOrNull((element) => element.valueEqual(binary)); |
|
|
|
orElse: () => null); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// finds the ID of the given binary. |
|
|
|
/// finds the ID of the given binary. |
|
|
|
/// if it can't be found, [KdbxCorruptedFileException] is thrown. |
|
|
|
/// if it can't be found, [KdbxCorruptedFileException] is thrown. |
|
|
|
int findBinaryId(KdbxBinary binary) { |
|
|
|
int findBinaryId(KdbxBinary binary) { |
|
|
|
assert(binary != null); |
|
|
|
assert(binary != null); |
|
|
|
assert(!binary.isInline); |
|
|
|
assert(!binary.isInline!); |
|
|
|
final id = _binaries.indexOf(binary); |
|
|
|
final id = _binaries.indexOf(binary); |
|
|
|
if (id < 0) { |
|
|
|
if (id < 0) { |
|
|
|
throw KdbxCorruptedFileException('Unable to find binary.' |
|
|
|
throw KdbxCorruptedFileException('Unable to find binary.' |
|
|
|
' (${binary.value.length},${binary.isInline})'); |
|
|
|
' (${binary.value!.length},${binary.isInline})'); |
|
|
|
} |
|
|
|
} |
|
|
|
return id; |
|
|
|
return id; |
|
|
|
} |
|
|
|
} |
|
|
@ -244,7 +244,7 @@ class KdbxBody extends KdbxNode { |
|
|
|
final xmlBytes = utf8.encode(xml.toXmlString()); |
|
|
|
final xmlBytes = utf8.encode(xml.toXmlString()); |
|
|
|
final compressedBytes = (kdbxFile.header.compression == Compression.gzip |
|
|
|
final compressedBytes = (kdbxFile.header.compression == Compression.gzip |
|
|
|
? KdbxFormat._gzipEncode(xmlBytes as Uint8List) |
|
|
|
? KdbxFormat._gzipEncode(xmlBytes as Uint8List) |
|
|
|
: xmlBytes) as Uint8List; |
|
|
|
: xmlBytes) as Uint8List?; |
|
|
|
|
|
|
|
|
|
|
|
final encrypted = await _encryptV3(kdbxFile, compressedBytes); |
|
|
|
final encrypted = await _encryptV3(kdbxFile, compressedBytes); |
|
|
|
writer.writeBytes(encrypted); |
|
|
|
writer.writeBytes(encrypted); |
|
|
@ -271,34 +271,34 @@ class KdbxBody extends KdbxNode { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<Uint8List> _encryptV3( |
|
|
|
Future<Uint8List> _encryptV3( |
|
|
|
KdbxFile kdbxFile, Uint8List compressedBytes) async { |
|
|
|
KdbxFile kdbxFile, Uint8List? compressedBytes) async { |
|
|
|
final byteWriter = WriterHelper(); |
|
|
|
final byteWriter = WriterHelper(); |
|
|
|
byteWriter.writeBytes( |
|
|
|
byteWriter.writeBytes( |
|
|
|
kdbxFile.header.fields[HeaderFields.StreamStartBytes].bytes); |
|
|
|
kdbxFile.header.fields[HeaderFields.StreamStartBytes]!.bytes!); |
|
|
|
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter); |
|
|
|
HashedBlockReader.writeBlocks(ReaderHelper(compressedBytes), byteWriter); |
|
|
|
final bytes = byteWriter.output.toBytes(); |
|
|
|
final bytes = byteWriter.output.toBytes(); |
|
|
|
|
|
|
|
|
|
|
|
final masterKey = await KdbxFormat._generateMasterKeyV3( |
|
|
|
final masterKey = await KdbxFormat._generateMasterKeyV3( |
|
|
|
kdbxFile.header, kdbxFile.credentials); |
|
|
|
kdbxFile.header, kdbxFile.credentials); |
|
|
|
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, |
|
|
|
final encrypted = KdbxFormat._encryptDataAes(masterKey, bytes, |
|
|
|
kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes); |
|
|
|
kdbxFile.header.fields[HeaderFields.EncryptionIV]!.bytes!); |
|
|
|
return encrypted; |
|
|
|
return encrypted; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Uint8List _encryptV4( |
|
|
|
Uint8List _encryptV4( |
|
|
|
KdbxFile kdbxFile, Uint8List compressedBytes, Uint8List cipherKey) { |
|
|
|
KdbxFile kdbxFile, Uint8List? compressedBytes, Uint8List cipherKey) { |
|
|
|
final header = kdbxFile.header; |
|
|
|
final header = kdbxFile.header; |
|
|
|
final cipher = header.cipher; |
|
|
|
final cipher = header.cipher; |
|
|
|
if (cipher == Cipher.aes) { |
|
|
|
if (cipher == Cipher.aes) { |
|
|
|
_logger.fine('We need AES'); |
|
|
|
_logger.fine('We need AES'); |
|
|
|
final result = kdbxFile.kdbxFormat |
|
|
|
final result = kdbxFile.kdbxFormat |
|
|
|
._encryptContentV4Aes(header, cipherKey, compressedBytes); |
|
|
|
._encryptContentV4Aes(header, cipherKey, compressedBytes!); |
|
|
|
// _logger.fine('Result: ${ByteUtils.toHexList(result)}'); |
|
|
|
// _logger.fine('Result: ${ByteUtils.toHexList(result)}'); |
|
|
|
return result; |
|
|
|
return result; |
|
|
|
} else if (cipher == Cipher.chaCha20) { |
|
|
|
} else if (cipher == Cipher.chaCha20) { |
|
|
|
_logger.fine('We need chacha20'); |
|
|
|
_logger.fine('We need chacha20'); |
|
|
|
return kdbxFile.kdbxFormat |
|
|
|
return kdbxFile.kdbxFormat |
|
|
|
.transformContentV4ChaCha20(header, compressedBytes, cipherKey); |
|
|
|
.transformContentV4ChaCha20(header, compressedBytes!, cipherKey); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
throw UnsupportedError('Unsupported cipherId $cipher'); |
|
|
|
throw UnsupportedError('Unsupported cipherId $cipher'); |
|
|
|
} |
|
|
|
} |
|
|
@ -314,7 +314,7 @@ class KdbxBody extends KdbxNode { |
|
|
|
// sync deleted objects. |
|
|
|
// sync deleted objects. |
|
|
|
final deleted = |
|
|
|
final deleted = |
|
|
|
Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e))); |
|
|
|
Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e))); |
|
|
|
final incomingDeleted = <KdbxUuid, KdbxDeletedObject>{}; |
|
|
|
final incomingDeleted = <KdbxUuid?, KdbxDeletedObject>{}; |
|
|
|
|
|
|
|
|
|
|
|
for (final obj in other.ctx._deletedObjects) { |
|
|
|
for (final obj in other.ctx._deletedObjects) { |
|
|
|
if (!deleted.containsKey(obj.uuid)) { |
|
|
|
if (!deleted.containsKey(obj.uuid)) { |
|
|
@ -335,7 +335,7 @@ class KdbxBody extends KdbxNode { |
|
|
|
if (ctx.findBinaryByValue(binary) == null) { |
|
|
|
if (ctx.findBinaryByValue(binary) == null) { |
|
|
|
ctx.addBinary(binary); |
|
|
|
ctx.addBinary(binary); |
|
|
|
mergeContext.trackChange(this, |
|
|
|
mergeContext.trackChange(this, |
|
|
|
debug: 'adding new binary ${binary.value.length}'); |
|
|
|
debug: 'adding new binary ${binary.value!.length}'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
meta.merge(other.meta); |
|
|
|
meta.merge(other.meta); |
|
|
@ -343,7 +343,7 @@ class KdbxBody extends KdbxNode { |
|
|
|
|
|
|
|
|
|
|
|
// remove deleted objects |
|
|
|
// remove deleted objects |
|
|
|
for (final incomingDelete in incomingDeleted.values) { |
|
|
|
for (final incomingDelete in incomingDeleted.values) { |
|
|
|
final object = mergeContext.objectIndex[incomingDelete.uuid]; |
|
|
|
final object = mergeContext.objectIndex![incomingDelete.uuid!]; |
|
|
|
mergeContext.trackChange(object, debug: 'was deleted.'); |
|
|
|
mergeContext.trackChange(object, debug: 'was deleted.'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -352,11 +352,11 @@ class KdbxBody extends KdbxNode { |
|
|
|
_logger.info('Finished merging:\n${mergeContext.debugChanges()}'); |
|
|
|
_logger.info('Finished merging:\n${mergeContext.debugChanges()}'); |
|
|
|
final incomingObjects = other._createObjectIndex(); |
|
|
|
final incomingObjects = other._createObjectIndex(); |
|
|
|
_logger.info('Merged: ${mergeContext.merged} vs. ' |
|
|
|
_logger.info('Merged: ${mergeContext.merged} vs. ' |
|
|
|
'(local objects: ${mergeContext.objectIndex.length}, ' |
|
|
|
'(local objects: ${mergeContext.objectIndex!.length}, ' |
|
|
|
'incoming objects: ${incomingObjects.length})'); |
|
|
|
'incoming objects: ${incomingObjects.length})'); |
|
|
|
|
|
|
|
|
|
|
|
// sanity checks |
|
|
|
// sanity checks |
|
|
|
if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) { |
|
|
|
if (mergeContext.merged.keys.length != mergeContext.objectIndex!.length) { |
|
|
|
// TODO figure out what went wrong. |
|
|
|
// TODO figure out what went wrong. |
|
|
|
} |
|
|
|
} |
|
|
|
return mergeContext; |
|
|
|
return mergeContext; |
|
|
@ -411,23 +411,23 @@ class KdbxBody extends KdbxNode { |
|
|
|
abstract class OverwriteContext { |
|
|
|
abstract class OverwriteContext { |
|
|
|
const OverwriteContext(); |
|
|
|
const OverwriteContext(); |
|
|
|
static const noop = OverwriteContextNoop(); |
|
|
|
static const noop = OverwriteContextNoop(); |
|
|
|
void trackChange(KdbxObject object, {String node, String debug}); |
|
|
|
void trackChange(KdbxObject object, {String? node, String? debug}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class OverwriteContextNoop implements OverwriteContext { |
|
|
|
class OverwriteContextNoop implements OverwriteContext { |
|
|
|
const OverwriteContextNoop(); |
|
|
|
const OverwriteContextNoop(); |
|
|
|
@override |
|
|
|
@override |
|
|
|
void trackChange(KdbxObject object, {String node, String debug}) {} |
|
|
|
void trackChange(KdbxObject object, {String? node, String? debug}) {} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class MergeChange { |
|
|
|
class MergeChange { |
|
|
|
MergeChange({this.object, this.node, this.debug}); |
|
|
|
MergeChange({this.object, this.node, this.debug}); |
|
|
|
|
|
|
|
|
|
|
|
final KdbxNode object; |
|
|
|
final KdbxNode? object; |
|
|
|
|
|
|
|
|
|
|
|
/// the name of the subnode of [object]. |
|
|
|
/// the name of the subnode of [object]. |
|
|
|
final String node; |
|
|
|
final String? node; |
|
|
|
final String debug; |
|
|
|
final String? debug; |
|
|
|
|
|
|
|
|
|
|
|
String debugString() { |
|
|
|
String debugString() { |
|
|
|
return [node, debug].where((e) => e != null).join(' '); |
|
|
|
return [node, debug].where((e) => e != null).join(' '); |
|
|
@ -436,8 +436,8 @@ class MergeChange { |
|
|
|
|
|
|
|
|
|
|
|
class MergeContext implements OverwriteContext { |
|
|
|
class MergeContext implements OverwriteContext { |
|
|
|
MergeContext({this.objectIndex, this.deletedObjects}); |
|
|
|
MergeContext({this.objectIndex, this.deletedObjects}); |
|
|
|
final Map<KdbxUuid, KdbxObject> objectIndex; |
|
|
|
final Map<KdbxUuid, KdbxObject>? objectIndex; |
|
|
|
final Map<KdbxUuid, KdbxDeletedObject> deletedObjects; |
|
|
|
final Map<KdbxUuid?, KdbxDeletedObject>? deletedObjects; |
|
|
|
final Map<KdbxUuid, KdbxObject> merged = {}; |
|
|
|
final Map<KdbxUuid, KdbxObject> merged = {}; |
|
|
|
final List<MergeChange> changes = []; |
|
|
|
final List<MergeChange> changes = []; |
|
|
|
|
|
|
|
|
|
|
@ -450,7 +450,7 @@ class MergeContext implements OverwriteContext { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
void trackChange(KdbxNode object, {String node, String debug}) { |
|
|
|
void trackChange(KdbxNode? object, {String? node, String? debug}) { |
|
|
|
changes.add(MergeChange( |
|
|
|
changes.add(MergeChange( |
|
|
|
object: object, |
|
|
|
object: object, |
|
|
|
node: node, |
|
|
|
node: node, |
|
|
@ -480,7 +480,7 @@ class _KeysV4 { |
|
|
|
class KdbxFormat { |
|
|
|
class KdbxFormat { |
|
|
|
KdbxFormat([this.argon2]) : assert(kdbxKeyCommonAssertConsistency()); |
|
|
|
KdbxFormat([this.argon2]) : assert(kdbxKeyCommonAssertConsistency()); |
|
|
|
|
|
|
|
|
|
|
|
final Argon2 argon2; |
|
|
|
final Argon2? argon2; |
|
|
|
static bool dartWebWorkaround = false; |
|
|
|
static bool dartWebWorkaround = false; |
|
|
|
|
|
|
|
|
|
|
|
/// Creates a new, empty [KdbxFile] with default settings. |
|
|
|
/// Creates a new, empty [KdbxFile] with default settings. |
|
|
@ -488,8 +488,8 @@ class KdbxFormat { |
|
|
|
KdbxFile create( |
|
|
|
KdbxFile create( |
|
|
|
Credentials credentials, |
|
|
|
Credentials credentials, |
|
|
|
String name, { |
|
|
|
String name, { |
|
|
|
String generator, |
|
|
|
String? generator, |
|
|
|
KdbxHeader header, |
|
|
|
KdbxHeader? header, |
|
|
|
}) { |
|
|
|
}) { |
|
|
|
header ??= argon2 == null ? KdbxHeader.createV3() : KdbxHeader.createV4(); |
|
|
|
header ??= argon2 == null ? KdbxHeader.createV3() : KdbxHeader.createV4(); |
|
|
|
final ctx = KdbxReadWriteContext(binaries: [], header: header); |
|
|
|
final ctx = KdbxReadWriteContext(binaries: [], header: header); |
|
|
@ -546,7 +546,7 @@ class KdbxFormat { |
|
|
|
throw UnsupportedError('Unsupported version ${header.version}'); |
|
|
|
throw UnsupportedError('Unsupported version ${header.version}'); |
|
|
|
} else if (file.header.version < KdbxVersion.V4) { |
|
|
|
} else if (file.header.version < KdbxVersion.V4) { |
|
|
|
final streamKey = |
|
|
|
final streamKey = |
|
|
|
file.header.fields[HeaderFields.ProtectedStreamKey].bytes; |
|
|
|
file.header.fields[HeaderFields.ProtectedStreamKey]!.bytes!; |
|
|
|
final gen = ProtectedSaltGenerator(streamKey); |
|
|
|
final gen = ProtectedSaltGenerator(streamKey); |
|
|
|
|
|
|
|
|
|
|
|
body.meta.headerHash.set(headerHash.buffer); |
|
|
|
body.meta.headerHash.set(headerHash.buffer); |
|
|
@ -712,7 +712,7 @@ class KdbxFormat { |
|
|
|
|
|
|
|
|
|
|
|
Uint8List transformContentV4ChaCha20( |
|
|
|
Uint8List transformContentV4ChaCha20( |
|
|
|
KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) { |
|
|
|
KdbxHeader header, Uint8List encrypted, Uint8List cipherKey) { |
|
|
|
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; |
|
|
|
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; |
|
|
|
final chaCha = ChaCha7539Engine() |
|
|
|
final chaCha = ChaCha7539Engine() |
|
|
|
..init(true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); |
|
|
|
..init(true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); |
|
|
|
return chaCha.process(encrypted); |
|
|
|
return chaCha.process(encrypted); |
|
|
@ -735,7 +735,7 @@ class KdbxFormat { |
|
|
|
|
|
|
|
|
|
|
|
Future<_KeysV4> _computeKeysV4( |
|
|
|
Future<_KeysV4> _computeKeysV4( |
|
|
|
KdbxHeader header, Credentials credentials) async { |
|
|
|
KdbxHeader header, Credentials credentials) async { |
|
|
|
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; |
|
|
|
final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes!; |
|
|
|
final kdfParameters = header.readKdfParameters; |
|
|
|
final kdfParameters = header.readKdfParameters; |
|
|
|
if (masterSeed.length != 32) { |
|
|
|
if (masterSeed.length != 32) { |
|
|
|
throw const FormatException('Master seed must be 32 bytes.'); |
|
|
|
throw const FormatException('Master seed must be 32 bytes.'); |
|
|
@ -743,7 +743,7 @@ class KdbxFormat { |
|
|
|
|
|
|
|
|
|
|
|
final credentialHash = credentials.getHash(); |
|
|
|
final credentialHash = credentials.getHash(); |
|
|
|
final key = |
|
|
|
final key = |
|
|
|
await KeyEncrypterKdf(argon2).encrypt(credentialHash, kdfParameters); |
|
|
|
await KeyEncrypterKdf(argon2!).encrypt(credentialHash, kdfParameters); |
|
|
|
|
|
|
|
|
|
|
|
// final keyWithSeed = Uint8List(65); |
|
|
|
// final keyWithSeed = Uint8List(65); |
|
|
|
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); |
|
|
|
// keyWithSeed.replaceRange(0, masterSeed.length, masterSeed); |
|
|
@ -762,9 +762,9 @@ class KdbxFormat { |
|
|
|
final protectedValueEncryption = header.innerRandomStreamEncryption; |
|
|
|
final protectedValueEncryption = header.innerRandomStreamEncryption; |
|
|
|
final streamKey = header.protectedStreamKey; |
|
|
|
final streamKey = header.protectedStreamKey; |
|
|
|
if (protectedValueEncryption == ProtectedValueEncryption.salsa20) { |
|
|
|
if (protectedValueEncryption == ProtectedValueEncryption.salsa20) { |
|
|
|
return ProtectedSaltGenerator(streamKey); |
|
|
|
return ProtectedSaltGenerator(streamKey!); |
|
|
|
} else if (protectedValueEncryption == ProtectedValueEncryption.chaCha20) { |
|
|
|
} else if (protectedValueEncryption == ProtectedValueEncryption.chaCha20) { |
|
|
|
return ProtectedSaltGenerator.chacha20(streamKey); |
|
|
|
return ProtectedSaltGenerator.chacha20(streamKey!); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
throw KdbxUnsupportedException( |
|
|
|
throw KdbxUnsupportedException( |
|
|
|
'Inner encryption: $protectedValueEncryption'); |
|
|
|
'Inner encryption: $protectedValueEncryption'); |
|
|
@ -789,7 +789,7 @@ class KdbxFormat { |
|
|
|
KdbxFile.protectedValues[el] = ProtectedValue.fromString(pw); |
|
|
|
KdbxFile.protectedValues[el] = ProtectedValue.fromString(pw); |
|
|
|
} catch (e, stackTrace) { |
|
|
|
} catch (e, stackTrace) { |
|
|
|
final stringKey = |
|
|
|
final stringKey = |
|
|
|
el.parentElement.singleElement(KdbxXml.NODE_KEY)?.text; |
|
|
|
el.parentElement!.singleElement(KdbxXml.NODE_KEY)?.text; |
|
|
|
final uuid = el.parentElement?.parentElement |
|
|
|
final uuid = el.parentElement?.parentElement |
|
|
|
?.singleElement(KdbxXml.NODE_UUID) |
|
|
|
?.singleElement(KdbxXml.NODE_UUID) |
|
|
|
?.text; |
|
|
|
?.text; |
|
|
@ -811,14 +811,14 @@ class KdbxFormat { |
|
|
|
final kdbxMeta = KdbxMeta.read(meta, ctx); |
|
|
|
final kdbxMeta = KdbxMeta.read(meta, ctx); |
|
|
|
// kdbx < 4 has binaries in the meta section, >= 4 in the binary header. |
|
|
|
// kdbx < 4 has binaries in the meta section, >= 4 in the binary header. |
|
|
|
final binaries = kdbxMeta.binaries?.isNotEmpty == true |
|
|
|
final binaries = kdbxMeta.binaries?.isNotEmpty == true |
|
|
|
? kdbxMeta.binaries |
|
|
|
? kdbxMeta.binaries! |
|
|
|
: header.innerHeader.binaries |
|
|
|
: header.innerHeader.binaries |
|
|
|
.map((e) => KdbxBinary.readBinaryInnerHeader(e)); |
|
|
|
.map((e) => KdbxBinary.readBinaryInnerHeader(e)); |
|
|
|
|
|
|
|
|
|
|
|
final deletedObjects = root |
|
|
|
final deletedObjects = root |
|
|
|
.findElements(KdbxXml.NODE_DELETED_OBJECTS) |
|
|
|
.findElements(KdbxXml.NODE_DELETED_OBJECTS) |
|
|
|
.singleOrNull |
|
|
|
.singleOrNull |
|
|
|
?.let((el) => el |
|
|
|
?.let((el) => el! |
|
|
|
.findElements(KdbxDeletedObject.NODE_NAME) |
|
|
|
.findElements(KdbxDeletedObject.NODE_NAME) |
|
|
|
.map((node) => KdbxDeletedObject.read(node, ctx))) ?? |
|
|
|
.map((node) => KdbxDeletedObject.read(node, ctx))) ?? |
|
|
|
[]; |
|
|
|
[]; |
|
|
@ -832,14 +832,14 @@ class KdbxFormat { |
|
|
|
|
|
|
|
|
|
|
|
Uint8List _decryptContent( |
|
|
|
Uint8List _decryptContent( |
|
|
|
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) { |
|
|
|
KdbxHeader header, Uint8List masterKey, 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(masterKey), encryptionIv)); |
|
|
|
final paddedDecrypted = |
|
|
|
final paddedDecrypted = |
|
|
|
AesHelper.processBlocks(decryptCipher, encryptedPayload); |
|
|
|
AesHelper.processBlocks(decryptCipher, encryptedPayload); |
|
|
|
|
|
|
|
|
|
|
|
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; |
|
|
|
final streamStart = header.fields[HeaderFields.StreamStartBytes]!.bytes!; |
|
|
|
|
|
|
|
|
|
|
|
if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) { |
|
|
|
if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) { |
|
|
|
_logger.warning( |
|
|
|
_logger.warning( |
|
|
@ -861,7 +861,7 @@ class KdbxFormat { |
|
|
|
|
|
|
|
|
|
|
|
Uint8List _decryptContentV4( |
|
|
|
Uint8List _decryptContentV4( |
|
|
|
KdbxHeader header, Uint8List cipherKey, 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( |
|
|
@ -876,7 +876,7 @@ class KdbxFormat { |
|
|
|
/// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?) |
|
|
|
/// TODO combine this with [_decryptContentV4] (or [_encryptDataAes]?) |
|
|
|
Uint8List _encryptContentV4Aes( |
|
|
|
Uint8List _encryptContentV4Aes( |
|
|
|
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) { |
|
|
|
KdbxHeader header, Uint8List cipherKey, Uint8List bytes) { |
|
|
|
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes; |
|
|
|
final encryptionIv = header.fields[HeaderFields.EncryptionIV]!.bytes!; |
|
|
|
final encryptCypher = CBCBlockCipher(AESFastEngine()); |
|
|
|
final encryptCypher = CBCBlockCipher(AESFastEngine()); |
|
|
|
encryptCypher.init( |
|
|
|
encryptCypher.init( |
|
|
|
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); |
|
|
|
true, ParametersWithIV(KeyParameter(cipherKey), encryptionIv)); |
|
|
@ -887,10 +887,10 @@ class KdbxFormat { |
|
|
|
static Future<Uint8List> _generateMasterKeyV3( |
|
|
|
static Future<Uint8List> _generateMasterKeyV3( |
|
|
|
KdbxHeader header, Credentials credentials) async { |
|
|
|
KdbxHeader header, Credentials credentials) async { |
|
|
|
final rounds = header.v3KdfTransformRounds; |
|
|
|
final rounds = header.v3KdfTransformRounds; |
|
|
|
final seed = header.fields[HeaderFields.TransformSeed].bytes; |
|
|
|
final seed = header.fields[HeaderFields.TransformSeed]!.bytes; |
|
|
|
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes; |
|
|
|
final masterSeed = header.fields[HeaderFields.MasterSeed]!.bytes!; |
|
|
|
_logger.finer( |
|
|
|
_logger.finer( |
|
|
|
'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds].bytes)})'); |
|
|
|
'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds]!.bytes)})'); |
|
|
|
final transformedKey = await KeyEncrypterKdf.encryptAesAsync( |
|
|
|
final transformedKey = await KeyEncrypterKdf.encryptAesAsync( |
|
|
|
EncryptAesArgs(seed, credentials.getHash(), rounds)); |
|
|
|
EncryptAesArgs(seed, credentials.getHash(), rounds)); |
|
|
|
|
|
|
|
|
|
|
@ -909,9 +909,9 @@ class KdbxFormat { |
|
|
|
encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); |
|
|
|
encryptCipher, AesHelper.pad(payload, encryptCipher.blockSize)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
static Uint8List _gzipEncode(Uint8List bytes) { |
|
|
|
static Uint8List? _gzipEncode(Uint8List bytes) { |
|
|
|
if (dartWebWorkaround) { |
|
|
|
if (dartWebWorkaround) { |
|
|
|
return GZipEncoder().encode(bytes) as Uint8List; |
|
|
|
return GZipEncoder().encode(bytes) as Uint8List?; |
|
|
|
} |
|
|
|
} |
|
|
|
return GZipCodec().encode(bytes) as Uint8List; |
|
|
|
return GZipCodec().encode(bytes) as Uint8List; |
|
|
|
} |
|
|
|
} |
|
|
|