Browse Source

Imlement generating of keyfiles (keyx format)

pull/5/head
Herbert Poul 3 years ago
parent
commit
96793a5206
  1. 91
      lib/src/credentials/keyfile.dart
  2. 4
      lib/src/crypto/protected_value.dart
  3. 45
      test/keyfile/keyfile_create_test.dart

91
lib/src/credentials/keyfile.dart

@ -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 {
@ -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);
}
@ -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 =
@ -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 {

4
lib/src/crypto/protected_value.dart

@ -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;
@ -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) {

45
test/keyfile/keyfile_create_test.dart

@ -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));
});
});
}
Loading…
Cancel
Save