Browse Source

always use little endian, fixed packet index for payload during serialization.

remove-cryptography-dependency
Herbert Poul 5 years ago
parent
commit
7eee2d672f
  1. 6
      CHANGELOG.md
  2. 197
      example/pubspec.lock
  3. 14
      example/pubspec.yaml
  4. 76
      lib/src/internal/byte_utils.dart
  5. 39
      lib/src/kdbx_format.dart
  6. 67
      lib/src/kdbx_header.dart
  7. 5
      lib/src/kdbx_object.dart
  8. 9
      pubspec.yaml
  9. 8
      test/internal/byte_utils_test.dart
  10. 7
      test/pytest/Dockerfile
  11. 7
      test/pytest/docker-compose.yml
  12. 6
      test/pytest/kdbx3_decrypt.py

6
CHANGELOG.md

@ -1,3 +1,9 @@
## 0.2.0
- Fixed writing of packet index for payload.
- Fixed big endian vs. little endian encoding.
- Compatibility fixes with other kdbx apps.
## 0.1.0
- Support for reading and writing kdbx 2.x files

197
example/pubspec.lock

@ -0,0 +1,197 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.2"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.11"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
cookie_jar:
dependency: transitive
description:
name: cookie_jar
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
dio:
dependency: transitive
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.16"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.0"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
kdbx:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.1.0"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+2"
logging_appenders:
dependency: transitive
description:
name: logging_appenders
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.4"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
pointycastle:
dependency: transitive
description:
name: pointycastle
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
prompts:
dependency: transitive
description:
name: prompts
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
rxdart:
dependency: transitive
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.22.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
sdks:
dart: ">=2.4.0 <3.0.0"

14
example/pubspec.yaml

@ -0,0 +1,14 @@
name: kdbx_example
description: Demonstrates how to use the kdbx plugin.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.2.2 <3.0.0"
dependencies:
flutter:
sdk: flutter
kdbx:
path: ../

76
lib/src/internal/byte_utils.dart

