-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Groundwork to support flexible multifactor database authentication and FIDO2 #10311
base: develop
Are you sure you want to change the base?
Conversation
If this general feature works okay I'll implement support for FIDO2 in KeePassDX next, so we'll have compatible implementations everywhere relevant. |
@@ -197,6 +197,8 @@ QString SymmetricCipher::modeToString(const Mode mode) | |||
return QStringLiteral("AES-128/CBC"); | |||
case Aes256_CBC: | |||
return QStringLiteral("AES-256/CBC"); | |||
case Aes256_CBC_UNPADDED: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AES256-CBC implementation here is encrypting exactly-32-byte-long key parts which are randomly generated. So padding doesn't really make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to introduce a new algorithm. If padding is not required (in this case a 32-byte aligned block) then padding won't be added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because decryption did actually fail with aligned blocks. Botan expects there to be a minimum of one padding block - how else would it know how much padding had been added?
In other words, if you have a 31 byte block, that would have one byte of padding and end up 32 bytes long. But if you have a 32 byte long block... How do you know on decryption whether it's 32 or 31 bytes before padding? There must be at least one byte of padding to store the padding length itself...
I think we need this "new" algorithm (well, variant).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm good point
@@ -95,6 +96,30 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointer<const CompositeK | |||
return false; | |||
} | |||
|
|||
auto authenticationFactorInfo = m_db->authenticationFactorInfo(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is one of the two changes to the existing database reading code: if we read information about authentication factors from the header, either add a MultiAuthenticationHeaderKey to the CompositeKey or replace the CompositeKey with one entirely.
@@ -224,6 +224,23 @@ bool Kdbx4Reader::readHeaderField(StoreDataStream& device, Database* db) | |||
variantBuffer.open(QBuffer::ReadOnly); | |||
QVariantMap data = readVariantMap(&variantBuffer); | |||
db->setPublicCustomData(data); | |||
|
|||
auto it = data.constFind(AUTHENTICATION_FACTORS_HEADER_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the second change to the existing database reading code: read, validate, and store authentication factor info from one member of the outer header.
|
||
auto key = QSharedPointer<CompositeKey>::create(); | ||
auto passwordKey = QSharedPointer<PasswordKey>::create(); | ||
passwordKey->setPassword(QByteArray::fromStdString("somepassword")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This KDBX file created with keepasspy doesn't actually use SHA256("somepassword") as its key. It uses a random key stored encrypted in the header, and SHA256("somepassword") can decrypt that key part.
In other words, this test shows the whole flow working end-to-end.
Oh this is fun! Will comb through this soon. |
I am willing to provide my design skills for some of the UI parts, although I'm definitely not a king at programming! |
explicit FactorKeyDerivation() = default; | ||
~FactorKeyDerivation() override = default; | ||
|
||
virtual bool derive(QByteArray& data, const QByteArray& key, const QByteArray& salt); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a purely virtual function (= 0
)
@@ -197,6 +197,8 @@ QString SymmetricCipher::modeToString(const Mode mode) | |||
return QStringLiteral("AES-128/CBC"); | |||
case Aes256_CBC: | |||
return QStringLiteral("AES-256/CBC"); | |||
case Aes256_CBC_UNPADDED: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to introduce a new algorithm. If padding is not required (in this case a 32-byte aligned block) then padding won't be added.
I've added a commit to attempt to address build failures. I think your test coverage agent is running a different compiler (likely gcc) than I am (clang). It appears there are some minor differences in how the QT libraries work between the two. If it fails again please forgive me - on my local machine, |
I believe the MacOS build failure is test flake, rather than a problem with this PR.
I don't have permission to rerun it, but other than that the automated checks now pass. |
I just stumbled over that myself: #10320 |
There is another piece of flake in |
@droidmonkey I think I've addressed all the feedback. I also checked that botan uses PKCS7 padding. PKCS7, when you need N bytes of padding, puts the number N N times. So a 15-byte message gets Are you waiting for something else from me here? If this direction is okay I can proceed to implement the saving and FIDO2 parts of the change. |
I'm good, will review as I can |
My "make format" seems to be doing something different from the CI formatting run. At any rate, this is still ready for review whenever. |
Yah cmake formatting was recently updated and we have a PR to align to the new version |
hi, is there any status update? |
This code is waiting for review by a project maintainer. |
This comment has been minimized.
This comment has been minimized.
This adds the ability to parse and validate (but not use) authentication factor information contained within the KDBX outer header. Use authentication factor headers if present for opening database Saving the database will still discard the header information, but read-only access works.
Closes #11582
Screenshots
No user-visible changes.
Testing strategy
Unit tests for parsing of the header included, and a Kdbx test opening a database secured using header-described password authentication.
Type of change
Relates to #9506 .
Description
This PR allows KeePassXC to read files containing an XML blob in their header describing multifactor authentication for the database. This blob can describe a replacement for the existing composite key, or append to it. One or more "groups" can be used, and each group is one-of-N.
Either way, the multifactor methods' output goes through the EXISTING key derivation function - it's just input to the KDF.
The target use case here is to support using one-of-N FIDO authenticators: for that use case, the header would describe a single group with N-many Factors in it.
I have implemented end-to-end support for FIDO2 in this format in KeePassPy at libkeepass/pykeepass#373 , including human-readable documentation of the file format. But this repository is generally cleaner than that one so I figured I'd raise an intermediate PR rather than a big-bang here.
What is Done
What is Not Yet Done
Please let me know if this is going in an acceptable direction. If so, I'll keep going, but I'd rather not have a PR with 3000-plus lines in it.
Note that this PR contains two cleanly separated commits and it may be easier to look at the first one first.