keepassxreboot / keepassxc

KeePassXC is a cross-platform community-driven port of the Windows application “Keepass Password Safe”.
https://keepassxc.org/
Other
21.16k stars 1.46k forks source link

Support PassKey Integration (Web Authentication API) #1870

Closed Ajedi32 closed 1 year ago

Ajedi32 commented 6 years ago

Recently, the W3C finalized their Candidate Recommendation for the Web Authentication Standard, a new browser feature that allows sites to request user authentication through a standardized API.

Implementation is currently underway in Chrome, Firefox, and Edge.

Once this standard starts being implemented by sites, I'd like to be able to use KeePassXC as an Authenticator for it, so that I can store my Web Authentication private keys in the same place my existing legacy passwords are stored, and keep them synchronized across all my devices.

While it might still be too early to begin working on a concrete implementation (available documentation for the Web Authentication API is still a little sparse, outside of the standards document itself), I figured it can't hurt to start looking into this now so we're prepared when the time comes.

Would integration with the Web Authentication API be a feature worth supporting?

droidmonkey commented 6 years ago

Looks very promising!

varjolintu commented 6 years ago

I will definitely keep an eye on this!

francoisferrand commented 6 years ago

Web Authentication is supported in the latest Google Chrome release (67)

varjolintu commented 6 years ago

So far I haven't found a way to use a software as an Authenticator, and I assume it will not be possible unless you have a idiot-proof way to emulate a USB device.

Ajedi32 commented 6 years ago

It's possible official support for third-party software authenticators could be coming in the future (though I've heard no news about anything like that yet); but even in the absence of that couldn't you use a browser extension to directly intercept calls to the Web Authentication API and implement it that way?

Until we have some real-world examples of sites using the Web Authentication API as a password replacement, it's hard to say what the best way to approach the problem is.

Ajedi32 commented 5 years ago

FYI, the Web Authentication standard is now officially a W3C recommendation: https://www.w3.org/TR/2019/REC-webauthn-1-20190304/

It's possible to implement an Authenticator via a browser extension by intercepting calls to the Web Authentication API and returning an appropriate response to the web page. Krypton is a good example of how this can be done: https://github.com/kryptco/kr-u2f/blob/192af059419501bb702148ae5301f042c9859447/src/inject_webauthn_chromium.ts (Note the linked code is not FOSS so we can't copy it directly, but it does provide a general idea of how this can be done.)

WebAuthn can be tested at https://webauthn.org/ or https://webauthn.io/

droidmonkey commented 5 years ago

Cool, thank you for keeping on top of this

rugk commented 5 years ago

But is not WebAuthn intended to be used as a 2FA method? Not a replacement for passwords…

And if you use WebAuthn as a password replacement, you cannot really use any 2FA anymore? Or what should one do, if one then wants to use a hardware key as 2FA WebAuthn e.g.?

droidmonkey commented 5 years ago

WebAuthn is replacing human generated passwords with challenge response keys. These CAN be stored on hardware tokens, but certainly not necessary or desired in most cases. I for one hate carrying a token around with me.

rugk commented 5 years ago

. I for one hate carrying a token around with me.

But you notice this is a security advantage?

So let's think again about 2FA: You want:

(all and/or connected, 2 factors for two-factor-authenticatiion obviously)

So "traditionally" passwords are the first (you know them) and things like a TOTP app (on a different mobile phone) or hardware tokens are "what you have". With KeePassXC, it would only be "what you know"…

Respectively it would all be on one device (being one factor in the end). If that is compromised, all your logins are compromised. Without it, you have currently have to compromise two devices (KeePass DB and on one phone or even a hardware key), which is obviously more secure…

droidmonkey commented 5 years ago

Security is all about choice. I did not say this is replacing 2FA or that it is stronger than 2FA. The vast majority of the world has no idea what 2FA is, nor do they care. I have lowered my personal risk on those accounts that I have deemed are "high value" by using true 2FA. For accounts that are not high value, I would very much like to have the convenience of using WebAuth without a hardware token.

