0 [WIP] FIDO2 hmac otp
Dimitri Witkowski edited this page 2021-03-31 17:37:18 +02:00

This is a draft of using hmac-otp FIDO2 feature for YubiKey support in KeeWeb.
This document is not ready and cannot be used for implementation, it has unresolved security issues.

You will find a discussion here: https://github.com/keeweb/keeweb/issues/1681

First, some context. FIDO2 hmac-secret extension provides support for hashing arbitrary data using a secret attached to a credential stored on the authenticator. It has two inputs:

  1. credential id generated by the authenticator, obtained during credential registration (512 bit)
  2. challenge, client-defined data (512 bit)

Some notes to clarify the implementation:

  • The secret used for signing data is stored on the authenticator and is never exposed. It's not possible to provide your own secret when making a new credential. This means, you can't make a backup of your YubiKey that would produce the same hash based on the same input.
  • Credential id must be generated by the authenticator, it's not possible to make your own one. Even if it's non-resident (see below), the authenticator must not accept credentials produced not by this authenticator.
  • FIDO2.1 defines discoverable (aka "resident") credentials. To retrieve such credential, most of authenticators will present the PIN question.
    • benefit: we don't have to store credential id (however we still need to identify the authenticator, so there's not much benefit here as it's also noted in the linked issue in KeePassXC and we can just use regular, not persistent credentials)
    • downside: space for these credentials is very limited, it's usually around 5..20 credentials per authenticator in total, if it's implemented at all
  • hmac-secret is a FIDO2 assertion, most of authenticators check user presence, for example, on a YubiKey you need to press the button.

In the KDBX format, such kind of hashing is used to calculate the master hash based on salt stored in the public file header. Let's assume, we want to protect our database with a password and a YubiKey. To open the database, we need both things. In this case the master key is calculated as follows (this explanation is simplified to illustrate the idea):

MasterKey = KDF( PasswordHash ⨁ HMAC(Challenge) )

where:

  • KDF is a so-called key derivation function, one-way resource-hard transformation, such as Argon2
  • PasswordHash is calculated based on entered password
  • HMAC is the transformation performed by the authenticator
  • Challenge is something stored in the public file header
  • MasterKey is the encryption key used to decrypt data stored in the database

Our goal is to have several authenticators each of them would be able to open the database. As it's not possible to program your own secret, these authenticators can't have equal HMAC transformation and instead they provide HMAC_1, HMAC_2, and so on.

We want to find a way to make such challenge that:

MasterKey = KDF( PasswordHash ⨁ HMAC_1(Challenge) )
MasterKey = KDF( PasswordHash ⨁ HMAC_2(Challenge) )
...
MasterKey = KDF( PasswordHash ⨁ HMAC_N(Challenge) )

To achieve this, we add an additional step to HMAC function that depends on used authenticator and an additional value stored in the header (let's call it HMAC*):

HMAC*: (data, x) => {
    TransformResponse(
        HMAC_X(
            MakeChallenge(data, x)
        ),
        data,
        x
    )
}

MakeChallenge: (data, x) => {
    item.challenge from data
        where AuthenticatorXHasIdentity(item.identity)
}

TransformResponse: (response, data, x) => {
    ❗ this is not good! it needs one-way transformation here
    response ⨁ (item.transform from data
                    where AuthenticatorXHasIdentity(item.identity))
}

where

  • x is authenticator id
  • data is a data structure stored in the public header, see below

Addition to the header presented in human-readable form (would be stored in the header extensions part as binary, json is only for explanation):

{
    "auth": [
        {
            "❗ TODO": "decide what to do with these identities",
            "identity": "credential identification, for example, credential_id",
            "challenge": "challenge used to compute HMAC on the authenticator",
            "transform": "piece of data used for HMAC transformation"
        }
    ]
}

When saving the file, we need to generate a new challenge-response on this authenticator. It can be achieved by updating transform value for other authenticators used to decrypt this file, so that HMAC* calculation will produce the same result:

ReplaceHmac: (data, x) => {
    // ❗ TODO: rewrite this thing, it should not be possible to read authenticator responses from the header
    // this can be cached to avoid computation
    old_hmac := HMAC*(data, x)

    data[x].challenge := random()
    data[x].transform := random()

    new_hmac := HMAC*(data, x)

    authenticator_i_response := data[i].transform ⨁ old_hmac
    data[i].transform := new_hmac ⨁ authenticator_i_response
        for i in data where i ≠ x
}

Additionally, there can be a section inside the file where you can assign names to authenticators. This will allow users to detach authenticators from files. This section will contain the mapping of data.identity to a human-readable representation.

Here's an image representation of the proposed change:

TODO: update the diagram with an OWF transform

hmac-secret in kdbx

Benefits of this approach:

  1. Users can link any number of authenticators to one database;
  2. YubiKeys don't have to be re-programmed manually, so the probability of secret leakage is less;
  3. It can be extensible for other authentication methods, for example, WebAuthn on web.

Concerns:

  1. Storing identities in the public header of the database exposes used authenticators, if attackers get access to the kdbx file, they will know that you use an authenticator to open it.

TODO:

  1. Add a one-way transform function so that responses from other authenticators cannot be read by just one of them.
  2. Describe the storage inside the file.