@ -6,7 +6,6 @@ import 'dart:typed_data';
import ' package:archive/archive.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:crypto/crypto.dart ' as crypto ;
import ' package:kdbx/kdbx.dart ' ;
import ' package:kdbx/src/crypto/key_encrypter_kdf.dart ' ;
@ -14,11 +13,14 @@ import 'package:kdbx/src/crypto/protected_salt_generator.dart';
import ' package:kdbx/src/internal/consts.dart ' ;
import ' package:kdbx/src/internal/crypto_utils.dart ' ;
import ' package:kdbx/src/internal/extension_utils.dart ' ;
import ' package:kdbx/src/internal/pointycastle_argon2.dart ' ;
import ' package:kdbx/src/kdbx_deleted_object.dart ' ;
import ' package:kdbx/src/kdbx_entry.dart ' ;
import ' package:kdbx/src/kdbx_group.dart ' ;
import ' package:kdbx/src/kdbx_header.dart ' ;
import ' package:kdbx/src/kdbx_xml.dart ' ;
import ' package:kdbx/src/utils/byte_utils.dart ' ;
import ' package:kdbx/src/utils/sequence.dart ' ;
import ' package:logging/logging.dart ' ;
import ' package:meta/meta.dart ' ;
import ' package:pointycastle/export.dart ' ;
@ -28,40 +30,6 @@ 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 ? . let ( ( that ) = > PasswordCredentials ( that ) ) ,
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 ( {
@ -121,11 +89,11 @@ class KdbxReadWriteContext {
/ / / finds the ID of the given binary .
/ / / if it can ' t be found, [KdbxCorruptedFileException] is thrown.
int findBinaryId ( KdbxBinary binary ) {
assert ( ! binary . isInline ! ) ;
assert ( ! binary . isInline ) ;
final id = _binaries . indexOf ( binary ) ;
if ( id < 0 ) {
throw KdbxCorruptedFileException ( ' Unable to find binary. '
' ( ${ binary . value ! . length } , ${ binary . isInline } ) ' ) ;
' ( ${ binary . value . length } , ${ binary . isInline } ) ' ) ;
}
return id ;
}
@ -138,76 +106,12 @@ class KdbxReadWriteContext {
' 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 . XmlDocument . parse ( keyFileAsString ) ;
final metaVersion =
xmlContent . findAllElements ( ' Version ' ) . singleOrNull ? . text ;
final key = xmlContent . findAllElements ( ' Key ' ) . single ;
final dataString = key . findElements ( ' Data ' ) . single ;
final encoded = dataString . text . replaceAll ( RegExp ( r'\s' ) , ' ' ) ;
Uint8List dataBytes ;
if ( metaVersion ! = null & & metaVersion . startsWith ( ' 2. ' ) ) {
dataBytes = convert . hex . decode ( encoded ) as Uint8List ;
} else {
dataBytes = base64 . decode ( encoded ) ;
}
_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 ;
void addDeletedObject ( KdbxUuid uuid , [ DateTime ? now ] ) {
_deletedObjects . add ( KdbxDeletedObject . create ( this , uuid ) ) ;
}
}
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 ) ;
@ -265,14 +169,14 @@ class KdbxBody extends KdbxNode {
KdbxFile kdbxFile , Uint8List ? compressedBytes ) async {
final byteWriter = WriterHelper ( ) ;
byteWriter . writeBytes (
kdbxFile . header . fields [ HeaderFields . StreamStartBytes ] ! . bytes ! ) ;
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 ! ) ;
kdbxFile . header . fields [ HeaderFields . EncryptionIV ] ! . bytes ) ;
return encrypted ;
}
@ -326,7 +230,7 @@ class KdbxBody extends KdbxNode {
if ( ctx . findBinaryByValue ( binary ) = = null ) {
ctx . addBinary ( binary ) ;
mergeContext . trackChange ( this ,
debug: ' adding new binary ${ binary . value ! . length } ' ) ;
debug: ' adding new binary ${ binary . value . length } ' ) ;
}
}
meta . merge ( other . meta ) ;
@ -334,7 +238,25 @@ class KdbxBody extends KdbxNode {
/ / remove deleted objects
for ( final incomingDelete in incomingDeleted . values ) {
final object = mergeContext . objectIndex ! [ incomingDelete . uuid ! ] ;
final object = mergeContext . objectIndex [ incomingDelete . uuid ] ;
if ( object = = null ) {
mergeContext . trackWarning (
' Incoming deleted object not found locally ${ incomingDelete . uuid } ' ) ;
continue ;
}
final parent = object . parent ;
if ( parent = = null ) {
mergeContext . trackWarning ( ' Unable to delete object $ object - '
' already deleted? ( ${ incomingDelete . uuid } ) ' ) ;
continue ;
}
if ( object is KdbxGroup ) {
parent . internalRemoveGroup ( object ) ;
} else if ( object is KdbxEntry ) {
parent . internalRemoveEntry ( object ) ;
} else {
throw StateError ( ' Invalid object type $ object ' ) ;
}
mergeContext . trackChange ( object , debug: ' was deleted. ' ) ;
}
@ -343,11 +265,11 @@ class KdbxBody extends KdbxNode {
_logger . info ( ' Finished merging: \n ${ mergeContext . debugChanges ( ) } ' ) ;
final incomingObjects = other . _createObjectIndex ( ) ;
_logger . info ( ' Merged: ${ mergeContext . merged } vs. '
' (local objects: ${ mergeContext . objectIndex ! . length } , '
' (local objects: ${ mergeContext . objectIndex . length } , '
' incoming objects: ${ incomingObjects . length } ) ' ) ;
/ / sanity checks
if ( mergeContext . merged . keys . length ! = mergeContext . objectIndex ! . length ) {
if ( mergeContext . merged . keys . length ! = mergeContext . objectIndex . length ) {
/ / TODO figure out what went wrong .
}
return mergeContext ;
@ -425,12 +347,28 @@ class MergeChange {
}
}
class MergeWarning {
MergeWarning ( this . debug ) ;
final String debug ;
@ override
String toString ( ) {
return debug ;
}
}
class MergeContext implements OverwriteContext {
MergeContext ( { this . objectIndex , this . deletedObjects } ) ;
final Map < KdbxUuid , KdbxObject > ? objectIndex ;
final Map < KdbxUuid ? , KdbxDeletedObject > ? deletedObjects ;
MergeContext ( { required this . objectIndex , required this . deletedObjects } ) ;
final Map < KdbxUuid , KdbxObject > objectIndex ;
final Map < KdbxUuid ? , KdbxDeletedObject > deletedObjects ;
final Map < KdbxUuid , KdbxObject > merged = { } ;
final List < MergeChange > changes = [ ] ;
final List < MergeWarning > warnings = [ ] ;
int totalChanges ( ) {
return deletedObjects . length + changes . length ;
}
void markAsMerged ( KdbxObject object ) {
if ( merged . containsKey ( object . uuid ) ) {
@ -449,6 +387,11 @@ class MergeContext implements OverwriteContext {
) ) ;
}
void trackWarning ( String warning ) {
_logger . warning ( warning , StackTrace . current ) ;
warnings . add ( MergeWarning ( warning ) ) ;
}
String debugChanges ( ) {
final group =
changes . groupBy ( ( element ) = > element . object , valueTransform: ( x ) = > x ) ;
@ -459,6 +402,17 @@ class MergeContext implements OverwriteContext {
] . join ( ' \n ' ) )
. join ( ' \n ' ) ;
}
String debugSummary ( ) {
return ' Changes: ${ changes . length } , '
' Deleted: ${ deletedObjects . length } , '
' Warnings: ${ warnings . isEmpty ? ' None ' : warnings . join ( ' , ' ) } ' ;
}
@ override
String toString ( ) {
return ' $ runtimeType { ${ debugSummary ( ) } } ' ;
}
}
class _KeysV4 {
@ -469,9 +423,13 @@ class _KeysV4 {
}
class KdbxFormat {
KdbxFormat ( [ this . argon2 ] ) : assert ( kdbxKeyCommonAssertConsistency ( ) ) ;
KdbxFormat ( [ Argon2 ? argon2 ] )
: assert ( kdbxKeyCommonAssertConsistency ( ) ) ,
argon2 = argon2 = = null | | ! argon2 . isImplemented
? const PointyCastleArgon2 ( )
: argon2 ;
final Argon2 ? argon2 ;
final Argon2 argon2 ;
static bool dartWebWorkaround = false ;
/ / / Creates a new , empty [ KdbxFile ] with default settings .
@ -482,7 +440,7 @@ class KdbxFormat {
String ? generator ,
KdbxHeader ? header ,
} ) {
header ? ? = argon2 = = null ? KdbxHeader . createV3 ( ) : KdbxHeader . createV4 ( ) ;
header ? ? = KdbxHeader . createV4 ( ) ;
final ctx = KdbxReadWriteContext ( binaries: [ ] , header: header ) ;
final meta = KdbxMeta . create (
databaseName: name ,
@ -516,10 +474,20 @@ class KdbxFormat {
}
/ / / Saves the given file .
Future < Uint8List > save ( KdbxFile file ) async {
Future < T > save < T > ( KdbxFile file , FileSaveCallback < T > saveBytes ) async {
_logger . finer ( ' Saving ${ file . body . rootGroup . uuid } '
' (locked: ${ file . saveLock . locked } ) ' ) ;
return file . saveLock . synchronized ( ( ) = > _saveSynchronized ( file ) ) ;
return file . saveLock . synchronized ( ( ) async {
final savedAt = TimeSequence . now ( ) ;
final bytes = await _saveSynchronized ( file ) ;
_logger . fine ( ' Saving bytes. ' ) ;
final ret = await saveBytes ( bytes ) ;
_logger . fine ( ' Saved bytes. ' ) ;
file . onSaved ( savedAt ) ;
return ret ;
} ) ;
}
Future < Uint8List > _saveSynchronized ( KdbxFile file ) async {
@ -537,7 +505,7 @@ class KdbxFormat {
throw UnsupportedError ( ' Unsupported version ${ header . version } ' ) ;
} else if ( file . header . version < KdbxVersion . V4 ) {
final streamKey =
file . header . fields [ HeaderFields . ProtectedStreamKey ] ! . bytes ! ;
file . header . fields [ HeaderFields . ProtectedStreamKey ] ! . bytes ;
final gen = ProtectedSaltGenerator ( streamKey ) ;
body . meta . headerHash . set ( headerHash . buffer ) ;
@ -553,7 +521,6 @@ class KdbxFormat {
} else {
throw UnsupportedError ( ' Unsupported version ${ header . version } ' ) ;
}
file . onSaved ( ) ;
return output . toBytes ( ) ;
}
@ -703,7 +670,7 @@ class KdbxFormat {
Uint8List transformContentV4ChaCha20 (
KdbxHeader header , Uint8List encrypted , Uint8List cipherKey ) {
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ! ;
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ;
final chaCha = ChaCha7539Engine ( )
. . init ( true , ParametersWithIV ( KeyParameter ( cipherKey ) , encryptionIv ) ) ;
return chaCha . process ( encrypted ) ;
@ -726,7 +693,7 @@ class KdbxFormat {
Future < _KeysV4 > _computeKeysV4 (
KdbxHeader header , Credentials credentials ) async {
final masterSeed = header . fields [ HeaderFields . MasterSeed ] ! . bytes ! ;
final masterSeed = header . fields [ HeaderFields . MasterSeed ] ! . bytes ;
final kdfParameters = header . readKdfParameters ;
if ( masterSeed . length ! = 32 ) {
throw const FormatException ( ' Master seed must be 32 bytes. ' ) ;
@ -734,7 +701,7 @@ class KdbxFormat {
final credentialHash = credentials . getHash ( ) ;
final key =
await KeyEncrypterKdf ( argon2 ! ) . encrypt ( credentialHash , kdfParameters ) ;
await KeyEncrypterKdf ( argon2 ) . encrypt ( credentialHash , kdfParameters ) ;
/ / final keyWithSeed = Uint8List ( 65 ) ;
/ / keyWithSeed . replaceRange ( 0 , masterSeed . length , masterSeed ) ;
@ -823,14 +790,16 @@ class KdbxFormat {
Uint8List _decryptContent (
KdbxHeader header , Uint8List masterKey , Uint8List encryptedPayload ) {
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ! ;
final decryptCipher = CBCBlockCipher ( AESFast Engine ( ) ) ;
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ;
final decryptCipher = CBCBlockCipher ( AESEngine ( ) ) ;
decryptCipher . init (
false , ParametersWithIV ( KeyParameter ( masterKey ) , encryptionIv ) ) ;
_logger . finer ( ' decrypting ${ encryptedPayload . length } with block size '
' ${ decryptCipher . blockSize } ' ) ;
final paddedDecrypted =
AesHelper . processBlocks ( decryptCipher , encryptedPayload ) ;
final streamStart = header . fields [ HeaderFields . StreamStartBytes ] ! . bytes ! ;
final streamStart = header . fields [ HeaderFields . StreamStartBytes ] ! . bytes ;
if ( paddedDecrypted . lengthInBytes < streamStart . lengthInBytes ) {
_logger . warning (
@ -852,9 +821,9 @@ class KdbxFormat {
Uint8List _decryptContentV4 (
KdbxHeader header , Uint8List cipherKey , Uint8List encryptedPayload ) {
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ! ;
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ;
final decryptCipher = CBCBlockCipher ( AESFast Engine ( ) ) ;
final decryptCipher = CBCBlockCipher ( AESEngine ( ) ) ;
decryptCipher . init (
false , ParametersWithIV ( KeyParameter ( cipherKey ) , encryptionIv ) ) ;
final paddedDecrypted =
@ -867,8 +836,8 @@ class KdbxFormat {
/ / / 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 ( AESFast Engine ( ) ) ;
final encryptionIv = header . fields [ HeaderFields . EncryptionIV ] ! . bytes ;
final encryptCypher = CBCBlockCipher ( AESEngine ( ) ) ;
encryptCypher . init (
true , ParametersWithIV ( KeyParameter ( cipherKey ) , encryptionIv ) ) ;
final paddedBytes = AesHelper . pad ( bytes , encryptCypher . blockSize ) ;
@ -879,7 +848,7 @@ class KdbxFormat {
KdbxHeader header , Credentials credentials ) async {
final rounds = header . v3KdfTransformRounds ;
final seed = header . fields [ HeaderFields . TransformSeed ] ! . bytes ;
final masterSeed = header . fields [ HeaderFields . MasterSeed ] ! . bytes ! ;
final masterSeed = header . fields [ HeaderFields . MasterSeed ] ! . bytes ;
_logger . finer (
' Rounds: $ rounds ( ${ ByteUtils . toHexList ( header . fields [ HeaderFields . TransformRounds ] ! . bytes ) } ) ' ) ;
final transformedKey = await KeyEncrypterKdf . encryptAesAsync (
@ -893,7 +862,7 @@ class KdbxFormat {
static Uint8List _encryptDataAes (
Uint8List masterKey , Uint8List payload , Uint8List encryptionIv ) {
final encryptCipher = CBCBlockCipher ( AESFast Engine ( ) ) ;
final encryptCipher = CBCBlockCipher ( AESEngine ( ) ) ;
encryptCipher . init (
true , ParametersWithIV ( KeyParameter ( masterKey ) , encryptionIv ) ) ;
return AesHelper . processBlocks (