|
2 | 2 | // (operations/index.native.ts), which checks the HSM biometric sensor. |
3 | 3 | import { |
4 | 4 | areLocalCredentialsKnownToServer, |
| 5 | + createCredential, |
5 | 6 | deviceCheckFailureReason, |
6 | 7 | deviceVerificationType, |
7 | 8 | doesDeviceSupportAuthenticationMethod, |
8 | 9 | } from '@components/MultifactorAuthentication/biometrics/operations'; |
9 | 10 |
|
| 11 | +import type {RegistrationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes'; |
10 | 12 | import VALUES from '@libs/MultifactorAuthentication/VALUES'; |
11 | 13 |
|
12 | 14 | import CONST from '@src/CONST'; |
13 | 15 | import ONYXKEYS from '@src/ONYXKEYS'; |
| 16 | +import Base64URL from '@src/utils/Base64URL'; |
14 | 17 |
|
15 | 18 | import Onyx from 'react-native-onyx'; |
16 | 19 | import waitForBatchedUpdates from 'tests/utils/waitForBatchedUpdates'; |
17 | 20 |
|
18 | 21 | const mockIsSensorAvailable = jest.fn(); |
19 | 22 | const mockGetAllKeys = jest.fn(); |
| 23 | +const mockCreateKeys = jest.fn(); |
20 | 24 |
|
21 | 25 | jest.mock('@sbaiahmed1/react-native-biometrics', () => ({ |
22 | 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return |
23 | 27 | isSensorAvailable: (...args: unknown[]) => mockIsSensorAvailable(...args), |
24 | 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return |
25 | 29 | getAllKeys: (...args: unknown[]) => mockGetAllKeys(...args), |
| 30 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-return |
| 31 | + createKeys: (...args: unknown[]) => mockCreateKeys(...args), |
26 | 32 | })); |
27 | 33 |
|
28 | 34 | const ACCOUNT_ID = 12345; |
29 | 35 | // The keystore returns the public key as plain base64 while the server stores base64url IDs, so the |
30 | 36 | // characters below only match after the module's base64url conversion. |
31 | 37 | const LOCAL_PUBLIC_KEY_BASE64 = 'Ab+/cd=='; |
32 | 38 | const LOCAL_CREDENTIAL_ID = 'Ab-_cd'; |
| 39 | +const REGISTRATION_CHALLENGE: RegistrationChallenge = { |
| 40 | + challenge: 'native-registration-challenge', |
| 41 | + rp: {id: 'expensify.com'}, |
| 42 | + user: {id: 'native-test-user', displayName: 'Native Test User'}, |
| 43 | + pubKeyCredParams: [{type: 'public-key', alg: -7}], |
| 44 | + timeout: 60000, |
| 45 | +}; |
33 | 46 |
|
34 | 47 | describe('biometrics operations (native)', () => { |
35 | 48 | beforeEach(() => { |
@@ -97,4 +110,49 @@ describe('biometrics operations (native)', () => { |
97 | 110 | await expect(areLocalCredentialsKnownToServer(ACCOUNT_ID)).resolves.toBe(false); |
98 | 111 | }); |
99 | 112 | }); |
| 113 | + |
| 114 | + // Mirrors the `register` cases in useNativeBiometricsHSM.test.ts, which move over here when that |
| 115 | + // hook is deleted. |
| 116 | + describe('createCredential', () => { |
| 117 | + beforeEach(() => { |
| 118 | + mockCreateKeys.mockResolvedValue({publicKey: LOCAL_PUBLIC_KEY_BASE64}); |
| 119 | + }); |
| 120 | + |
| 121 | + it('creates the HSM key with the account-specific alias', async () => { |
| 122 | + await createCredential({accountID: ACCOUNT_ID, registrationChallenge: REGISTRATION_CHALLENGE}); |
| 123 | + |
| 124 | + expect(mockCreateKeys).toHaveBeenCalledWith('12345_HSM_KEY', 'ec256', undefined, true, false); |
| 125 | + }); |
| 126 | + |
| 127 | + it('returns the exact NativeBiometricsHSMKeyInfo shape on success', async () => { |
| 128 | + const result = await createCredential({accountID: ACCOUNT_ID, registrationChallenge: REGISTRATION_CHALLENGE}); |
| 129 | + |
| 130 | + expect(result).toEqual({ |
| 131 | + success: true, |
| 132 | + keyInfo: { |
| 133 | + rawId: LOCAL_CREDENTIAL_ID, |
| 134 | + type: CONST.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_HSM_TYPE, |
| 135 | + response: { |
| 136 | + clientDataJSON: Base64URL.encode(JSON.stringify({challenge: REGISTRATION_CHALLENGE.challenge})), |
| 137 | + biometric: { |
| 138 | + publicKey: LOCAL_CREDENTIAL_ID, |
| 139 | + algorithm: CONST.COSE_ALGORITHM.ES256, |
| 140 | + }, |
| 141 | + }, |
| 142 | + }, |
| 143 | + }); |
| 144 | + }); |
| 145 | + |
| 146 | + it('returns a failed result with the mapped reason when the library throws', async () => { |
| 147 | + mockCreateKeys.mockRejectedValue(Object.assign(new Error('Key creation failed'), {code: 'CREATE_KEYS_ERROR'})); |
| 148 | + |
| 149 | + const result = await createCredential({accountID: ACCOUNT_ID, registrationChallenge: REGISTRATION_CHALLENGE}); |
| 150 | + |
| 151 | + expect(result.success).toBe(false); |
| 152 | + if (result.success) { |
| 153 | + throw new Error('Expected credential creation to fail'); |
| 154 | + } |
| 155 | + expect(result.error.reason).toBe(CONST.MULTIFACTOR_AUTHENTICATION.REASON.LOCAL_ERRORS.HSM.KEY_CREATION_FAILED); |
| 156 | + }); |
| 157 | + }); |
100 | 158 | }); |
0 commit comments