How bad it is store data into webAuthN userHandle?

71 Views Asked by At

UPDATE 28.02.24

  • The first known problem is that user handle (id) is limited to 64 bytes
According to this answer, the authenticators might not use id as a sensitive (though through my tests I couldn't find evidence for that)

I just built a client-based 2FA App.

I want it to run only in the client, without any server, so the user's data will be stored in a secured vault in the client, and they can sync it (encrypted) to the Gdrive application data folder, which is not exposed without API key combined with user oauth2 token.

Every time the user refreshes the browser the db of the OTP is closed, and I am thinking about ways to make it easy for the user to open the vault, without typing again the password.

My intended solution is to use the WebAuthn id field, to store the vault-hashed user password in this field and extract it from there every time the user wants to log in.

I know that this is a "Hacky" way to do it, and it's not what WebAuthn is for.
But can you see any problem with it?

here is a code demo of what I am planning to do (tested it, the code is working)

And I'll be really happy to learn if there is any security problem with storing sensitive data on user id or something I am missing in general :-)

[and please guys, I know that this is a hack, the question is if it's problematic in any way]

const LOCAL_STORAGE_KEY = "credentialId";

async function storeSensitiveDataToWebauthN(sensitiveString) {
  let userHandle = new TextEncoder().encode(sensitiveString);

  let createOptions = {
    publicKey: {
      rp: { name: "My local test" },
      user: {
        id: userHandle,
        name: "[email protected]",
        displayName: "Example User",
      },
      challenge: new Uint8Array(32), // In practice, should be generated by the server
      pubKeyCredParams: [{ alg: -7, type: "public-key" }],
    },
  };

  const credential = await navigator.credentials.create(createOptions);

  localStorage.setItem(
    LOCAL_STORAGE_KEY,
    btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)))
  );
}

async function retrieveSensitiveDataFromWebauthN() {
  let credentialId = localStorage.getItem(LOCAL_STORAGE_KEY);

  // Correctly decode the Base64-encoded credential ID before using it

  const decodedCredentialId = Uint8Array.from(atob(credentialId), (c) =>
    c.charCodeAt(0)
  );

  let getCredentialDefaultArgs = {
    publicKey: {
      challenge: new Uint8Array(32), // Should be securely generated
      allowCredentials: [{ id: decodedCredentialId, type: "public-key" }],
    },
  };

  const assertion = await navigator.credentials.get(getCredentialDefaultArgs);

  let userHandle = assertion.response.userHandle
    ? new TextDecoder().decode(assertion.response.userHandle)
    : null;

  return userHandle;
}

export async function testWebAuthN() {
  const sensitiveString = "USER_HASHED_PASSWORD";
  if (localStorage.getItem(LOCAL_STORAGE_KEY) === null) {
    await storeSensitiveDataToWebauthN(sensitiveString);
  }
  const dateRecieved = await retrieveSensitiveDataFromWebauthN();
  if (!dateRecieved === sensitiveString) {
    console.error("Error: WebauthN failed to retrieve the correct data");
  }
  console.log("dateRecieved = ", dateRecieved);
}

some references:

1

There are 1 best solutions below

0
ks75vl On

User Handle should not contents user informations or identities, it's strong RECOMMENDED to be or delivered from at least 64 random bytes.This is crucial since user handles can be accessed via the CTAP interface without user verification (PIN or Biometric aka UV). But, You might consider to incorporate a hash with a private salt.

From W3C link:

Since the user handle is not considered personally identifying information in § 14.4.2 Privacy of personally identifying information Stored in Authenticators, and since authenticators MAY reveal user handles without first performing user verification, the Relying Party MUST NOT include personally identifying information, e.g., e-mail addresses or usernames, in the user handle. This includes hash values of personally identifying information, unless the hash function is salted with salt values private to the Relying Party, since hashing does not prevent probing for guessable input values. It is RECOMMENDED to let the user handle be 64 random bytes, and store this value in the user account.