You can make your own choices. That's the beautiful thing about KeePassXC!

nagromc commented 5 years ago

. I for one hate carrying a token around with me.

But you notice this is a security advantage?

So let's think again about 2FA: You want:

* something you know

* something you have

* something you are

(all and/or connected, 2 factors for two-factor-authenticatiion obviously)

So "traditionally" passwords are the first (you know them) and things like a TOTP app (on a different mobile phone) or hardware tokens are "what you have". With KeePassXC, it would only be "what you know"…

Respectively it would all be on one device (being one factor in the end). If that is compromised, all your logins are compromised. Without it, you have currently have to compromise two devices (KeePass DB and on one phone or even a hardware key), which is obviously more secure…

It is understandable and I agree with you. But as @droidmonkey said, it's a trade-off between security vs. convenience. And it is perfectly assumed in the FAQ:

We believe that storing both together [passwords and TOTP secrets] can still be more secure than not using 2FA at all, but to maximize the security gain from using 2FA, you should always store TOTP secrets in a separate database, secured with a different password, possibly even on a different computer.

You have the choice to store your 2FA (TOTP or FIDO2/CTAP) secrets in the same database.

phoerious commented 5 years ago

Nothing says you could not generate a WebAuthn response from more than one factor (I haven't read the spec, but it's quite flexible and generic from what I've heard). As used today, 2FA is largely a mitigation of weak passwords and not of break ins into your machine. If your PC is taken over, then taking over your phone as well is usually rather easy (or vice versa). You only get the full advantage of 2FA if at least one of your tokens never comes in contact with the other (like e.g. a YubiKey or a dedicated hardware token generator that is hard to impossible to penetrate with malware).

rugk commented 5 years ago

If your PC is taken over, then taking over your phone as well is usually rather easy (or vice versa).

I would totally challenge that assumption…


But yes, it can be totally useful as an optional feature. I totally think users can trade-off things there. (We have the same thing with TOTP tokens in KeePassXC.)

What I am just not sure about is, whether the WebAuthn spec actually intends this implementation? Does it take the thing into account (for security implications or so) or is the spec rather intended for 2FA?

Victor239 commented 5 years ago

It's up to the website to support 2FA, I don't see any reason why WebAuthn can't be used as one factor. Similarly to how Google implements 2FA login it would just be WebAuthn on one page then TOTP or something else on the next.

varjolintu commented 5 years ago

I already have a test branch that can intercept WebAuthn create/get on the test page. For now it doesn't do anything yet, but I'll try to improve things when I have some time. But it's certainly possible to do.

darkdragon-001 commented 3 years ago

What's the state of this?

varjolintu commented 3 years ago

@darkdragon-001 Haven't had time to do it any further. Not at simple as it sounds.

nm17 commented 2 years ago

Any news on this?

varjolintu commented 2 years ago

@nm17 Yes. Sort of. I've tested some possibilities with this via the browser extension, but so far there are some security issues how to transfer certain messages from/to content scripts. At this point I haven't found a way to securely transfer some variables related to authentication without the possibility of other scripts capturing them as well.

rugk commented 2 years ago

Would it make sense to elaborate on that i.e./respectively document that progress somewhere (so other scan help/pick it up), or ask some question on Stackoverflow or similar about the problem you are facing?

varjolintu commented 2 years ago

@rugk Sure, I can provide some details. If anyone have better ideas for the implementation, please let me know.

To get the Webauthn to work, browser extension has to add a separate script file to web_accesible_resources in manifest.json, and inject that script to every page. Yes. Without that there's no way you can override navigator.credentials with your own implementation (see details: https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API). Injecting a script to every web page already gives me some shivers, but that's not all. If I want to send a message to the actual content script (and to KeePassXC from there) I'll have to use window.postMessage() and window.addEventListener('message', ...) for the response, just because the script is injected. Transferring messages directly to the background script seems impossible, and using window for messages means every other script/extension can also listen to them. Much secure.

Ajedi32 commented 2 years ago

It sounds like what you're looking for is browser.runtime.sendMessage. The KeePassXC Browser extension already has a wrapper for that defined in its existing content script that already gets injected into every page, so you can probably just reuse that.

Also, it's probably worth noting that other scripts on the same page as the content script are necessarily same-origin with the domain you're authenticating against, so they can, by design, already get access to things like passwords (by reading the value attribute of a password input element), or perform authentication using public key credentials (by calling navigator.credentials.get themselves). There's very little that can be done to prevent that, nor is it really the responsibility of WebAuthn to do so. It's only really scripts on other origins you need to worry about, and that can be solved using the sendMessage API I mentioned above.

Obviously you don't want scripts on the page being able to read the private key, regardless of their origin. The solution to that I believe is to not send the private key to content scripts in the first place. Instead you basically want to run those private parts of the script in the extension's background page/worker and communicate with the content script via message passing (e.g. using the sendMessage API I just described).

varjolintu commented 2 years ago

@Ajedi32 Thanks for your comment. I had problems passing messages using browser.runtime.sendMessage because browser namespace is not defined or available in the injected script. Otherwise it would be an ideal solution.

Ajedi32 commented 2 years ago

Okay, I see the problem you're having.

Problem summary

Content scripts can't interfere with the runtime environment of other scripts on the page, which you need to do in this case since you need to override the WebAuthn API. So instead you're declaring the WebAuthn override script as a web-accessible resource and using a content script to manually inject that into the page.

The problem is that this injected script doesn't have access to browser.runtime.sendMessage; since only content scripts can access private extension APIs. You could theoretically fix that by declaring the page as externally connectable to allow it to use postMessage, except that since WebAuthn could potentially be used on any site you would have to do that for all pages, and declaring all pages as externally connectable is explicitly disallowed for some reason.

That leaves window.postMessage as the next most obvious way of communicating between the injected script and the extension, as explained here. The only problem with that is that other scripts could potentially be listening to those events, and the normal way of preventing that using the targetOrigin parameter is, again, explicitly not allowed in extensions for some reason.

Possible solutions

Solution 1: Use postMessage anyway

The good news is, after giving it some thought I'm not certain those limitations on targetOrigin are actually a deal breaker. Obviously leaking the contents of WebAuthn API calls to third-party origins would be bad, but I'm not sure that's actually possible as long as you're only calling postMessage on your own window. Indeed, the MDN docs warn not to use a target origin of * when you use postMessage to send data to other windows, but do not give that same warning for sending data to your own window. So assuming I'm not overlooking anything, using postMessage might still be viable.

One issue is that you wouldn't be able to send responses back from the extension with postMessage, since browsers apparently set the origin of such requests to null, making them impossible to verify. That's solvable though by sending a communication channel from the host page using the Channel Messaging API and communicating back via that method rather than via postMessage (or alternately, by authenticating the postMessage response using a shared secret sent in the request message).

Another other issue is that, as you pointed out, this method doesn't protect against other scripts on the same page intercepting messages to the WebAuth API. As I said though, I don't think that's actually a security issue, since those scripts should all be same-origin with the host page (unless I'm overlooking something) and so could already intercept communications with the WebAuthn API by doing exactly what we're doing (overriding the API with their own functions).

