MasterKale / SimpleWebAuthn

WebAuthn, Simplified. A collection of TypeScript-first libraries for simpler WebAuthn integration. Supports modern browsers, Node, Deno, and more.
https://simplewebauthn.dev
MIT License
1.62k stars 137 forks source link

Uint8Array --> String --> Uint8Array Conversion from isoUint8Array helper doesn't work #509

Closed seba9999 closed 10 months ago

seba9999 commented 10 months ago

Describe the issue

I'm trying to convert the credentialID (Uint8Array) to an UTF-8 string in order to save it in my MongoDB Database. I've tried multiple packages to do so ... Until I found out that there's some helpers into this library itself ...

But the flow : Uint8Array --> String --> Uint8Array seems ... broken ? Am I missing something ?

Reproduction Steps


const { isoUint8Array } = require('@simplewebauthn/server/helpers');

------

let verification;
try {
  verification = await verifyRegistrationResponse({
    response: challengeResponse,
    expectedChallenge: user.challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
  });
  if (verification.verified) {
    // Save authenticator :
    console.log('Generated CredentialID = ', verification.registrationInfo.credentialID);
    const stringifiedCredentialID = isoUint8Array.toUTF8String(
      verification.registrationInfo.credentialID
    );
    console.log('stringifiedCredentialID = ', stringifiedCredentialID);
    const backToUInt8Array = isoUint8Array.fromUTF8String(stringifiedCredentialID);
    console.log('textEncodedCredentialID = ', backToUInt8Array );

    const auth = new Authenticator({
      ...verification.registrationInfo,
      credentialID: stringifiedCredentialID,
      credentialPublicKey: '' // @todo,
      transports: challengeResponse?.response?.transports,
    });
    auth.userId = user._id;
    await auth.save();
  }
  res.status(200).json({ ok: verification.verified });
} catch (error) {
  console.error(error);
  return res.status(400).send({ message: error.message });
}

And here is the result : image

Expected behavior

The converted back Uint8Array should be the same ? Right ? And the String must be UTF-8 no ?

Dependencies

$ npm list --depth=0 | grep @simplewebauthn
├── @simplewebauthn/browser@8.3.4
├── @simplewebauthn/server@8.3.6
# ...
seba9999 commented 10 months ago

I've noticed that it's working with the isoUint8Array.toHex helper :

// Saving :
const hexCredentialID = isoUint8Array.toHex(verification.registrationInfo.credentialID);
const hexCredentialPublicKey = isoUint8Array.toHex(verification.registrationInfo.credentialPublicKey);

// Authentification Option : 
const previousAuthenticator = await Authenticator.findOne({ userId: user._id });
const credentialIDbackFromHex = isoUint8Array.fromHex(previousAuthenticator.credentialID);
console.log('credentialIDbackFromHex = ', credentialIDbackFromHex ); // The "restored" uInt8Array is the same !

const options = await generateAuthenticationOptions({
  rpID,
  allowCredentials: [
    {
      id: credentialIDbackFromHex ,
      type: 'public-key',
      transports: previousAuthenticator.transports,
    },
  ],
  userVerification: 'preferred',
});

I can then trigger the windows hello prompt successfully but then on the verify part I get this error : Error: No packed values available :

image

Verify part code :

const previousAuthenticator = await Authenticator.findOne({ userId: user?._id });
try {
  const backFromHexCredentialID = isoUint8Array.fromHex(previousAuthenticator.credentialID);
  const backFromHexPublicKey = isoUint8Array.fromHex(previousAuthenticator.credentialID);

  verification = await verifyAuthenticationResponse({
    response: challengeResponse,
    expectedChallenge: user?.challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    authenticator: {
      ...previousAuthenticator,
      credentialID: backFromHexCredentialID,
      credentialPublicKey: backFromHexPublicKey,
    },
    requireUserVerification: true,
  });
  res.status(200).json({ ok: verification.verified });
} catch (error) {
  console.error(error);
  return res.status(400).send({ message: error.message });
}
MasterKale commented 10 months ago

Credential ID bytes are effectively random so I'm not at all surprised they're not encoding to readable UTF-8. I recommend using the isoBase64URL helper instead, its isoBase64URL.fromBuffer() and isoBase64URL.toBuffer() are better at encoding these bytes into strings (though isoUint8Array.toHex() and isoUint8Array.fromHex() are fine too, though its seemingly unorthodox to me because so much of WebAuthn uses base64url encoding.)

can then trigger the windows hello prompt successfully but then on the verify part I get this error : Error: No packed values available

Looking at your code sample I think I see the problem:

const backFromHexPublicKey = isoUint8Array.fromHex(previousAuthenticator.credentialID);

previousAuthenticator.credentialID should probably be previousAuthenticator.credentialPublicKey (or whatever it's called on your end)...

seba9999 commented 10 months ago

O.M.G

I can't believe this ! I've changed credentialID to credentialPublicKey and it works ... 💀

Too much copy / paste kills copy / paste 😢

Sorry to made you loose your time ! And a big thank you ! ♥

At least I've learned that I should probably use isoBase64URL.fromBuffer() / isoBase64URL.toBuffer() 😅