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

biometric-ed25519 bug fix on supporting Bitwarden #1339

Merged
merged 8 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-coats-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@near-js/biometric-ed25519": patch
---

Bug fix on createKey and getKeys
6 changes: 4 additions & 2 deletions packages/biometric-ed25519/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"build": "tsc -p ./tsconfig.json",
"lint:ts": "eslint -c ../../.eslintrc.ts.yml src/**/*.ts --no-eslintrc",
"lint:ts:fix": "eslint -c ../../.eslintrc.ts.yml src/**/*.ts --no-eslintrc --fix"
"lint:ts:fix": "eslint -c ../../.eslintrc.ts.yml src/**/*.ts --no-eslintrc --fix",
"test": "jest test"
},
"keywords": [],
"author": "Pagoda",
Expand All @@ -24,6 +25,7 @@
"fido2-lib": "3.4.1"
},
"devDependencies": {
"@types/node": "18.11.18"
"@types/node": "18.11.18",
"jest": "26.0.1"
}
}
9 changes: 5 additions & 4 deletions packages/biometric-ed25519/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
publicKeyCredentialToJSON,
recoverPublicKey,
uint8ArrayToBigInt,
convertToArrayBuffer
sanitizeCreateKeyResponse,
sanitizeGetKeyResponse
} from './utils';
import { Fido2 } from './fido2';
import { AssertionResponse } from './index.d';
Expand Down Expand Up @@ -64,7 +65,7 @@ export const createKey = async (username: string): Promise<KeyPair> => {
throw new PasskeyProcessCanceled('Failed to retrieve response from navigator.credentials.create');
}

const sanitizedResponse = convertToArrayBuffer(res);
const sanitizedResponse = sanitizeCreateKeyResponse(res);

