diff --git a/lib/kdbx.dart b/lib/kdbx.dart index a8b690d..ac807b0 100644 --- a/lib/kdbx.dart +++ b/lib/kdbx.dart @@ -1,6 +1,7 @@ /// dart library for reading keepass file format (kdbx). library kdbx; +export 'src/crypto/key_encrypter_kdf.dart' show KeyEncrypterKdf, KdfType; export 'src/crypto/protected_value.dart' show ProtectedValue, StringValue, PlainValue; export 'src/kdbx_binary.dart' show KdbxBinary; @@ -16,6 +17,7 @@ export 'src/kdbx_header.dart' KdbxException, KdbxInvalidKeyException, KdbxCorruptedFileException, - KdbxUnsupportedException; + KdbxUnsupportedException, + KdbxVersion; export 'src/kdbx_meta.dart'; export 'src/kdbx_object.dart'; diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart index 0e25c4d..11793f0 100644 --- a/lib/src/internal/byte_utils.dart +++ b/lib/src/internal/byte_utils.dart @@ -35,6 +35,10 @@ class ByteUtils { list?.map((val) => toHex(val))?.join(' ') ?? '(null)'; } +extension Uint8ListExt on Uint8List { + String encodeBase64() => base64.encode(this); +} + class ReaderHelper { factory ReaderHelper(Uint8List byteData) => KdbxFormat.dartWebWorkaround ? ReaderHelperDartWeb(byteData) diff --git a/lib/src/internal/consts.dart b/lib/src/internal/consts.dart index 7d6128b..2b7fe24 100644 --- a/lib/src/internal/consts.dart +++ b/lib/src/internal/consts.dart @@ -1,7 +1,12 @@ +import 'dart:typed_data'; + import 'package:kdbx/src/kdbx_object.dart'; enum Cipher { + /// the only cipher supported in kdbx <= 3 aes, + + /// Support since kdbx 4. chaCha20, } @@ -10,4 +15,9 @@ class CryptoConsts { Cipher.aes: KdbxUuid('McHy5r9xQ1C+WAUhavxa/w=='), Cipher.chaCha20: KdbxUuid('1gOKK4tvTLWlJDOaMdu1mg=='), }; + static final cipherByUuid = + CIPHER_IDS.map((key, value) => MapEntry(value, key)); + + static Cipher cipherFromBytes(Uint8List bytes) => + cipherByUuid[KdbxUuid.fromBytes(bytes)]; } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 9c2fbda..a542392 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -258,19 +258,19 @@ class KdbxBody extends KdbxNode { 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) { + final cipher = header.cipher; + if (cipher == Cipher.aes) { _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) { + } else if (cipher == Cipher.chaCha20) { _logger.fine('We need chacha20'); return kdbxFile.kdbxFormat .transformContentV4ChaCha20(header, compressedBytes, cipherKey); } else { - throw UnsupportedError('Unsupported cipherId $cipherId'); + throw UnsupportedError('Unsupported cipherId $cipher'); } } @@ -308,7 +308,7 @@ class KdbxBody extends KdbxNode { // 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; + final node = builder.buildDocument(); return node; } @@ -533,17 +533,17 @@ class KdbxFormat { 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) { + final cipher = header.cipher; + if (cipher == Cipher.aes) { _logger.fine('We need AES'); final result = _decryptContentV4(header, cipherKey, encrypted); return result; - } else if (cipherId == CryptoConsts.CIPHER_IDS[Cipher.chaCha20].uuid) { + } else if (cipher == Cipher.chaCha20) { _logger.fine('We need chacha20'); // throw UnsupportedError('chacha20 not yet supported $cipherId'); return transformContentV4ChaCha20(header, encrypted, cipherKey); } else { - throw UnsupportedError('Unsupported cipherId $cipherId'); + throw UnsupportedError('Unsupported cipherId $cipher'); } } diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index 848447c..01e652c 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' as crypto; @@ -49,6 +48,8 @@ enum ProtectedValueEncryption { plainText, arc4variant, salsa20, chaCha20 } enum HeaderFields { EndOfHeader, Comment, + + /// the cipher to use as defined by [Cipher]. in kdbx 3 this is always aes. CipherID, CompressionFlags, MasterSeed, @@ -262,9 +263,8 @@ class KdbxHeader { 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; + final cipher = this.cipher; + final ivLength = cipher == Cipher.chaCha20 ? 12 : 16; _setHeaderField( HeaderFields.EncryptionIV, ByteUtils.randomBytes(ivLength)); } else { @@ -471,6 +471,37 @@ class KdbxHeader { /// end position of the header, if we have been reading from a stream. final int endPos; + Cipher get cipher { + if (version < KdbxVersion.V4) { + assert( + CryptoConsts.cipherFromBytes(fields[HeaderFields.CipherID].bytes) == + Cipher.aes); + return Cipher.aes; + } + try { + return CryptoConsts.cipherFromBytes(fields[HeaderFields.CipherID].bytes); + } catch (e, stackTrace) { + _logger.warning( + 'Unable to find cipher. ' + '${fields[HeaderFields.CipherID]?.bytes?.encodeBase64()}', + e, + stackTrace); + throw KdbxCorruptedFileException( + 'Invalid cipher. ' + '${fields[HeaderFields.CipherID]?.bytes?.encodeBase64()}', + ); + } + } + + set cipher(Cipher cipher) { + checkArgument(version >= KdbxVersion.V4 || cipher == Cipher.aes, + message: 'Kdbx 3 only supports aes, tried to set it to $cipher'); + _setHeaderField( + HeaderFields.CipherID, + CryptoConsts.CIPHER_IDS[cipher].toBytes(), + ); + } + Compression get compression { final id = ReaderHelper.singleUint32(fields[HeaderFields.CompressionFlags].bytes); diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index f77d319..a50d08d 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -175,6 +175,8 @@ class KdbxUuid { KdbxUuid.random() : this(base64.encode(uuidGenerator.parse(uuidGenerator.v4()))); + KdbxUuid.fromBytes(Uint8List bytes) : this(base64.encode(bytes)); + /// https://tools.ietf.org/html/rfc4122.html#section-4.1.7 /// > The nil UUID is special form of UUID that is specified to have all /// 128 bits set to zero.