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:
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: