diff --git a/lib/src/crypto/protected_salt_generator.dart b/lib/src/crypto/protected_salt_generator.dart index 265f8a7..f0219c5 100644 --- a/lib/src/crypto/protected_salt_generator.dart +++ b/lib/src/crypto/protected_salt_generator.dart @@ -11,15 +11,20 @@ class ProtectedSaltGenerator { return ProtectedSaltGenerator._(cipher); } - ProtectedSaltGenerator._(this.cipher); + ProtectedSaltGenerator._(this._cipher); static final SalsaNonce = Uint8List.fromList([0xE8, 0x30, 0x09, 0x4B, 0x97, 0x20, 0x5D, 0x2A]); - final StreamCipher cipher; + final StreamCipher _cipher; String decryptBase64(String protectedValue) { final bytes = base64.decode(protectedValue); - final result = cipher.process(bytes); + final result = _cipher.process(bytes); final decrypted = utf8.decode(result); return decrypted; } + + String encryptToBase64(String plainValue) { + final encrypted = _cipher.process(utf8.encode(plainValue) as Uint8List); + return base64.encode(encrypted); + } } diff --git a/lib/src/internal/byte_utils.dart b/lib/src/internal/byte_utils.dart index dbcfedd..dca690c 100644 --- a/lib/src/internal/byte_utils.dart +++ b/lib/src/internal/byte_utils.dart @@ -1,8 +1,14 @@ import 'dart:ffi'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; class ByteUtils { + static final _random = Random.secure(); + + static Uint8List randomBytes(int length) => + Uint8List.fromList(List.generate(length, (i) => _random.nextInt(1 << 8))); + static bool eq(List a, List b) { if (a.length != b.length) { return false; @@ -17,7 +23,8 @@ class ByteUtils { static String toHex(int val) => '0x${val.toRadixString(16)}'; - static String toHexList(List list) => list.map((val) => toHex(val)).join(' '); + static String toHexList(List list) => + list.map((val) => toHex(val)).join(' '); } class ReaderHelper { @@ -37,11 +44,14 @@ class ReaderHelper { ByteBuffer readBytes(int size) => _nextByteBuffer(size); + ByteBuffer readBytesUpTo(int maxSize) => + _nextByteBuffer(min(maxSize, data.lengthInBytes - pos)); + Uint8List readRemaining() => data.sublist(pos) as Uint8List; } class WriterHelper { - WriterHelper(this.output); + WriterHelper([BytesBuilder output]) : output = output ?? BytesBuilder(); final BytesBuilder output; @@ -55,8 +65,12 @@ class WriterHelper { // output.asUint32List().add(value); } + void writeUint64(int value) { + output.add(Uint64List.fromList([value]).buffer.asUint8List()); + } + void writeUint16(int value) { - output.add(Uint32List.fromList([value]).buffer.asUint32List()); + output.add(Uint16List.fromList([value]).buffer.asUint8List()); } void writeUint8(int value) { diff --git a/lib/src/internal/crypto_utils.dart b/lib/src/internal/crypto_utils.dart index 5e79bb1..8733d4a 100644 --- a/lib/src/internal/crypto_utils.dart +++ b/lib/src/internal/crypto_utils.dart @@ -67,6 +67,17 @@ class AesHelper { return Uint8List(len)..setRange(0, len, src); } + static Uint8List pad(Uint8List src, int blockSize) { + final pad = PKCS7Padding(); + pad.init(null); + + final padLength = blockSize - (src.length % blockSize); + final out = Uint8List(src.length + padLength)..setAll(0, src); + pad.addPadding(out, src.length); + + return out; + } + static Uint8List processBlocks(BlockCipher cipher, Uint8List inp) { final out = Uint8List(inp.lengthInBytes); diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index ba4e3a9..c923676 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -6,8 +6,6 @@ import 'package:kdbx/src/kdbx_group.dart'; import 'package:kdbx/src/kdbx_object.dart'; import 'package:xml/xml.dart'; -String _canonicalizeKey(String key) => key?.toLowerCase(); - /// Represents a case insensitive (but case preserving) key. class KdbxKey { KdbxKey(this.key) : _canonicalKey = key.toLowerCase(); @@ -29,7 +27,7 @@ class KdbxEntry extends KdbxObject { } KdbxEntry.read(this.parent, XmlElement node) : super.read(node) { - strings.addEntries(node.findElements('String').map((el) { + _strings.addEntries(node.findElements('String').map((el) { final key = KdbxKey(el.findElements('Key').single.text); final valueNode = el.findElements('Value').single; if (valueNode.getAttribute('Protected')?.toLowerCase() == 'true') { @@ -40,11 +38,39 @@ class KdbxEntry extends KdbxObject { })); } + XmlElement toXml() { + final el = node.copy() as XmlElement; + el.children.removeWhere((e) => e is XmlElement && e.name.local == 'String'); + el.children.addAll(strings.entries.map((stringEntry) { + final value = XmlElement(XmlName('Value')); + if (stringEntry.value is ProtectedValue) { + value.attributes.add(XmlAttribute(XmlName('Protected'), 'true')); + KdbxFile.setProtectedValueForNode( + value, stringEntry.value as ProtectedValue); + } else { + value.children.add(XmlText(stringEntry.value.getText())); + } + return XmlElement(XmlName('String')) + ..children.addAll([ + XmlElement(XmlName('Key')) + ..children.add(XmlText(stringEntry.key.key)), + value, + ]); + })); + return el; + } + KdbxGroup parent; - Map strings = {}; + final Map _strings = {}; + + Map get strings => UnmodifiableMapView(_strings); + + void setString(KdbxKey key, StringValue value) { + _strings[key] = value; + } String _plainValue(KdbxKey key) { - final value = strings[key]; + final value = _strings[key]; if (value is PlainValue) { return value.getText(); } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 55415f6..8cd98cc 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -43,6 +43,10 @@ class KdbxFile { return protectedValues[node]; } + static void setProtectedValueForNode(xml.XmlElement node, ProtectedValue value) { + protectedValues[node] = value; + } + final Credentials credentials; final KdbxHeader header; final KdbxBody body; @@ -55,6 +59,7 @@ class KdbxFile { body.write(writer, this); return output.toBytes(); } + } class KdbxBody extends KdbxNode { @@ -64,7 +69,9 @@ class KdbxBody extends KdbxNode { node.children.add(rootNode); rootNode.children.add(rootGroup.node); } - KdbxBody.read(xml.XmlElement node, this.meta, this.rootGroup) : super.read(node); + + KdbxBody.read(xml.XmlElement node, this.meta, this.rootGroup) + : super.read(node); // final xml.XmlDocument xmlDocument; final KdbxMeta meta; @@ -72,18 +79,63 @@ class KdbxBody extends KdbxNode { void write(WriterHelper writer, KdbxFile kdbxFile) { assert(kdbxFile.header.versionMajor == 3); - _writeV3(writer, kdbxFile); + final streamKey = kdbxFile + .header.fields[HeaderFields.ProtectedStreamKey].bytes + .asUint8List(); + final gen = ProtectedSaltGenerator(streamKey); + + _writeV3(writer, kdbxFile, gen); } - void _writeV3(WriterHelper writer, KdbxFile kdbxFile) { - meta.headerHash.set((crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List).buffer); + void _writeV3(WriterHelper writer, KdbxFile kdbxFile, + ProtectedSaltGenerator saltGenerator) { + meta.headerHash.set( + (crypto.sha256.convert(writer.output.toBytes()).bytes as Uint8List) + .buffer); + final xml = toXml(saltGenerator); + final xmlBytes = utf8.encode(xml.toXmlString()); + final Uint8List compressedBytes = (kdbxFile.header.compression == Compression.gzip ? + GZipCodec().encode(xmlBytes) : xmlBytes) as Uint8List; + + final byteWriter = WriterHelper(); + 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); } - xml.XmlDocument toXml() { - final doc = xml.XmlDocument(); - doc.children.add(xml.XmlProcessing('xml', 'version="1.0" encoding="utf-8" standalone="yes"')); - doc.children.add(node.copy()); - return doc; + xml.XmlDocument toXml(ProtectedSaltGenerator saltGenerator) { + final rootGroupNode = rootGroup.toXml(); + // update protected values... + for (final el in rootGroupNode + .findAllElements('Value') + .where((el) => el.getAttribute('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 { + _logger.warning('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; } } @@ -91,15 +143,19 @@ class KdbxMeta extends KdbxNode { KdbxMeta.create({@required String databaseName}) : super.create('Meta') { this.databaseName.set(databaseName); } + KdbxMeta.read(xml.XmlElement node) : super.read(node); StringNode get databaseName => StringNode(this, 'DatabaseName'); + Base64Node get headerHash => Base64Node(this, 'HeaderHash'); + xml.XmlElement toXml() { + return node; + } } class KdbxFormat { - static KdbxFile create(Credentials credentials, String name) { final header = KdbxHeader.create(); final meta = KdbxMeta.create(databaseName: name); @@ -108,7 +164,6 @@ class KdbxFormat { return KdbxFile(credentials, header, body); } - static KdbxFile read(Uint8List input, Credentials credentials) { final reader = ReaderHelper(input); final header = KdbxHeader.read(reader); @@ -167,7 +222,8 @@ class KdbxFormat { final decryptCipher = CBCBlockCipher(AESFastEngine()); decryptCipher.init(false, ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List())); - final decrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); + final paddedDecrypted = AesHelper.processBlocks(decryptCipher, encryptedPayload); + final decrypted = paddedDecrypted;//AesHelper.unpad(paddedDecrypted); final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes; @@ -205,4 +261,12 @@ class KdbxFormat { .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)); + + } } diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 362dec2..28890b2 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -23,6 +23,15 @@ class KdbxGroup extends KdbxObject { .map((el) => KdbxEntry.read(this, el)) .forEach(_entries.add); } + + XmlElement toXml() { + final el = node.copy() as XmlElement; + XmlUtils.removeChildrenByName(el, 'Group'); + XmlUtils.removeChildrenByName(el, 'Entry'); + el.children.addAll(groups.map((g) => g.toXml())); + el.children.addAll(_entries.map((e) => e.toXml())); + return el; + } /// Returns all groups plus this group itself. List getAllGroups() => groups diff --git a/lib/src/kdbx_header.dart b/lib/src/kdbx_header.dart index 2d21a9f..d492f84 100644 --- a/lib/src/kdbx_header.dart +++ b/lib/src/kdbx_header.dart @@ -70,8 +70,10 @@ class KdbxHeader { fields: _defaultFieldValues(), ); - static ByteBuffer _intAsUintBytes(int val) => - Uint8List.fromList([val]).buffer; + static ByteBuffer _intAsUint32Bytes(int val) => + (WriterHelper()..writeUint32(val)).output.toBytes().buffer; + static ByteBuffer _intAsUint64Bytes(int val) => + (WriterHelper()..writeUint64(val)).output.toBytes().buffer; static List _requiredFields(int majorVersion) { if (majorVersion < 3) { @@ -113,16 +115,18 @@ class KdbxHeader { void generateSalts() { // TODO make sure default algorithm is "secure" engouh. Or whether we should - // use [Random.secure]? - final random = SecureRandom(); - _setHeaderField(HeaderFields.MasterSeed, random.nextBytes(32).buffer); + // use like [SecureRandom] from PointyCastle? + _setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(32).buffer); if (versionMajor < 4) { - _setHeaderField(HeaderFields.TransformSeed, random.nextBytes(32).buffer); - _setHeaderField(HeaderFields.StreamStartBytes, random.nextBytes(32).buffer); - _setHeaderField(HeaderFields.ProtectedStreamKey, random.nextBytes(32).buffer); - _setHeaderField(HeaderFields.EncryptionIV, random.nextBytes(16).buffer); + _setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32).buffer); + _setHeaderField( + HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32).buffer); + _setHeaderField( + HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32).buffer); + _setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16).buffer); } else { - throw KdbxUnsupportedException('We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)'); + throw KdbxUnsupportedException( + 'We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)'); } } @@ -138,6 +142,7 @@ class KdbxHeader { in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) { _writeField(writer, field); } + fields[HeaderFields.EndOfHeader] = HeaderField(HeaderFields.EndOfHeader, Uint8List(0).buffer); _writeField(writer, HeaderFields.EndOfHeader); } @@ -146,6 +151,8 @@ class KdbxHeader { if (value == null) { return; } + _logger.finer('Writing header $field (${value.bytes.lengthInBytes})'); + writer.writeUint8(field.index); _writeFieldSize(writer, value.bytes.lengthInBytes); writer.writeBytes(value.bytes.asUint8List()); } @@ -162,11 +169,11 @@ class KdbxHeader { Map.fromEntries([ HeaderField(HeaderFields.CipherID, CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes()), - HeaderField(HeaderFields.CompressionFlags, _intAsUintBytes(1)), - HeaderField(HeaderFields.TransformRounds, _intAsUintBytes(6000)), + HeaderField(HeaderFields.CompressionFlags, _intAsUint32Bytes(1)), + HeaderField(HeaderFields.TransformRounds, _intAsUint64Bytes(6000)), HeaderField( HeaderFields.InnerRandomStreamID, - _intAsUintBytes(ProtectedValueEncryption.values + _intAsUint32Bytes(ProtectedValueEncryption.values .indexOf(ProtectedValueEncryption.salsa20))), ].map((f) => MapEntry(f.field, f))); @@ -251,13 +258,16 @@ class KdbxUnsupportedException implements KdbxException { } class HashedBlockReader { + static const BLOCK_SIZE = 1024 * 1024; + static const HASH_SIZE = 32; + static Uint8List readBlocks(ReaderHelper reader) => Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList()); static Iterable readNextBlock(ReaderHelper reader) sync* { while (true) { final blockIndex = reader.readUint32(); - final blockHash = reader.readBytes(32); + final blockHash = reader.readBytes(HASH_SIZE); final blockSize = reader.readUint32(); if (blockSize > 0) { final blockData = reader.readBytes(blockSize).asUint8List(); @@ -271,4 +281,27 @@ class HashedBlockReader { } } } + +// static Uint8List writeBlocks(WriterHelper writer) => + + static void writeBlocks(ReaderHelper reader, WriterHelper writer) { + while (true) { + int blockIndex = 0; + final block = reader.readBytesUpTo(BLOCK_SIZE); + if (block.lengthInBytes == 0) { + // written all data, write a last empty block. + writer.writeUint32(blockIndex++); + writer.writeBytes(Uint8List.fromList(List.generate(HASH_SIZE, (i) => 0))); + writer.writeUint32(0); + return; + } + final blockSize = block.lengthInBytes; + final blockHash = crypto.sha256.convert(block.asUint8List()); + assert(blockHash.bytes.length == HASH_SIZE); + writer.writeUint32(blockIndex++); + writer.writeBytes(blockHash.bytes as Uint8List); + writer.writeUint32(blockSize); + writer.writeBytes(block.asUint8List()); + } + } } diff --git a/lib/src/kdbx_object.dart b/lib/src/kdbx_object.dart index 66965ba..77b1c0b 100644 --- a/lib/src/kdbx_object.dart +++ b/lib/src/kdbx_object.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:kdbx/src/kdbx_xml.dart'; import 'package:uuid/uuid.dart'; +import 'package:uuid/uuid_util.dart'; import 'package:xml/xml.dart'; class KdbxTimes { @@ -47,8 +48,12 @@ abstract class KdbxObject extends KdbxNode { class KdbxUuid { const KdbxUuid(this.uuid); + KdbxUuid.random() : this(uuidGenerator.v4()); + + static final Uuid uuidGenerator = Uuid(options: { + 'grng': UuidUtil.cryptoRNG + }); - KdbxUuid.random() : this(Uuid().v4()); /// base64 representation of uuid. final String uuid; diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index ccc24ca..1455d0e 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -115,3 +115,9 @@ class BooleanNode extends KdbxSubTextNode { String encode(bool value) => value ? 'true' : 'false'; } + +class XmlUtils { + static void removeChildrenByName(XmlNode node, String name) { + node.children.removeWhere((node) => node is XmlElement && node.name.local == name); + } +} \ No newline at end of file diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index a448707..fda9415 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -2,13 +2,23 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:kdbx/kdbx.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/kdbx_format.dart'; -import 'package:kdbx/src/kdbx_header.dart'; import 'package:logging/logging.dart'; import 'package:logging_appenders/logging_appenders.dart'; import 'package:test/test.dart'; +class FakeProtectedSaltGenerator implements ProtectedSaltGenerator { + @override + String decryptBase64(String protectedValue) => 'fake'; + + @override + String encryptToBase64(String plainValue) => 'fake'; + +} + void main() { Logger.root.level = Level.ALL; PrintAppender().attachToLogger(Logger.root); @@ -28,13 +38,35 @@ void main() { expect(kdbx.body.rootGroup, isNotNull); expect(kdbx.body.rootGroup.name.get(), 'CreateTest'); expect(kdbx.body.meta.databaseName.get(), 'CreateTest'); - print(kdbx.body.toXml().toXmlString(pretty: true)); + print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); }); test('Create Entry', () { final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); final rootGroup = kdbx.body.rootGroup; - rootGroup.addEntry(KdbxEntry.create(rootGroup)); - print(kdbx.body.toXml().toXmlString(pretty: true)); + final entry = KdbxEntry.create(rootGroup); + rootGroup.addEntry(entry); + entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); + print(kdbx.body.toXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); + }); + }); + + group('Integration', () { + test('Simple save and load', () { + final credentials = Credentials(ProtectedValue.fromString('FooBar')); + Uint8List saved = (() { + final kdbx = KdbxFormat.create(credentials, 'CreateTest'); + final rootGroup = kdbx.body.rootGroup; + final entry = KdbxEntry.create(rootGroup); + rootGroup.addEntry(entry); + entry.setString( + KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); + return kdbx.save(); + })(); + +// print(ByteUtils.toHexList(saved)); + + final kdbx = KdbxFormat.read(saved, credentials); + expect(kdbx.body.rootGroup.entries.first.strings[KdbxKey('Password')].getText(), 'LoremIpsum'); }); }); }