@ -23,30 +23,65 @@ class ByteUtils {
static String toHex(int val) => '0x${val.toRadixString(16)}';
static String toHexList(List<int> list) =>
list.map((val) => toHex(val)).join(' ');
list?.map((val) => toHex(val))?.join(' ') ?? '(null)';
}
class ReaderHelper {
ReaderHelper(this.data);
ReaderHelper(this.byteData) : lengthInBytes = byteData.lengthInBytes;
final Uint8List data;
final Uint8List byteData;
int pos = 0;
final int lengthInBytes;
// ByteData _nextByteBuffer(int byteCount) {
// final ret = ByteData.view(data, pos, pos += byteCount);
// pos += byteCount;
// return ret;
// }
// ByteData _nextByteBuffer(int byteCount) =>
// ByteData.view(data, pos, (pos += byteCount) - pos);
// ByteData _nextByteBuffer(int byteCount) {
// try {
// return ByteData.view(data, pos, byteCount);
// } finally {
// pos += byteCount;
// }
// }
ByteData _nextByteBuffer(int byteCount) => _advanceByteCount(
byteCount,
() => ByteData.view(
byteData.buffer, pos + byteData.offsetInBytes, byteCount));
Uint8List _nextBytes(int byteCount) => _advanceByteCount(
byteCount,
() => Uint8List.view(
byteData.buffer, pos + byteData.offsetInBytes, byteCount));
T _advanceByteCount<T>(int byteCount, T Function() func) {
try {
return func();
} finally {
pos += byteCount;
}
}
ByteBuffer _nextByteBuffer(int byteCount) =>
(data.sublist(pos, pos += byteCount) as Uint8List).buffer;
int readUint32() => _nextByteBuffer(4).asUint32List().first;
int readUint16() => _nextByteBuffer(2).asUint16List().first;
int readUint8() => _nextByteBuffer(1).getUint8(0);
int readUint16() => _nextByteBuffer(2).getUint16(0, Endian.little);
int readUint32() => _nextByteBuffer(4).getUint32(0, Endian.little);
int readUint64() => _nextByteBuffer(8).getUint64(0, Endian.little);
int readUint8() => data[pos++];
Uint8List readBytes(int size) => _nextBytes(size);
ByteBuffer readBytes(int size) => _nextByteBuffer(size);
Uint8List readBytesUpTo(int maxSize) =>
_nextBytes(min(maxSize, lengthInBytes - pos));
ByteBuffer readBytesUpTo(int maxSize) =>
_nextByteBuffer(min(maxSize, data.lengthInBytes - pos));
Uint8List readRemaining() => _nextBytes(lengthInBytes - pos);
Uint8List readRemaining() => data.sublist(pos) as Uint8List;
static int singleUint32(Uint8List bytes) => ReaderHelper(bytes).readUint32();
static int singleUint64(Uint8List bytes) => ReaderHelper(bytes).readUint64();
}
class WriterHelper {
@ -54,25 +89,32 @@ class WriterHelper {
final BytesBuilder output;
void _write(ByteData byteData) => output.add(byteData.buffer.asUint8List());
void writeBytes(Uint8List bytes) {
output.add(bytes);
// output.asUint8List().addAll(bytes);
}
void writeUint32(int value) {
output.add(Uint32List.fromList([value]).buffer.asUint8List());
_write(ByteData(4)..setUint32(0, value, Endian.little));
// output.asUint32List().add(value);
}
void writeUint64(int value) {
output.add(Uint64List.fromList([value]).buffer.asUint8List());
_write(ByteData(8)..setUint64(0, value, Endian.little));
}
void writeUint16(int value) {
output.add(Uint16List.fromList([value]).buffer.asUint8List());
_write(ByteData(2)..setUint16(0, value, Endian.little));
}
void writeUint8(int value) {
output.addByte(value);
}
static Uint8List singleUint32Bytes(int val) =>
(WriterHelper()..writeUint32(val)).output.toBytes();
static Uint8List singleUint64Bytes(int val) =>
(WriterHelper()..writeUint64(val)).output.toBytes();
}

39
lib/src/kdbx_format.dart

@ -89,8 +89,7 @@ class KdbxFile {
header.generateSalts();
header.write(writer);
final streamKey =
header.fields[HeaderFields.ProtectedStreamKey].bytes.asUint8List();
final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey);
body.meta.headerHash.set(
@ -165,16 +164,15 @@ class KdbxBody extends KdbxNode {
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);
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());
kdbxFile.header.fields[HeaderFields.EncryptionIV].bytes);
return encrypted;
}
@ -276,8 +274,7 @@ class KdbxFormat {
throw KdbxUnsupportedException(
'Inner encryption: $protectedValueEncryption');
}
final streamKey =
header.fields[HeaderFields.ProtectedStreamKey].bytes.asUint8List();
final streamKey = header.fields[HeaderFields.ProtectedStreamKey].bytes;
final gen = ProtectedSaltGenerator(streamKey);
final document = xml.parse(xmlString);
@ -301,26 +298,25 @@ class KdbxFormat {
KdbxHeader header, Uint8List masterKey, Uint8List encryptedPayload) {
final encryptionIv = header.fields[HeaderFields.EncryptionIV].bytes;
final decryptCipher = CBCBlockCipher(AESFastEngine());
decryptCipher.init(false,
ParametersWithIV(KeyParameter(masterKey), encryptionIv.asUint8List()));
decryptCipher.init(
false, ParametersWithIV(KeyParameter(masterKey), encryptionIv));
final paddedDecrypted =
AesHelper.processBlocks(decryptCipher, encryptedPayload);
final streamStart = header.fields[HeaderFields.StreamStartBytes].bytes;
if (paddedDecrypted.lengthInBytes < streamStart.lengthInBytes) {
_logger.warning('decrypted content was shorter than expected stream start block.');
_logger.warning(
'decrypted content was shorter than expected stream start block.');
throw KdbxInvalidKeyException();
}
_logger.finest(
'streamStart: ${ByteUtils.toHexList(streamStart.asUint8List())}');
_logger.finest('streamStart: ${ByteUtils.toHexList(streamStart)}');
_logger.finest(
'actual : ${ByteUtils.toHexList(paddedDecrypted.sublist(0, streamStart.lengthInBytes))}');
if (!ByteUtils.eq(streamStart.asUint8List(),
paddedDecrypted.sublist(0, streamStart.lengthInBytes))) {
if (!ByteUtils.eq(
streamStart, paddedDecrypted.sublist(0, streamStart.lengthInBytes))) {
throw KdbxInvalidKeyException();
}
@ -333,11 +329,12 @@ class KdbxFormat {
static Uint8List _generateMasterKeyV3(
KdbxHeader header, Credentials credentials) {
final rounds =
header.fields[HeaderFields.TransformRounds].bytes.asUint64List().first;
final seed = header.fields[HeaderFields.TransformSeed].bytes.asUint8List();
final rounds = ReaderHelper.singleUint64(
header.fields[HeaderFields.TransformRounds].bytes);
final seed = header.fields[HeaderFields.TransformSeed].bytes;
final masterSeed = header.fields[HeaderFields.MasterSeed].bytes;
_logger.finer('Rounds: $rounds');
_logger.finer(
'Rounds: $rounds (${ByteUtils.toHexList(header.fields[HeaderFields.TransformRounds].bytes)})');
final cipher = ECBBlockCipher(AESFastEngine())
..init(true, KeyParameter(seed));
@ -348,7 +345,7 @@ class KdbxFormat {
}
transformedKey = crypto.sha256.convert(transformedKey).bytes as Uint8List;
final masterKey = crypto.sha256
.convert(Uint8List.fromList(masterSeed.asUint8List() + transformedKey))
.convert(Uint8List.fromList(masterSeed + transformedKey))
.bytes as Uint8List;
return masterKey;
}

67
lib/src/kdbx_header.dart

@ -45,7 +45,7 @@ class HeaderField {
HeaderField(this.field, this.bytes);
final HeaderFields field;
final ByteBuffer bytes;
final Uint8List bytes;
String get name => field.toString();
}
@ -68,11 +68,6 @@ class KdbxHeader {
fields: _defaultFieldValues(),
);
static ByteBuffer _intAsUint32Bytes(int val) =>
(WriterHelper()..writeUint32(val)).output.toBytes().buffer;
static ByteBuffer _intAsUint64Bytes(int val) =>
(WriterHelper()..writeUint64(val)).output.toBytes().buffer;
static List<HeaderFields> _requiredFields(int majorVersion) {
if (majorVersion < 3) {
throw KdbxUnsupportedException('Unsupported version: $majorVersion');
@ -107,21 +102,20 @@ class KdbxHeader {
}
}
void _setHeaderField(HeaderFields field, ByteBuffer bytes) {
void _setHeaderField(HeaderFields field, Uint8List bytes) {
fields[field] = HeaderField(field, bytes);
}
void generateSalts() {
// TODO make sure default algorithm is "secure" engouh. Or whether we should
// use like [SecureRandom] from PointyCastle?
_setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(32).buffer);
_setHeaderField(HeaderFields.MasterSeed, ByteUtils.randomBytes(32));
if (versionMajor < 4) {
_setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32).buffer);
_setHeaderField(
HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32).buffer);
_setHeaderField(HeaderFields.TransformSeed, ByteUtils.randomBytes(32));
_setHeaderField(HeaderFields.StreamStartBytes, ByteUtils.randomBytes(32));
_setHeaderField(
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32).buffer);
_setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16).buffer);
HeaderFields.ProtectedStreamKey, ByteUtils.randomBytes(32));
_setHeaderField(HeaderFields.EncryptionIV, ByteUtils.randomBytes(16));
} else {
throw KdbxUnsupportedException(
'We do not support Kdbx 4.x right now. ($versionMajor.$versionMinor)');
@ -140,7 +134,8 @@ class KdbxHeader {
in HeaderFields.values.where((f) => f != HeaderFields.EndOfHeader)) {
_writeField(writer, field);
}
fields[HeaderFields.EndOfHeader] = HeaderField(HeaderFields.EndOfHeader, Uint8List(0).buffer);
fields[HeaderFields.EndOfHeader] =
HeaderField(HeaderFields.EndOfHeader, Uint8List(0));
_writeField(writer, HeaderFields.EndOfHeader);
}
@ -152,7 +147,7 @@ class KdbxHeader {
_logger.finer('Writing header $field (${value.bytes.lengthInBytes})');
writer.writeUint8(field.index);
_writeFieldSize(writer, value.bytes.lengthInBytes);
writer.writeBytes(value.bytes.asUint8List());
writer.writeBytes(value.bytes);
}
void _writeFieldSize(WriterHelper writer, int size) {
@ -167,11 +162,13 @@ class KdbxHeader {
Map.fromEntries([
HeaderField(HeaderFields.CipherID,
CryptoConsts.CIPHER_IDS[Cipher.aes].toBytes()),
HeaderField(HeaderFields.CompressionFlags, _intAsUint32Bytes(1)),
HeaderField(HeaderFields.TransformRounds, _intAsUint64Bytes(6000)),
HeaderField(
HeaderFields.CompressionFlags, WriterHelper.singleUint32Bytes(1)),
HeaderField(
HeaderFields.TransformRounds, WriterHelper.singleUint64Bytes(6000)),
HeaderField(
HeaderFields.InnerRandomStreamID,
_intAsUint32Bytes(ProtectedValueEncryption.values
WriterHelper.singleUint32Bytes(ProtectedValueEncryption.values
.indexOf(ProtectedValueEncryption.salsa20))),
].map((f) => MapEntry(f.field, f)));
@ -207,8 +204,9 @@ class KdbxHeader {
final headerId = reader.readUint8();
final int bodySize =
versionMajor >= 4 ? reader.readUint32() : reader.readUint16();
_logger.finer('Read header ${HeaderFields.values[headerId]}');
final bodyBytes = bodySize > 0 ? reader.readBytes(bodySize) : null;
_logger.finer(
'Read header ${HeaderFields.values[headerId]}: ${ByteUtils.toHexList(bodyBytes)}');
if (headerId > 0) {
yield HeaderField(HeaderFields.values[headerId], bodyBytes);
} else {
@ -224,7 +222,8 @@ class KdbxHeader {
final Map<HeaderFields, HeaderField> fields;
Compression get compression {
switch (fields[HeaderFields.CompressionFlags].bytes.asUint32List().single) {
switch (ReaderHelper.singleUint32(
fields[HeaderFields.CompressionFlags].bytes)) {
case 0:
return Compression.none;
case 1:
@ -235,8 +234,8 @@ class KdbxHeader {
}
ProtectedValueEncryption get innerRandomStreamEncryption =>
ProtectedValueEncryption.values[
fields[HeaderFields.InnerRandomStreamID].bytes.asUint32List().single];
ProtectedValueEncryption.values[ReaderHelper.singleUint32(
fields[HeaderFields.InnerRandomStreamID].bytes)];
}
class KdbxException implements Exception {}
@ -263,15 +262,17 @@ class HashedBlockReader {
Uint8List.fromList(readNextBlock(reader).expand((x) => x).toList());
static Iterable<Uint8List> readNextBlock(ReaderHelper reader) sync* {
int expectedBlockIndex = 0;
while (true) {
// ignore: unused_local_variable
final blockIndex = reader.readUint32();
assert(blockIndex == expectedBlockIndex++);
final blockHash = reader.readBytes(HASH_SIZE);
final blockSize = reader.readUint32();
if (blockSize > 0) {
final blockData = reader.readBytes(blockSize).asUint8List();
if (!ByteUtils.eq(crypto.sha256.convert(blockData).bytes as Uint8List,
blockHash.asUint8List())) {
final blockData = reader.readBytes(blockSize);
if (!ByteUtils.eq(
crypto.sha256.convert(blockData).bytes as Uint8List, blockHash)) {
throw KdbxCorruptedFileException();
}
yield blockData;
@ -284,23 +285,23 @@ class HashedBlockReader {
// static Uint8List writeBlocks(WriterHelper writer) =>
static void writeBlocks(ReaderHelper reader, WriterHelper writer) {
while (true) {
int blockIndex = 0;
for (int blockIndex = 0;; blockIndex++) {
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);
writer.writeUint32(blockIndex);
writer.writeBytes(Uint8List.fromList(
List.generate(HASH_SIZE, (i) => 0))); // hash 32 ** 0x0
writer.writeUint32(0); // block size = 0
return;
}
final blockSize = block.lengthInBytes;
final blockHash = crypto.sha256.convert(block.asUint8List());
final blockHash = crypto.sha256.convert(block);
assert(blockHash.bytes.length == HASH_SIZE);
writer.writeUint32(blockIndex++);
writer.writeUint32(blockIndex);
writer.writeBytes(blockHash.bytes as Uint8List);
writer.writeUint32(blockSize);
writer.writeBytes(block.asUint8List());
writer.writeBytes(block);
}
}
}

5
lib/src/kdbx_object.dart

@ -13,7 +13,6 @@ import 'package:xml/xml.dart';
final _logger = Logger('kdbx.kdbx_object');
class ChangeEvent<T> {
ChangeEvent({this.object, this.isDirty});
@ -29,7 +28,7 @@ mixin Changeable<T> {
bool _isDirty = false;
set isDirty(bool dirty) {
_logger.finest('changing dirty (old:$_isDirty) $dirty');
// _logger.finest('changing dirty (old:$_isDirty) $dirty');
_isDirty = dirty;
_controller.add(ChangeEvent(object: this as T, isDirty: dirty));
}
@ -113,7 +112,7 @@ class KdbxUuid {
/// base64 representation of uuid.
final String uuid;
ByteBuffer toBytes() => base64.decode(uuid).buffer;
Uint8List toBytes() => base64.decode(uuid);
@override
String toString() => uuid;

9
pubspec.yaml

@ -8,6 +8,8 @@ environment:
sdk: '>=2.4.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
# path: ^1.6.0
logging: '>=0.11.3+2 <1.0.0'
crypto: '>=2.0.0 <3.0.0'
@ -26,5 +28,8 @@ dependencies:
logging_appenders: '>=0.1.0 <1.0.0'
dev_dependencies:
pedantic: ^1.7.0
test: ^1.6.0
flutter_test:
sdk: flutter
pedantic: '>=1.7.0 <2.0.0'
test: '>=1.6.0 <2.0.0'

8
test/internal/byte_utils_test.dart

@ -1,4 +1,3 @@
import 'dart:io';
import 'package:kdbx/src/internal/byte_utils.dart';
@ -13,5 +12,12 @@ void main() {
print('result: ' + ByteUtils.toHexList(writer.output.toBytes()));
expect(writer.output.toBytes(), hasLength(4));
});
test('uint64', () {
final bytes = WriterHelper.singleUint64Bytes(6000);
final read = ReaderHelper.singleUint64(bytes);
print('read: $read');
expect(read, 6000);
print('bytes: ${ByteUtils.toHexList(bytes)}');
});
});
}

7
test/pytest/Dockerfile

@ -0,0 +1,7 @@
FROM python
RUN apt-get update && apt-get install -y libgcrypt20-dev
RUN pip install pygcrypt lxml
WORKDIR /root

7
test/pytest/docker-compose.yml

@ -0,0 +1,7 @@
version: '3'
services:
py:
build: .
volumes:
- ./:/root

6
test/kdbx3_decrypt.py → test/pytest/kdbx3_decrypt.py

@ -9,7 +9,7 @@
import struct
database = 'FooBar.kdbx'
database = 'test.kdbx'
password = b'FooBar'
# password = None
#keyfile = 'test3.key'
@ -147,6 +147,7 @@ payload_data = b''
while True:
# read index of block (4 bytes)
block_index = struct.unpack('<I', raw_payload_area[offset:offset + 4])[0]
print('read block_index %d' % block_index)
# read block_data sha256 hash (32 bytes)
block_hash = raw_payload_area[offset + 4:offset + 36]
# read block_data length (4 bytes)
@ -172,3 +173,6 @@ if struct.unpack('<I', header['compression_flags']):
xml_data = zlib.decompress(payload_data, 16 + 15)
else:
xml_data = payload_data
print("got xml_data: %s" % xml_data)
Loading…
Cancel
Save