diff --git a/lib/src/crypto/protected_value.dart b/lib/src/crypto/protected_value.dart index e4f34bd..4c6d6c5 100644 --- a/lib/src/crypto/protected_value.dart +++ b/lib/src/crypto/protected_value.dart @@ -41,6 +41,11 @@ class ProtectedValue implements StringValue { return ProtectedValue(_xor(valueBytes, salt), salt); } + factory ProtectedValue.fromBinary(Uint8List value) { + final Uint8List salt = _randomBytes(value.length); + return ProtectedValue(_xor(value, salt), salt); + } + static final random = Random.secure(); final Uint8List _value; diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 30c4c9e..4fd2f12 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -22,7 +22,13 @@ import 'kdbx_object.dart'; final _logger = Logger('kdbx.format'); abstract class Credentials { - factory Credentials(ProtectedValue password) => PasswordCredentials(password); + factory Credentials(ProtectedValue password) => + Credentials.composite(password, null); //PasswordCredentials(password); + factory Credentials.composite(ProtectedValue password, Uint8List keyFile) => + KeyFileComposite( + password: password == null ? null : PasswordCredentials(password), + keyFile: keyFile == null ? null : KeyFileCredentials(keyFile), + ); Credentials._(); @@ -31,18 +37,70 @@ abstract class Credentials { Uint8List getHash(); } -class PasswordCredentials implements Credentials { +class KeyFileComposite implements Credentials { + KeyFileComposite({@required this.password, @required this.keyFile}); + PasswordCredentials password; + KeyFileCredentials keyFile; + + Uint8List getHash() { + final buffer = [...?password?.getBinary(), ...?keyFile?.getBinary()]; + return crypto.sha256.convert(buffer).bytes as Uint8List; + +// final output = convert.AccumulatorSink(); +// final input = crypto.sha256.startChunkedConversion(output); +//// input.add(password.getHash()); +// input.add(buffer); +// input.close(); +// return output.events.single.bytes as Uint8List; + } +} + +abstract class CredentialsPart { + Uint8List getBinary(); +} + +class KeyFileCredentials implements CredentialsPart { + factory KeyFileCredentials(Uint8List keyFileContents) { + final keyFileAsString = utf8.decode(keyFileContents); + try { + if (_hexValuePattern.hasMatch(keyFileAsString)) { + return KeyFileCredentials._(ProtectedValue.fromBinary( + convert.hex.decode(keyFileAsString) as Uint8List)); + } + final xmlContent = xml.parse(keyFileAsString); + final key = xmlContent.findAllElements('Key').single; + final dataString = key.findElements('Data').single; + final dataBytes = base64.decode(dataString.text); + _logger.finer('Decoded base64 of keyfile.'); + return KeyFileCredentials._(ProtectedValue.fromBinary(dataBytes)); + } catch (e, stackTrace) { + _logger.warning( + 'Unable to parse key file as hex or XML, use as is.', e, stackTrace); + final bytes = crypto.sha256.convert(keyFileContents).bytes as Uint8List; + return KeyFileCredentials._(ProtectedValue.fromBinary(bytes)); + } + } + KeyFileCredentials._(this._keyFileValue); + + static final RegExp _hexValuePattern = RegExp(r'/^[a-f\d]{64}$/i'); + + final ProtectedValue _keyFileValue; + + @override + Uint8List getBinary() { + return _keyFileValue.binaryValue; +// return crypto.sha256.convert(_keyFileValue.binaryValue).bytes as Uint8List; + } +} + +class PasswordCredentials implements CredentialsPart { PasswordCredentials(this._password); final ProtectedValue _password; @override - Uint8List getHash() { - final output = convert.AccumulatorSink(); - final input = crypto.sha256.startChunkedConversion(output); - input.add(_password.hash); - input.close(); - return output.events.single.bytes as Uint8List; + Uint8List getBinary() { + return _password.hash; } } diff --git a/test/kdbx_test.dart b/test/kdbx_test.dart index 0d6bda1..f772821 100644 --- a/test/kdbx_test.dart +++ b/test/kdbx_test.dart @@ -15,7 +15,6 @@ class FakeProtectedSaltGenerator implements ProtectedSaltGenerator { @override String encryptToBase64(String plainValue) => 'fake'; - } void main() { @@ -25,27 +24,46 @@ void main() { setUp(() {}); test('First Test', () async { - final data = await File('test/FooBar.kdbx').readAsBytes() as Uint8List; + final data = await File('test/FooBar.kdbx').readAsBytes(); KdbxFormat.read(data, Credentials(ProtectedValue.fromString('FooBar'))); }); }); + group('Composite key', () { + test('Read with PW and keyfile', () async { + final keyFileBytes = + await File('test/password-and-keyfile.key').readAsBytes(); + final cred = Credentials.composite( + ProtectedValue.fromString('asdf'), keyFileBytes); + final data = await File('test/password-and-keyfile.kdbx').readAsBytes(); + final file = KdbxFormat.read(data, cred); + expect(file.body.rootGroup.entries.length, 1); + }); + }); + group('Creating', () { test('Simple create', () { - final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); + final kdbx = KdbxFormat.create( + Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); expect(kdbx, isNotNull); expect(kdbx.body.rootGroup, isNotNull); expect(kdbx.body.rootGroup.name.get(), 'CreateTest'); expect(kdbx.body.meta.databaseName.get(), 'CreateTest'); - print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); + print(kdbx.body + .generateXml(FakeProtectedSaltGenerator()) + .toXmlString(pretty: true)); }); test('Create Entry', () { - final kdbx = KdbxFormat.create(Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); + final kdbx = KdbxFormat.create( + Credentials(ProtectedValue.fromString('FooBar')), 'CreateTest'); final rootGroup = kdbx.body.rootGroup; final entry = KdbxEntry.create(kdbx, rootGroup); rootGroup.addEntry(entry); - entry.setString(KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); - print(kdbx.body.generateXml(FakeProtectedSaltGenerator()).toXmlString(pretty: true)); + entry.setString( + KdbxKey('Password'), ProtectedValue.fromString('LoremIpsum')); + print(kdbx.body + .generateXml(FakeProtectedSaltGenerator()) + .toXmlString(pretty: true)); }); }); @@ -65,7 +83,11 @@ void main() { // print(ByteUtils.toHexList(saved)); final kdbx = KdbxFormat.read(saved, credentials); - expect(kdbx.body.rootGroup.entries.first.getString(KdbxKey('Password')).getText(), 'LoremIpsum'); + expect( + kdbx.body.rootGroup.entries.first + .getString(KdbxKey('Password')) + .getText(), + 'LoremIpsum'); File('test.kdbx').writeAsBytesSync(saved); }); }); diff --git a/test/password-and-keyfile.kdbx b/test/password-and-keyfile.kdbx new file mode 100644 index 0000000..a166724 Binary files /dev/null and b/test/password-and-keyfile.kdbx differ diff --git a/test/password-and-keyfile.key b/test/password-and-keyfile.key new file mode 100644 index 0000000..f554b99 --- /dev/null +++ b/test/password-and-keyfile.key @@ -0,0 +1,9 @@ + + + + 1.00 + + + SZgOtaihUic9M/gfyhLVPkX5HQ1ipuSSYe5ej4SIRv8= + + \ No newline at end of file