From cac818173050ec1e18d73d0f880c826901cf4abd Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 2 Jun 2020 11:51:28 -0700 Subject: Add new Base64URL<->Buffer helper methods --- .../browser/src/helpers/base64URLStringToBuffer.ts | 33 ++++++++++++++++++++++ .../src/helpers/bufferToBase64URLString.test.ts | 15 ++++++++++ .../browser/src/helpers/bufferToBase64URLString.ts | 21 ++++++++++++++ .../browser/src/helpers/toBase64String.test.ts | 15 ---------- packages/browser/src/helpers/toBase64String.ts | 6 ---- 5 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 packages/browser/src/helpers/base64URLStringToBuffer.ts create mode 100644 packages/browser/src/helpers/bufferToBase64URLString.test.ts create mode 100644 packages/browser/src/helpers/bufferToBase64URLString.ts delete mode 100644 packages/browser/src/helpers/toBase64String.test.ts delete mode 100644 packages/browser/src/helpers/toBase64String.ts (limited to 'packages/browser/src/helpers') diff --git a/packages/browser/src/helpers/base64URLStringToBuffer.ts b/packages/browser/src/helpers/base64URLStringToBuffer.ts new file mode 100644 index 0000000..baa258f --- /dev/null +++ b/packages/browser/src/helpers/base64URLStringToBuffer.ts @@ -0,0 +1,33 @@ +/** + * Convert from a Base64URL-encoded string to an Array Buffer. Best used when converting a + * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or + * excludeCredentials + * + * Helper method to compliment `bufferToBase64URLString` + */ +export default function base64URLStringToBuffer(base64URLString: string): ArrayBuffer { + // Convert from Base64URL to Base64 + const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); + /** + * Pad with '=' until it's a multiple of four + * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding + * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding + * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding + * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding + */ + const padLength = (4 - (base64.length % 4)) % 4; + const padded = base64.padEnd(base64.length + padLength, '='); + + // Convert to a binary string + const binary = atob(padded); + + // Convert binary string to buffer + const buffer = new ArrayBuffer(binary.length); + const bytes = new Uint8Array(buffer); + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return buffer; +} diff --git a/packages/browser/src/helpers/bufferToBase64URLString.test.ts b/packages/browser/src/helpers/bufferToBase64URLString.test.ts new file mode 100644 index 0000000..bbcb11b --- /dev/null +++ b/packages/browser/src/helpers/bufferToBase64URLString.test.ts @@ -0,0 +1,15 @@ +import toBase64String from './toBase64String'; + +import toUint8Array from './toUint8Array'; + +test('should convert a Buffer to a string with a length that is a multiple of 4', () => { + const base64 = toBase64String(Buffer.from('123456', 'ascii')); + + expect(base64.length % 4).toEqual(0); +}); + +test('should convert a Uint8Array to a string with a length that is a multiple of 4', () => { + const base64 = toBase64String(toUint8Array('123456')); + + expect(base64.length % 4).toEqual(0); +}); diff --git a/packages/browser/src/helpers/bufferToBase64URLString.ts b/packages/browser/src/helpers/bufferToBase64URLString.ts new file mode 100644 index 0000000..954f4b0 --- /dev/null +++ b/packages/browser/src/helpers/bufferToBase64URLString.ts @@ -0,0 +1,21 @@ +/** + * Convert the given array buffer into a Base64URL-encoded string. Ideal for converting various + * credential response ArrayBuffers to string for sending back to the server as JSON. + * + * Helper method to compliment `base64URLStringToBuffer` + */ +export default function bufferToBase64URLString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let str = ''; + + for (const charCode of bytes) { + str += String.fromCharCode(charCode); + } + + const base64String = btoa(str); + + return base64String + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} diff --git a/packages/browser/src/helpers/toBase64String.test.ts b/packages/browser/src/helpers/toBase64String.test.ts deleted file mode 100644 index bbcb11b..0000000 --- a/packages/browser/src/helpers/toBase64String.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import toBase64String from './toBase64String'; - -import toUint8Array from './toUint8Array'; - -test('should convert a Buffer to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(Buffer.from('123456', 'ascii')); - - expect(base64.length % 4).toEqual(0); -}); - -test('should convert a Uint8Array to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(toUint8Array('123456')); - - expect(base64.length % 4).toEqual(0); -}); diff --git a/packages/browser/src/helpers/toBase64String.ts b/packages/browser/src/helpers/toBase64String.ts deleted file mode 100644 index 3178695..0000000 --- a/packages/browser/src/helpers/toBase64String.ts +++ /dev/null @@ -1,6 +0,0 @@ -import base64js from 'base64-js'; - -export default function toBase64String(buffer: ArrayBuffer): string { - // TODO: Make sure converting buffer to Uint8Array() is correct - return base64js.fromByteArray(new Uint8Array(buffer)).replace(/\+/g, '-').replace(/\//g, '_'); -} -- cgit v1.2.3 From 451ae557b51875f6b0eedb055d5561f22ad4456f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 2 Jun 2020 11:52:12 -0700 Subject: Refactor to incorporate new helper --- packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'packages/browser/src/helpers') diff --git a/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts b/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts index c37fed0..8fad78b 100644 --- a/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts +++ b/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts @@ -1,16 +1,14 @@ -import base64js from 'base64-js'; import type { PublicKeyCredentialDescriptorJSON } from '@simplewebauthn/typescript-types'; +import base64URLStringToBuffer from './base64URLStringToBuffer'; + export default function toPublicKeyCredentialDescriptor( descriptor: PublicKeyCredentialDescriptorJSON, ): PublicKeyCredentialDescriptor { - // Make sure the Base64'd credential ID length is a multiple of 4 or else toByteArray will throw const { id } = descriptor; - const padLength = 4 - (id.length % 4); - const paddedId = id.padEnd(id.length + padLength, '='); return { ...descriptor, - id: base64js.toByteArray(paddedId), + id: base64URLStringToBuffer(id), }; } -- cgit v1.2.3 From ae19fbdf22fb13dd8848370ff201963850682db7 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 2 Jun 2020 12:52:52 -0700 Subject: Get unit tests passing again --- .../src/helpers/bufferToBase64URLString.test.ts | 15 ------- .../browser/src/methods/startAssertion.test.ts | 48 +++++++++++----------- .../browser/src/methods/startAttestation.test.ts | 29 ++++++------- 3 files changed, 39 insertions(+), 53 deletions(-) delete mode 100644 packages/browser/src/helpers/bufferToBase64URLString.test.ts (limited to 'packages/browser/src/helpers') diff --git a/packages/browser/src/helpers/bufferToBase64URLString.test.ts b/packages/browser/src/helpers/bufferToBase64URLString.test.ts deleted file mode 100644 index bbcb11b..0000000 --- a/packages/browser/src/helpers/bufferToBase64URLString.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import toBase64String from './toBase64String'; - -import toUint8Array from './toUint8Array'; - -test('should convert a Buffer to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(Buffer.from('123456', 'ascii')); - - expect(base64.length % 4).toEqual(0); -}); - -test('should convert a Uint8Array to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(toUint8Array('123456')); - - expect(base64.length % 4).toEqual(0); -}); diff --git a/packages/browser/src/methods/startAssertion.test.ts b/packages/browser/src/methods/startAssertion.test.ts index d106d05..669e8eb 100644 --- a/packages/browser/src/methods/startAssertion.test.ts +++ b/packages/browser/src/methods/startAssertion.test.ts @@ -1,13 +1,9 @@ -import base64js from 'base64-js'; - import { AssertionCredential, PublicKeyCredentialRequestOptionsJSON, } from '@simplewebauthn/typescript-types'; -import toUint8Array from '../helpers/toUint8Array'; import supportsWebauthn from '../helpers/supportsWebauthn'; -import toBase64String from '../helpers/toBase64String'; import startAssertion from './startAssertion'; @@ -16,16 +12,16 @@ jest.mock('../helpers/supportsWebauthn'); const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; const mockSupportsWebauthn = supportsWebauthn as jest.Mock; -const mockAuthenticatorData = toBase64String(toUint8Array('mockAuthenticatorData')); -const mockClientDataJSON = toBase64String(toUint8Array('mockClientDataJSON')); -const mockSignature = toBase64String(toUint8Array('mockSignature')); -const mockUserHandle = toBase64String(toUint8Array('mockUserHandle')); +const mockAuthenticatorData = 'mockAuthenticatorData'; +const mockClientDataJSON = 'mockClientDataJSON'; +const mockSignature = 'mockSignature'; +const mockUserHandle = 'mockUserHandle'; const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { challenge: 'fizz', allowCredentials: [ { - id: 'abcdefgfdnsdfunguisdfgs', + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', type: 'public-key', transports: ['nfc'], }, @@ -45,7 +41,10 @@ test('should convert options before passing to navigator.credentials.get(...)', mockNavigatorGet.mockImplementation( (): Promise => { return new Promise(resolve => { - resolve({ response: {} }); + resolve({ + response: {}, + getClientExtensionResults: () => ({}), + }); }); }, ); @@ -53,16 +52,17 @@ test('should convert options before passing to navigator.credentials.get(...)', await startAssertion(goodOpts1); const argsPublicKey = mockNavigatorGet.mock.calls[0][0].publicKey; - const credId = base64js.fromByteArray(argsPublicKey.allowCredentials[0].id); + const credId = argsPublicKey.allowCredentials[0].id; - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); - // Make sure the credential ID is a proper base64 with a length that's a multiple of 4 - expect(credId.length % 4).toEqual(0); + expect(JSON.stringify(argsPublicKey.challenge)).toEqual('{"0":102,"1":105,"2":122,"3":122}'); + // Make sure the credential ID is an ArrayBuffer with a length of 64 + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); done(); }); -test('should return base64-encoded response values', async done => { +test('should return base64url-encoded response values', async done => { mockSupportsWebauthn.mockReturnValue(true); mockNavigatorGet.mockImplementation( @@ -70,12 +70,12 @@ test('should return base64-encoded response values', async done => { return new Promise(resolve => { resolve({ id: 'foobar', - rawId: toUint8Array('foobar'), + rawId: Buffer.from('foobar', 'ascii'), response: { - authenticatorData: base64js.toByteArray(mockAuthenticatorData), - clientDataJSON: base64js.toByteArray(mockClientDataJSON), - signature: base64js.toByteArray(mockSignature), - userHandle: base64js.toByteArray(mockUserHandle), + authenticatorData: Buffer.from(mockAuthenticatorData, 'ascii'), + clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), + signature: Buffer.from(mockSignature, 'ascii'), + userHandle: Buffer.from(mockUserHandle, 'ascii'), }, getClientExtensionResults: () => ({}), type: 'webauthn.get', @@ -87,10 +87,10 @@ test('should return base64-encoded response values', async done => { const response = await startAssertion(goodOpts1); expect(response.rawId).toEqual('Zm9vYmFy'); - expect(response.response.authenticatorData).toEqual(mockAuthenticatorData); - expect(response.response.clientDataJSON).toEqual(mockClientDataJSON); - expect(response.response.signature).toEqual(mockSignature); - expect(response.response.userHandle).toEqual(mockUserHandle); + expect(response.response.authenticatorData).toEqual('bW9ja0F1dGhlbnRpY2F0b3JEYXRh'); + expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWVudERhdGFKU09O'); + expect(response.response.signature).toEqual('bW9ja1NpZ25hdHVyZQ'); + expect(response.response.userHandle).toEqual('bW9ja1VzZXJIYW5kbGU'); done(); }); diff --git a/packages/browser/src/methods/startAttestation.test.ts b/packages/browser/src/methods/startAttestation.test.ts index a54e8b8..926db40 100644 --- a/packages/browser/src/methods/startAttestation.test.ts +++ b/packages/browser/src/methods/startAttestation.test.ts @@ -1,5 +1,3 @@ -import base64js from 'base64-js'; - import { AttestationCredential, PublicKeyCredentialCreationOptionsJSON, @@ -38,7 +36,7 @@ const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { }, timeout: 1, excludeCredentials: [{ - id: 'authIdentifier', + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', type: 'public-key', transports: ['internal'], }], @@ -64,14 +62,17 @@ test('should convert options before passing to navigator.credentials.create(...) await startAttestation(goodOpts1); const argsPublicKey = mockNavigatorCreate.mock.calls[0][0].publicKey; + const credId = argsPublicKey.excludeCredentials[0].id; - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); - expect(argsPublicKey.user.id).toEqual(toUint8Array(goodOpts1.user.id)); - expect(argsPublicKey.excludeCredentials).toEqual([{ - id: base64js.toByteArray('authIdentifier=='), - type: 'public-key', - transports: ['internal'], - }]) + // Make sure challenge and user.id are converted to Buffers + expect(JSON.stringify(argsPublicKey.challenge)).toEqual('{"0":102,"1":105,"2":122,"3":122}'); + expect(JSON.stringify(argsPublicKey.user.id)).toEqual('{"0":53,"1":54,"2":55,"3":56}'); + + // Confirm construction of excludeCredentials array + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); + expect(argsPublicKey.excludeCredentials[0].type).toEqual('public-key'); + expect(argsPublicKey.excludeCredentials[0].transports).toEqual(['internal']); done(); }); @@ -86,8 +87,8 @@ test('should return base64-encoded response values', async done => { id: 'foobar', rawId: toUint8Array('foobar'), response: { - attestationObject: base64js.toByteArray(mockAttestationObject), - clientDataJSON: base64js.toByteArray(mockClientDataJSON), + attestationObject: Buffer.from(mockAttestationObject, 'ascii'), + clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), }, getClientExtensionResults: () => ({}), type: 'webauthn.create', @@ -99,8 +100,8 @@ test('should return base64-encoded response values', async done => { const response = await startAttestation(goodOpts1); expect(response.rawId).toEqual('Zm9vYmFy'); - expect(response.response.attestationObject).toEqual(mockAttestationObject); - expect(response.response.clientDataJSON).toEqual(mockClientDataJSON); + expect(response.response.attestationObject).toEqual('bW9ja0F0dGU'); + expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWU'); done(); }); -- cgit v1.2.3