Skip to content

Commit

Permalink
Imlement generating of keyfiles (keyx format)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpoul committed Aug 22, 2021
1 parent e2fd168 commit 96793a5
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 8 deletions.
91 changes: 85 additions & 6 deletions lib/src/credentials/keyfile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ import 'dart:typed_data';

import 'package:collection/collection.dart' show IterableExtension;
import 'package:convert/convert.dart' as convert;
import 'package:crypto/crypto.dart' as crypto;
import 'package:kdbx/src/credentials/credentials.dart';
import 'package:kdbx/src/crypto/protected_value.dart';
import 'package:xml/xml.dart' as xml;
import 'package:crypto/crypto.dart' as crypto;

import 'package:kdbx/src/utils/byte_utils.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml;

final _logger = Logger('keyfile');

const _nodeVersion = 'Version';
const _nodeKey = 'Key';
const _nodeData = 'Data';
const _nodeMeta = 'Meta';
const _nodeKeyFile = 'KeyFile';
const _nodeHash = 'Hash';

class KeyFileCredentials implements CredentialsPart {
factory KeyFileCredentials(Uint8List keyFileContents) {
try {
Expand All @@ -22,13 +30,27 @@ class KeyFileCredentials implements CredentialsPart {
}
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;
xmlContent.findAllElements(_nodeVersion).singleOrNull?.text;
final key = xmlContent.findAllElements(_nodeKey).single;
final dataString = key.findElements(_nodeData).single;
final encoded = dataString.text.replaceAll(RegExp(r'\s'), '');
Uint8List dataBytes;
if (metaVersion != null && metaVersion.startsWith('2.')) {
dataBytes = convert.hex.decode(encoded) as Uint8List;
assert((() {
final hash = dataString.getAttribute(_nodeHash);
if (hash == null) {
throw const FormatException('Keyfile must contain a hash.');
}
final expectedHashBytes = convert.hex.decode(hash);
final actualHash =
crypto.sha256.convert(dataBytes).bytes.sublist(0, 4) as Uint8List;
if (!ByteUtils.eq(expectedHashBytes, actualHash)) {
throw const FormatException(
'Corrupted keyfile. Hash does not match');
}
return true;
})());
} else {
dataBytes = base64.decode(encoded);
}
Expand All @@ -42,6 +64,13 @@ class KeyFileCredentials implements CredentialsPart {
}
}

/// Creates a new random (32 bytes) keyfile value.
factory KeyFileCredentials.random() => KeyFileCredentials._(
ProtectedValue.fromBinary(ByteUtils.randomBytes(32)));

factory KeyFileCredentials.fromBytes(Uint8List bytes) =>
KeyFileCredentials._(ProtectedValue.fromBinary(bytes));

KeyFileCredentials._(this._keyFileValue);

static final RegExp _hexValuePattern =
Expand All @@ -54,6 +83,56 @@ class KeyFileCredentials implements CredentialsPart {
return _keyFileValue.binaryValue;
// return crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List;
}

/// Generates a `.keyx` file as described for Keepass keyfile:
/// https://keepass.info/help/base/keys.html#keyfiles
Uint8List toXmlV2() {
return utf8.encode(toXmlV2String()) as Uint8List;
}

/// Generates a `.keyx` file as described for Keepass keyfile:
/// https://keepass.info/help/base/keys.html#keyfiles
@visibleForTesting
String toXmlV2String() {
final hash =
(crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List)
.sublist(0, 4);
final hashHexString = hexFormatLikeKeepass(convert.hex.encode(hash));
final keyHexString =
hexFormatLikeKeepass(convert.hex.encode(_keyFileValue.binaryValue));

final builder = xml.XmlBuilder()
..processing('xml', 'version="1.0" encoding="utf-8"');
builder.element(_nodeKeyFile, nest: () {
builder.element(_nodeMeta, nest: () {
builder.element(_nodeVersion, nest: () {
builder.text('2.0');
});
});
builder.element(_nodeKey, nest: () {
builder.element(_nodeData, nest: () {
builder.attribute(_nodeHash, hashHexString);
builder.text(keyHexString);
});
});
});
return builder.buildDocument().toXmlString(pretty: true);
}

/// keypass has all-uppercase letters in pairs of 4 bytes (8 characters).
@visibleForTesting
static String hexFormatLikeKeepass(final String hexString) {
final hex = hexString.toUpperCase();
const _groups = 8;
final remaining = hex.length % _groups;
return [
for (var i = 0; i < hex.length ~/ _groups; i++)
hex.substring(i * _groups, i * _groups + _groups),
if (remaining != 0) hex.substring(hex.length - remaining)
].join(' ');
// range(0, hexString.length / 8).map((i) => hexString.substring(i*_groups, i*_groups + _groups));
// hexString.toUpperCase().chara
}
}

class KeyFileComposite implements Credentials {
Expand Down
4 changes: 2 additions & 2 deletions lib/src/crypto/protected_value.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ProtectedValue implements StringValue {
return ProtectedValue(_xor(value, salt), salt);
}

static final random = Random.secure();
static final _random = Random.secure();

final Uint8List _value;
final Uint8List _salt;
Expand All @@ -57,7 +57,7 @@ class ProtectedValue implements StringValue {

static Uint8List _randomBytes(int length) {
return Uint8List.fromList(
List.generate(length, (i) => random.nextInt(0xff)));
List.generate(length, (i) => _random.nextInt(0xff)));
}

static Uint8List _xor(Uint8List a, Uint8List b) {
Expand Down
45 changes: 45 additions & 0 deletions test/keyfile/keyfile_create_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:typed_data';

import 'package:kdbx/kdbx.dart';
import 'package:logging/logging.dart';
import 'package:quiver/iterables.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';

import '../internal/test_utils.dart';

final _logger = Logger('keyfile_create_test');

void main() {
// ignore: unused_local_variable
final testUtils = TestUtil.instance;
final exampleBytes = Uint8List.fromList(
range(0, 16).expand((element) => [0xca, 0xfe]).toList());
group('creating keyfile', () {
test('Create keyfile', () {
final keyFile = KeyFileCredentials.fromBytes(exampleBytes);
final output = keyFile.toXmlV2String();
_logger.info(output);
expect(output, contains('Hash="4CA06E29"'));
expect(output, contains('CAFECAFE CAFECAFE'));
});
test('hex format', () {
final toTest = {
'abcd': 'ABCD',
'abcdefgh': 'ABCDEFGH',
'abcdef': 'ABCDEF',
'1234567812345678': '12345678 12345678',
'12345678123456': '12345678 123456',
};
for (final e in toTest.entries) {
expect(KeyFileCredentials.hexFormatLikeKeepass(e.key), e.value);
}
});
test('create and load', () {
final keyFile = KeyFileCredentials.fromBytes(exampleBytes);
final output = keyFile.toXmlV2();
final read = KeyFileCredentials(output);
expect(read.getBinary(), equals(exampleBytes));
});
});
}

0 comments on commit 96793a5

Please sign in to comment.