Solution 2: Custom Events

Another alternative solution is hinted at in the MDN docs about using postMessage in extensions, where it suggests using "custom events to communicate with content scripts", "with randomly generated event names, if needed, to prevent snooping from the guest page".

See the Events article on MDN for details on how that would work. Basically you'd create a CustomEvent with a randomly-generated name, attach event listeners for that event to some common DOM object that both the content script and injected WebAuthn script have access to, then send messages back and forth by triggering that event on the DOM object. I'm not sure if this is actually more secure than postMessage, but it does seem less likely to cause accidental interference between the extension and host page at least.

varjolintu commented 2 years ago

@Ajedi32 Thanks. I've already looked at both solutions before and I agree with you that there's no perfect way to transfer those messages, but Custom Messages is a slightly better solution. What's a little concerning is that while WebAuthn is intended to be quite secure it doesn't take that much to make a malicious extension that can potentially listen to authentication messages.

The most optimal solution would be that browsers have an API for developers with a separate permission for extensions. This could allow making software authenticators much easier, but I totally understand why things are kept behind closed doors. Still, there's no permissions needed for overriding navigator.credentials when using injected scripts.

powerman commented 2 years ago

authenticating the postMessage response using a shared secret sent in the request message

If this shared secret can be detected by others by listening to all messages (or by detecting secret on own page, in case all pages uses same shared secret), then it's probably not a good idea. But there is a usual private/public key solution to this issue.

