-
Notifications
You must be signed in to change notification settings - Fork 95
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
Implement framework for flexible 2FA #373
Conversation
.gitignore
Outdated
@@ -9,3 +9,4 @@ Pipfile.lock | |||
*.kdbx | |||
*.kdbx.out | |||
.idea | |||
.venv |
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.
Not using an isolated Python environment rapidly leads to a mess. I've marked the demi-standard .venv
directory as ignored to make it easier to keep a virtual environment inside the project folder.
pykeepass/multifactor_format.rst
Outdated
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 file isn't just for pykeepass
. I'm going to implement support for this in other projects next, and figured it would be good to have a reference, since the whole KeePass file format isn't centrally owned.
pykeepass/__init__.py
Outdated
|
||
from .version import __version__ | ||
|
||
__all__ = ["PyKeePass", "create_database", "__version__"] | ||
__all__ = ["PyKeePass", "create_database", "__version__", 'FactorInfo', 'FactorGroup', 'FIDO2Factor', 'KeyFileFactor'] |
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.
As these classes are necessary to make a database start using multifactor auth, I figured it made sense to expose them at the top level.
pykeepass/fido2.py
Outdated
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 file - and only this file - contains code for talking to FIDO2 authenticators.
The two operations we perform are, of course, make-credential and get-assertion.
pykeepass/kdbx_parsing/common.py
Outdated
@@ -105,7 +104,49 @@ def aes_kdf(key, rounds, key_composite): | |||
return hashlib.sha256(transformed_key).digest() | |||
|
|||
|
|||
def compute_key_composite(password=None, keyfile=None): | |||
def compute_keyfile_part_of_composite(keyfile): |
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.
Factored out of compute_key_composite
pykeepass/kdbx_parsing/common.py
Outdated
@@ -173,6 +181,30 @@ def compute_master(context): | |||
return master_key | |||
|
|||
|
|||
def populate_custom_data(kdbx, d): |
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 found working with construct
extremely difficult.
pykeepass/pykeepass.py
Outdated
@@ -152,14 +176,19 @@ def save(self, filename=None, transformed_key=None): | |||
if not filename: | |||
filename = self.filename | |||
|
|||
if hasattr(self.kdbx.header, 'data'): |
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 noticed that if you called save()
after changing anything in the header it didn't really save because the binary representation was already there...
pykeepass/pykeepass.py
Outdated
|
||
keepass_instance.save(transformed_key) | ||
keepass_instance.save(transformed_key=transformed_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.
I think this was bugged before? If I understand correctly it would pass transformed_key
as the filename
. So I guess nobody really used this function that way?
pyproject.toml
Outdated
@@ -14,6 +14,7 @@ dependencies = [ | |||
"argon2_cffi", | |||
"pycryptodomex>=3.6.2", | |||
"lxml", | |||
"fido2[pcsc]" |
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.
New dependency
requirements.txt
Outdated
@@ -1,5 +1,6 @@ | |||
lxml==4.8.0 | |||
lxml==5.1.0 |
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.
Version bump to work with fido2
. Doesn't seem to break things.
pykeepass/kdbx_parsing/factorinfo.py
Outdated
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 file contains most of the implementation heavy lift.
pykeepass/kdbx_parsing/factorinfo.py
Outdated
digest = hmac.new(decrypted_key, self.group.validation_in, 'SHA-512').digest() | ||
else: | ||
# Can't verify, we don't know how | ||
pass |
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.
In the event it's wrong we'll just end up failing to open the database later, as the constructed composite key won't pass the header checksum.
pykeepass/kdbx_parsing/factorinfo.py
Outdated
|
||
next_challenge = random.randbytes(32) | ||
|
||
if len(fido2_factors) > 0: |
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.
Rather important (and very inconvenient to implement...) feature here. We make one FIDO2 call per authenticator per group.
If there are ten Factor entries, each one of which is a FIDO2 authenticator... that means for each authenticator one would succeed and nine would fail.
If there's one authenticator plugged into the system it would not be great for the user to have to type their PIN ten times.
So instead we gather all the credentials in the group and send them in one call. The authenticator will accept the credential it created, and ignore the others.
pykeepass/fido2.py
Outdated
], | ||
extensions={ | ||
"hmacCreateSecret": True, | ||
"credentialProtectionPolicy": CredProtectExtension.POLICY.REQUIRED, |
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 forces the user to provide "verification" (a PIN, a fingerprint, etc - not just a touch) every time the credential is used.
This is important because the hmac-secret extension says that the keys used are silently different for UV (user verification) vs non-UV scenarios. It would not be great if the key were created with non-UV, and then later the authenticator is set to always-UV mode and so it becomes impossible to get the original key again.
pykeepass/fido2.py
Outdated
PublicKeyCredentialDescriptor( | ||
type=PublicKeyCredentialType.PUBLIC_KEY, | ||
id=credential_id | ||
) for credential_id in already_enrolled_credentials |
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.
Passing the credentials already in the group here prevents adding the same authenticator to the same group twice.
You can still add the same authenticator to two different groups, and of course to two different files. But I don't think it makes sense to have two credentials from the same authenticator in one group...
Closes #311 This looks good so far. I'll ask to move the contents of I'll hold off on merging until the thread over at keepassxc is mostly resolved. |
Sorry, accidentally auto-closed as I updated my branch. I will reopen this momentarily with your requested changes. |
This adds support for using the hmac-secret FIDO extension to contribute keying material for a KeePass 4 file.
It does this by storing an additional XML statekeeping blob in the outer ("public") header. This blob is designed to hold a variety of different authentication factors, such as passwords, key files, and Yubikey challenge-response devices.
The implementation here has support for passwords (primitively - no brute-force resistance), key files, and FIDO2 authenticators. It contains documentation about the changes to the KeePass file format, and tests covering the basic functionality.
It adds a dependency on
python-fido2
to perform actual FIDO2 operations with connected PC/SC or USB-HID authenticators.