const result = await f2l.attestation({
clientAttestationResponse: sanitizedResponse,
Expand Down Expand Up @@ -95,8 +96,8 @@ export const getKeys = async (username: string): Promise<[KeyPair, KeyPair]> =>

setBufferIfUndefined();
return navigator.credentials.get({ publicKey })
.then(async (response: Credential) => {
const sanitizedResponse = convertToArrayBuffer(response);
.then(async (response) => {
const sanitizedResponse = sanitizeGetKeyResponse(response);
const getAssertionResponse: AssertionResponse = publicKeyCredentialToJSON(sanitizedResponse);
const signature = base64.toArrayBuffer(getAssertionResponse.response.signature, true);

Expand Down
76 changes: 67 additions & 9 deletions packages/biometric-ed25519/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,75 @@ export const uint8ArrayToBigInt = (uint8Array: Uint8Array) => {
return BigInt('0x' + array.map(byte => byte.toString(16).padStart(2, '0')).join(''));
};

// This function is tries converts Uint8Array, Array or object to ArrayBuffer. Returns the original object if it doesn't match any of the aforementioned types.
export const convertToArrayBuffer = (obj) => {
const convertUint8ArrayToArrayBuffer = (obj: any) => {
if (obj instanceof Uint8Array) {
return obj.buffer.slice(obj.byteOffset, obj.byteOffset + obj.byteLength);
} else if (Array.isArray(obj)) {
return obj.map(convertToArrayBuffer);
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
acc[key] = convertToArrayBuffer(obj[key]);
return acc;
}, {});
}
return obj;
};

// This function is used to sanitize the response from navigator.credentials.create(), seeking for any Uint8Array and converting them to ArrayBuffer
// This function has multiple @ts-ignore because types are not up to date with standard type below:
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse
// an AuthenticatorAttestationResponse (when the PublicKeyCredential is created via CredentialsContainer.create())
export const sanitizeCreateKeyResponse = (res: Credential) => {
if (res instanceof PublicKeyCredential && (
res.rawId instanceof Uint8Array ||
res.response.clientDataJSON instanceof Uint8Array ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - attestationObject is not defined in Credential
res.response.attestationObject instanceof Uint8Array
)) {
return {
...res,
rawId: convertUint8ArrayToArrayBuffer(res.rawId),
response: {
...res.response,
clientDataJSON: convertUint8ArrayToArrayBuffer(res.response.clientDataJSON),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - attestationObject is not defined in Credential
attestationObject: convertUint8ArrayToArrayBuffer(res.response.attestationObject),
}
};
}
return res;
};

// This function is used to sanitize the response from navigator.credentials.get(), seeking for any Uint8Array and converting them to ArrayBuffer
// This function has multiple @ts-ignore because types are not up to date with standard type below:
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse
// an AuthenticatorAssertionResponse (when the PublicKeyCredential is obtained via CredentialsContainer.get()).
export const sanitizeGetKeyResponse = (res: Credential) => {
if (res instanceof PublicKeyCredential && (
res.rawId instanceof Uint8Array ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - authenticatorData is not defined in Credential
res.response.authenticatorData instanceof Uint8Array ||
res.response.clientDataJSON instanceof Uint8Array ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - signature is not defined in Credential
res.response.signature instanceof Uint8Array ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - userHandle is not defined in Credential
res.response.userHandle instanceof Uint8Array
)) {
return {
...res,
rawId: convertUint8ArrayToArrayBuffer(res.rawId),
response: {
...res.response,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - authenticatorData is not defined in Credential
authenticatorData: convertUint8ArrayToArrayBuffer(res.response.authenticatorData),
clientDataJSON: convertUint8ArrayToArrayBuffer(res.response.clientDataJSON),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - signature is not defined in Credential
signature: convertUint8ArrayToArrayBuffer(res.response.signature),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - userHandle is not defined in Credential
userHandle: convertUint8ArrayToArrayBuffer(res.response.userHandle),
}
};
}
return res;
};
111 changes: 111 additions & 0 deletions packages/biometric-ed25519/test/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const { sanitizeCreateKeyResponse, sanitizeGetKeyResponse } = require("../lib/utils");

// Define a mock PublicKeyCredential
class PublicKeyCredentialMock {
constructor({
rawId,
clientDataJSON,
attestationObject,
authenticatorData,
signature,
userHandle
}) {
this.rawId = rawId;
this.response = {
clientDataJSON,
...(attestationObject ? { attestationObject } : {}),
...(signature ? { signature } : {}),
...(userHandle ? { userHandle } : {}),
...(authenticatorData ? { authenticatorData } : {}),
};
}
}

// Define global PublicKeyCredential to make it available during tests
global.PublicKeyCredential = PublicKeyCredentialMock;

jest.mock('../lib/utils', () => {
const originalModule = jest.requireActual('../lib/utils');
return {
...originalModule,
convertUint8ArrayToArrayBuffer: jest.fn().mockImplementation(input => input.buffer),
};
});

describe('sanitizeCreateKeyResponse', () => {
it('should convert Uint8Array properties to ArrayBuffer for PublicKeyCredential', () => {
const mockCredential = new PublicKeyCredentialMock({
rawId: new Uint8Array([10, 20, 30]),
clientDataJSON: new Uint8Array([40, 50, 60]),
attestationObject: new Uint8Array([70, 80, 90]),
});


const result = sanitizeCreateKeyResponse(mockCredential);
expect(result.rawId.constructor.name).toBe('ArrayBuffer');
expect(result.response.clientDataJSON.constructor.name).toBe('ArrayBuffer');
expect(result.response.attestationObject.constructor.name).toBe('ArrayBuffer');
});

it('should return the input unchanged if not PublicKeyCredential or without Uint8Arrays', () => {
const mockCredential = new PublicKeyCredentialMock({
rawId: [10, 20, 30],
clientDataJSON: [40, 50, 60],
attestationObject: [70, 80, 90],
});

const result = sanitizeCreateKeyResponse(mockCredential);
expect(result).toEqual(mockCredential);
});

it('should handle non-PublicKeyCredential input gracefully', () => {
const nonPublicKeyCredential = {
someProp: 'test'
}; // No casting needed

const result = sanitizeCreateKeyResponse(nonPublicKeyCredential);
expect(result).toEqual(nonPublicKeyCredential);
});
});

describe('sanitizeGetKeyResponse', () => {
it('should convert Uint8Array properties to ArrayBuffer in PublicKeyCredential', () => {
const mockCredential = new PublicKeyCredentialMock({
rawId: new Uint8Array([10, 20, 30]),
clientDataJSON: new Uint8Array([40, 50, 60]),
authenticatorData: new Uint8Array([70, 80, 90]),
signature: new Uint8Array([100, 110, 120]),
userHandle: new Uint8Array([130, 140, 150])
});

const result = sanitizeGetKeyResponse(mockCredential);
expect(result.rawId.constructor.name).toBe('ArrayBuffer');
expect(result.response.clientDataJSON.constructor.name).toBe('ArrayBuffer');
expect(result.response.authenticatorData.constructor.name).toBe('ArrayBuffer');
expect(result.response.signature.constructor.name).toBe('ArrayBuffer');
expect(result.response.userHandle.constructor.name).toBe('ArrayBuffer');
});

it('should return the input unchanged if it does not meet conversion criteria', () => {
const mockCredential = new PublicKeyCredentialMock({
rawId: [10, 20, 30],
clientDataJSON: [40, 50, 60],
authenticatorData: [70, 80, 90],
signature: [100, 110, 120],
userHandle: [130, 140, 150]
});


const result = sanitizeGetKeyResponse(mockCredential);
expect(result).toEqual(mockCredential);
});

it('should handle non-PublicKeyCredential input gracefully', () => {
const nonPublicKeyCredential = {
someProp: 'test value'
};

const result = sanitizeGetKeyResponse(nonPublicKeyCredential);
expect(result).toEqual(nonPublicKeyCredential);
});
});
Loading
Loading