Ajedi32 commented 2 years ago

Malicious extensions are a problem in general; the whole security model of the web is based on the same origin policy and extensions violate that by design. Trying to protect the navigator.credentials API specifically wouldn't really help; there are a million ways to steal sensitive information (cookies, passwords, user data, etc) off a site once you have XSS capability there. Chrome has been trying to combat that by discouraging script injection when possible, but for a lot of use-cases (including KeePassXC's) it's an unfortunate necessity.

Ideally yeah, browsers would have dedicated extension APIs for implementing WebAuthn providers and password managers so we wouldn't have to inject scripts at all, at least for this use-case. We're not there yet, unfortunately.

In the mean time I think a solution based on either postMessage or Custom Events could get the job done, though I recognize it's a bit more awkward to implement than a sendMessage-based solution would be, if that were allowed.

Ajedi32 commented 2 years ago

authenticating the postMessage response using a shared secret sent in the request message

If this shared secret can be detected by others by listening to all messages (or by detecting secret on own page, in case all pages uses same shared secret), then it's probably not a good idea.

As I explained, I don't think we need to worry about cross-origin scripts being able to listen to messages as long as we're only posting to our own window. The shared secret is just to authenticate that the response came from a script that actually saw the original request, since the source parameter which would normally be used for that purpose apparently gets stripped from the dispatched message when it's sent by an extension.

...though actually now that I look at it, you might not need the source parameter; the origin parameter might be enough to authenticate the response message if you're sending it from a content script, and AFAIK that doesn't get stripped.

dancojocaru2000 commented 2 years ago

Ideally yeah, browsers would have dedicated extension APIs for implementing WebAuthn providers and password managers so we wouldn't have to inject scripts at all, at least for this use-case. We're not there yet, unfortunately.

Maybe support for this will arrive following Apple's announcement of Passkeys (which, afaik, is fancy Apple speak for software WebAuthn) and Google's announcement that they will support it?

varjolintu commented 2 years ago

Maybe support for this will arrive following Apple's announcement of Passkeys (which, afaik, is fancy Apple speak for software WebAuthn) and Google's announcement that they will support it?

That's one possibility. I remember I saw a mention about support for software authenticators.

Victor239 commented 2 years ago

Support for WebAuthn is now in Android and Chrome. Hopefully support in KeePassXC will come for this soon so not too many users migrate.

varjolintu commented 2 years ago

Support for WebAuthn is now in Android and Chrome. Hopefully support in KeePassXC will come for this soon so not too many users migrate.

A quick glance at this shows that the API for Passkeys with Chrome is restricted to its own password manager. Which is a bit disappointing.

varjolintu commented 2 years ago

Just wanted to share that I'm making some progress with this, but unable to say if/when it's ready for even testing.

rkjnsn commented 2 years ago

It may now be possible to avoid having to inject a script at all. Chrome (and thus presumably Edge?) now has a (currently mostly undocumented, I believe, unfortunately) webAuthenticationProxy extension API that can theoretically be used to intercept WebAuthn requests, and on Firefox the extension could use wrappedJSObject and exportFunction()/cloneInto() to hook the WebAuthn API directly from the content script.

