Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Challenge mismatch due to differing packages used for base64 encoding/decoding #7

Open
3 tasks
titanism opened this issue Dec 13, 2023 · 9 comments
Open
3 tasks

Comments

@titanism
Copy link

Hi there - this project uses base64url on server-side and on client-side the demo uses base64-arraybuffer (albeit an outdated version). A new version of base64-arraybuffer is at https://github.com/niklasvh/base64-arraybuffer.

We recommend a few things:

@titanism
Copy link
Author

Actually, ignore this, there's a core bug in the package niklasvh/base64-arraybuffer#42

@titanism
Copy link
Author

Perhaps instead use https://github.com/mathiasbynens/base64 everywhere (it's browser-compatible too).

@titanism
Copy link
Author

Note that on the client-side, you can use this for converting from base64.decode to ArrayBuffer as required by navigator.credentials.create and navigator.credentials.get:

// <https://gist.github.com/miguelmota/5b06ae5698877322d0ca?permalink_comment_id=3611597#gistcomment-3611597>
// <https://stackoverflow.com/a/31394257>
function toArrayBuffer(buffer) {
  return buffer.buffer.slice(
    buffer.byteOffset,
    buffer.byteOffset + buffer.byteLength
  );
}

@titanism titanism reopened this Dec 13, 2023
@titanism
Copy link
Author

Re-opening with a little updated recommendation list (instead of original):

@titanism
Copy link
Author

This is what we mean by use toArrayBuffer on client-side above:

const credential = await navigator.credentials.create({
  publicKey: {
    rp: {
      name: 'SOME NAME'
    },
    user: {
      id: toArrayBuffer(base64url.toBuffer(window.USER.id)),
      // <https://blog.millerti.me/2023/02/14/controlling-the-name-displayed-during-webauthn-registration-and-authentication/>
      name: window.USER.email,
      displayName: window.USER.email
    },
    // https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/client_data_json.md
    challenge: toArrayBuffer(base64url.toBuffer(response.body.challenge)),

@titanism
Copy link
Author

Thank you @jaredhanson for all your work here on this project and package. We're implementing it on https://forwardemail.net. Reviewed a lot of the TODO's in the codebase. Happy to help maintain the project and npm releases. We also maintain @koajs, @expressjs, @ladjs, @breejs, @cabinjs, and more. If interested, just grant npm and GitHub access to the "titanism" user, and our team at @forwardemail will help maintain (we use np for releases and changelogs).

@titanism
Copy link
Author

Two other issues:

@hansemannn
Copy link

Noticed this as well. For us, using the native "Buffer.from" instead of the third party util solved it.

@intellix
Copy link

intellix commented Oct 29, 2024

Could this be because the custom library being used (base64url) is encoding strings without the base64 padding?

btoa('User:10') // 'VXNlcjoxMA=='
Buffer.from('User:10').toString('base64') // 'VXNlcjoxMA=='
base64url.encode('User:10') // 'VXNlcjoxMA'

I'm only able to get the challenge to be accepted by removing the padding:

Buffer.from('User:10').toString('base64').replaceAll('=', '');

If you decode the ArrayBuffers processed by this lib:

new TextDecoder('utf-8').decode(base64url.decode('VXNlcjoxMA')) // 'User:10'
new TextDecoder('utf-8').decode(base64url.decode('VXNlcjoxMA==')) // 'User:10\x00\x00'

If I instead use atob to create the arraybuffer:

function base64ToArrayBuffer(base64) {
    const binaryString = atob(base64);
    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}

and then I use TextDecoder to convert those from ArrayBuffers, I see a more consistent result (because it respects the paddings):

new TextDecoder('utf-8').decode(base64ToArrayBuffer('VXNlcjoxMA')) // 'User:10'
new TextDecoder('utf-8').decode(base64ToArrayBuffer('VXNlcjoxMA==')) // 'User:10'

So it seems you can use Buffer.from(str).toString('base64') on the server side, and then you just need to make sure you account for the paddings on the client side as well when doing navigator.credentials.create.

With these 2x helper functions in the browser for ArrayBuffer I'm getting it consistently working:

function base64ToArrayBuffer(base64) {
    const binaryString = atob(base64);
    const len = binaryString.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}

function arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < bytes.byteLength; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
}
return navigator.credentials.create({
  publicKey: {
    rp: {
      name: 'Todos'
    },
    user: {
      id: base64ToArrayBuffer(json.user.id),
      name: json.user.name,
      displayName: json.user.displayName
    },
    challenge: base64ToArrayBuffer(json.challenge),

and later:

.then(function(credential) {
  var body = {
    response: {
      clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
      attestationObject: arrayBufferToBase64(credential.response.attestationObject)
    }
  };

Edit: ok it seems base64url is a thing as well, my bad.. but you can do it in Node at least with toString('base64url')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants