From 08711987f51ca72f6af7da6346fe6d919704ca90 Mon Sep 17 00:00:00 2001 From: Herbert Poul Date: Sun, 20 Oct 2019 20:19:15 +0200 Subject: [PATCH] add support for decrypting with key file. (and password + keyfile) --- lib/src/crypto/protected_value.dart | 5 ++ lib/src/kdbx_format.dart | 74 +++++++++++++++++++++++++--- test/kdbx_test.dart | 38 +++++++++++--- test/password-and-keyfile.kdbx | Bin 0 -> 1358 bytes test/password-and-keyfile.key | 9 ++++ 5 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 test/password-and-keyfile.kdbx create mode 100644 test/password-and-keyfile.key 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 0000000000000000000000000000000000000000..a1667249a7e16a91650093811f5cf28008b2b852 GIT binary patch literal 1358 zcmV-U1+n@A*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0096100bZaxz4SO zYRVez$&-^pHB#M}JTYvuaU}H$8h0QFCT@L(1t0)py0ALQ+O{e=LQN?-%xljvMvB6Z z8ye!Z-llr%aa2nN2ms)d1ONa4000LN0FoCj&=XB}e)TM4w!qgPMF=1O7+TgO9e>tv zMEqDKE0!m!!_8BF7e2?7@%}OW%aNVM2_OJt<7&q5wjLJ!2@)0ikmXSQr7QB|lOY3_ zh!H5WI&$|41ONg600004007Xf3VH0AikkE=x&aliJ&CJ58`_KGEb%foD{9P?M{~)!NBYJ~w{MDx1#liva}P>JtHO z^5x?KE+d1%XAL@^R#VAcc5Y_MI!i_lf2#)5*9#qI>^o`D2-5Srt*J0Ib>0K*i90%E zI#yB!t|09FFAlaN<53wNs_QtYC*aX<>&Rf=v!t0pX2GMk&U~*JW1A}=4f`WpwIZ_l z)<*AcGW1OEph;0QTaF=i`jvV$;$D{g5+{Xsd`ZL8#J5pukutF~HuCmwxP zUU0G9&BDm%V24~2CzD3W_XMNeinCLRX$Tm(nk+FRb1LFp>E`g%D;DIsM_-!=LrP&v z@DzFSC-R>N8=SvFAX9&Yyx}AOM3%Bc$VDCdF5U!G6|2=9KTW%mC^ej^36q;b+^#jD z=?7`?w*r?Avey86IAb|kY?UDU;*X2GAKhj#Z3}>UI#9V zvtwawK*`!U?VQ3`#Y89MR3Rl(D6KSz9opG|zL!DH@S#Crx+s z(!I}~2%De%Fsz4T?StRx3T!0=V0Os-=UERFCMo8X6{A zs){<}Q%X}SkJuay`(ptN6~5Uet;N9c>+9HGyssD%HD}aK^UoVpdZ9;!A>iIRIvjIp zP~02ns2wGjGkr9qH{X-GE&9znJX&MiGrj~6wf+dX3aARklpQm7SyC^cm zM7tkeLK$$s7HN9!*(yq}*BUcP)nr4{9T9aq<3wnW;8af9Gq0!(y&9yZD2=%nS?s;lC?7_K1k+3Vm)XKzjC6X zK)KDyS3|`0iV%jeH0=jCgFIiA(Ji6SA*@{;jTF~9V2D(4^UIZjLVfTi9z9JL Q>XwQOLjB1lCVE_OC literal 0 HcmV?d00001 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