rugk commented 2 years ago

Hmm could not find any support data or doc for that on Chromium BTW. Also no WebExtension API doc for that webAuthenticationProxy API.

rkjnsn commented 2 years ago

Yes, it unfortunately doesn't appear to be documented at all. The implementation is here: https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/web_authentication_proxy/web_authentication_proxy_api.h

An API test extension exercising the API is here: https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/data/extensions/api_test/web_authentication_proxy/main/test.js

The gist seems to be that the extension adds listeners to the various events onCreateRequest, onGetRequest, onIsUvpaaRequest, and onRequestCanceled, and then calls attach. In the event handlers (except for onRequestCanceled), carry out the requested action and call the respective complete function with the result (e.g., completeCreateRequest, completeIsUvpaaRequest, et cetera). There's also an onRemoteSessionStateChange, which appears to provide a way for a native application to kick the extension when needed. This could hypothetically be used to have the extension detach when the database is locked or KeepassXC exits, and reattach when the database is unlocked.

varjolintu commented 2 years ago

@rkjnsn As far as I know that API cannot be accessed via browser.

rkjnsn commented 2 years ago

I haven't tried to implement it, but I can definitely see the API if I inspect a dummy extension with the appropriate permission: screenshot

varjolintu commented 2 years ago

@rkjnsn Interesting. Let's hope that API is something we can actually use instead of the dirty inject script version. And it's possible to release an extension that uses that specific permission to access the API.

varjolintu commented 2 years ago

At this point I have a KeePassXC version that can build (and decode) a proper response for the WebAuthn register request. So the hardest part is already done.

varjolintu commented 2 years ago

1/2 of the steps done using webauthn.io as a test site :)

webauthn_register
Ajedi32 commented 2 years ago

I just wanted to say thanks for the great work on this so far!

Now that browsers are starting to implement synced WebAuthn credentials themselves under the banner of passkeys, this feature is about to become way more useful as mainstream sites start using passkeys as their primary authentication method.

Furthermore, independent cross-browser implementations like KeePassXC are going to be really important for avoiding issues of vendor lock-in in the short term, since from what I've seen so far most of the browsers implementing passkeys right now don't support any mechanism of transferring the key database to another browser.

varjolintu commented 1 year ago

@Ajedi32 The next step here is of course to decide how the key is stored in KeePassXC so it can be easily exported as a file if needed. Importing keys to another database (or some other password manager) would be quite helpful.

EDIT: The proper format for the key is of course https://en.wikipedia.org/wiki/PKCS_8.

varjolintu commented 1 year ago
webauthn_get

One more step achieved.

varjolintu commented 1 year ago

One thing that will make this feature a bit problematic is the signature count which is updated every time a successful authentication is made. This means the database is updated every time the keys are used, which can cause problems to those users who are syncing the database manually for multiple devices. If the count added to the response is too low, authentication with the web site fails.

So for now I'm making the signature count as an attribute which can be easily edited if needed. Not the optimal solution, but it could help.

zroug commented 1 year ago

I think signature counters are optional and not supporting them has gotten more common since the introduction of passkeys, because they face the same problem. https://github.com/w3c/webauthn/issues/1734 talks a bit about this. Maybe just leaving the counter at zero is an option? However, my knowledge on this is very limited, so please excuse me if I'm mistaken on this. I just wanted to mention it, but I don't know if it is viable.

droidmonkey commented 1 year ago

I like the start at 0 stay at 0 approach.

varjolintu commented 1 year ago

The site I'm using to test this feature has a mandatory signature counter. The request from the server doesn't include any flag or variable that indicates if the counter is used or not, so staying at 0 will fail the authentication. There must be an option to increase it.

droidmonkey commented 1 year ago

Seems to be a bug in the test then because according to the github discussion it is sn optional feature.