-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: Authentication bypass via biometric enrollment change #7351
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
Open
OtavioStasiak
wants to merge
38
commits into
develop
Choose a base branch
from
feat.authentication-bypass-via-biometric-enrollment-change
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 15 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
5a78af5
feat: install react-native-keychain
OtavioStasiak 8691f37
feat(biometric-trust): add enrollment-bound trust store and Option C …
OtavioStasiak 3a59438
feat(biometric-trust): invalidate biometric trust on enrollment change
OtavioStasiak d384c0c
feat(biometric-trust): show explanatory subtitle on enrollment-change…
OtavioStasiak c35c0c6
feat(biometric-trust): silent-bind migration for existing biometry us…
OtavioStasiak 7ec65a4
feat: i18n translation
OtavioStasiak f091568
podfile
OtavioStasiak 8b84535
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak 898b8aa
fix(biometric-trust): revert ScreenLockConfig toggle when enrol fails
OtavioStasiak 3dbb3e3
fix(screen-lock): defer modal resolve until close animation finishes
OtavioStasiak 1565c0d
feat: add e2e tests
OtavioStasiak d01f17a
chore: format code and fix lint issues
OtavioStasiak 76da4bc
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak f6f59cb
fix: e2e tests
OtavioStasiak 952f38a
fix: test flow
OtavioStasiak fe0d66b
refactor: encapsulate biometric trust state and speed up screen-lock E2E
OtavioStasiak 13136b6
refactor: route biometric enabled flag through trust store API
OtavioStasiak 68a257f
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak fdb2cc1
refactor(biometric-trust): encapsulate biometry toggle and clarify tr…
OtavioStasiak 256e90b
refactor(biometric-trust): type unlock outcome as discriminated union…
OtavioStasiak bc0bd7b
refactor(biometric-trust): remove dead mount-time auto-biometry and v…
OtavioStasiak 063ec62
fix(screen-lock): prompt biometry from behind the passcode modal to s…
OtavioStasiak 4e6a86e
fix(biometric-trust): mark install trust-initialized on enrol to clos…
OtavioStasiak 66ab2f8
fix(biometric-trust): restore biometry opt-in prompt on first-passcod…
OtavioStasiak 5969082
fix(biometric-trust): clear enabled flag on unavailable to fix iOS en…
OtavioStasiak b7e21c0
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak f9c2f86
chore: biometricTrustStore docs
OtavioStasiak 43c18f4
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak 80f688f
chore: format code and fix lint issues
OtavioStasiak ad26251
chore: doc improvement
OtavioStasiak dcff7c3
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak b566a47
chore: code improvements
OtavioStasiak fbfd4e2
chore: format code and fix lint issues
OtavioStasiak 050b438
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak 122bc6a
fix: persist passcode attempts across re-renders, guard toggle double…
OtavioStasiak 6bd28b6
fix: reset attempts deterministically on lockout expiry and handle mo…
OtavioStasiak 410d8b0
Merge branch 'develop' into feat.authentication-bypass-via-biometric-…
OtavioStasiak bb51c78
fix: passcode unlock and deep link cancellation regressions
OtavioStasiak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -263,5 +263,6 @@ output.utils = { | |
| post, | ||
| login, | ||
| getDeepLink, | ||
| createDM | ||
| createDM, | ||
| sleep | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| appId: ${APP_ID} | ||
| name: Screen Lock | ||
| onFlowStart: | ||
| - runFlow: '../../helpers/setup.yaml' | ||
| onFlowComplete: | ||
| - evalScript: ${output.utils.deleteCreatedUsers()} | ||
| tags: | ||
| - test-8 | ||
|
|
||
| --- | ||
| - evalScript: ${output.user = output.utils.createUser()} | ||
| - runFlow: | ||
| file: '../../helpers/login-with-deeplink.yaml' | ||
| env: | ||
| USERNAME: ${output.user.username} | ||
| PASSWORD: ${output.user.password} | ||
|
|
||
| # Navigate to Screen Lock config | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'rooms-list-view' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'rooms-list-view-sidebar' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'sidebar-settings' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'sidebar-settings' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'settings-view-security-privacy' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'settings-view-security-privacy' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'security-privacy-view-screen-lock' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'security-privacy-view-screen-lock' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'screen-lock-config-view' | ||
| timeout: 60000 | ||
|
|
||
| # Enable "Unlock with passcode" -> opens PasscodeChoose | ||
| - tapOn: | ||
| id: 'screen-lock-config-view-auto-lock' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'passcode-button-1' | ||
| timeout: 60000 | ||
|
|
||
| # Choose passcode 123456 | ||
| - tapOn: | ||
| id: 'passcode-button-1' | ||
| - tapOn: | ||
| id: 'passcode-button-2' | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
|
|
||
| # Confirm passcode 123456 | ||
| - extendedWaitUntil: | ||
| visible: | ||
| text: 'Confirm your new passcode' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-1' | ||
| - tapOn: | ||
| id: 'passcode-button-2' | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
|
|
||
| # Back on Screen Lock config; choose "After 1 minute" | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'screen-lock-config-view-auto-lock-time-60' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'screen-lock-config-view-auto-lock-time-60' | ||
|
|
||
| # Background the app, wait past the auto-lock interval, relaunch | ||
| - pressKey: Home | ||
| - evalScript: ${output.utils.sleep(60000)} | ||
| - launchApp: | ||
| appId: ${APP_ID} | ||
|
|
||
| # Screen Lock modal must appear; unlock with current passcode | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'passcode-button-1' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-1' | ||
| - tapOn: | ||
| id: 'passcode-button-2' | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
|
|
||
| # After unlock, we land back on Screen Lock config. Tap "Change passcode" | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'rooms-list-view' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'rooms-list-view-sidebar' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'sidebar-settings' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'sidebar-settings' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'settings-view-security-privacy' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'settings-view-security-privacy' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'security-privacy-view-screen-lock' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'security-privacy-view-screen-lock' | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'screen-lock-config-view' | ||
| timeout: 60000 | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'screen-lock-config-view-change-passcode' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'screen-lock-config-view-change-passcode' | ||
|
|
||
| # Re-authenticate with current passcode (autoLock is on -> handleLocalAuthentication) | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'passcode-button-1' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-1' | ||
| - tapOn: | ||
| id: 'passcode-button-2' | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
|
|
||
| # Now the ChangePasscodeView (PasscodeChoose) opens; choose new passcode 3455678 | ||
| - extendedWaitUntil: | ||
| visible: | ||
| text: 'Choose your new passcode' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
| - tapOn: | ||
| id: 'passcode-button-7' | ||
| - tapOn: | ||
| id: 'passcode-button-8' | ||
|
|
||
| # Confirm new passcode 3455678 | ||
| - extendedWaitUntil: | ||
| visible: | ||
| text: 'Confirm your new passcode' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
| - tapOn: | ||
| id: 'passcode-button-7' | ||
| - tapOn: | ||
| id: 'passcode-button-8' | ||
|
|
||
| # Modal closes; we are back on the Screen Lock config screen | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'screen-lock-config-view' | ||
| timeout: 60000 | ||
| - assertVisible: | ||
| id: 'screen-lock-config-view-change-passcode' | ||
|
|
||
| # Background the app, wait past the auto-lock interval, relaunch | ||
| - pressKey: Home | ||
| - evalScript: ${output.utils.sleep(60000)} | ||
| - launchApp: | ||
| appId: ${APP_ID} | ||
|
|
||
| # Screen Lock modal must appear; unlock with current passcode | ||
| - extendedWaitUntil: | ||
| visible: | ||
| id: 'passcode-button-3' | ||
| timeout: 60000 | ||
| - tapOn: | ||
| id: 'passcode-button-3' | ||
| - tapOn: | ||
| id: 'passcode-button-4' | ||
| - tapOn: | ||
| id: 'passcode-button-5' | ||
| - tapOn: | ||
| id: 'passcode-button-6' | ||
| - tapOn: | ||
| id: 'passcode-button-7' | ||
| - tapOn: | ||
| id: 'passcode-button-8' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import React from 'react'; | ||
| import { fireEvent, render, waitFor } from '@testing-library/react-native'; | ||
|
|
||
| import PasscodeEnter from './PasscodeEnter'; | ||
| import { biometryAuth } from '../../lib/methods/helpers/localAuthentication'; | ||
| import { biometricTrustStore } from '../../lib/biometricTrustStore'; | ||
| import UserPreferences from '../../lib/methods/userPreferences'; | ||
| import { BIOMETRY_ENABLED_KEY } from '../../lib/constants/localAuthentication'; | ||
|
|
||
| jest.mock('../../lib/methods/helpers/localAuthentication', () => ({ | ||
| biometryAuth: jest.fn(), | ||
| resetAttempts: jest.fn(() => Promise.resolve()) | ||
| })); | ||
|
|
||
| jest.mock('../../lib/biometricTrustStore', () => ({ | ||
| biometricTrustStore: { | ||
| enrol: jest.fn(), | ||
| disenrol: jest.fn(() => Promise.resolve()), | ||
| verify: jest.fn(), | ||
| probeExists: jest.fn() | ||
| } | ||
| })); | ||
|
|
||
| jest.mock('../../lib/methods/userPreferences', () => ({ | ||
| __esModule: true, | ||
| default: { | ||
| getBool: jest.fn(), | ||
| setBool: jest.fn(), | ||
| getString: jest.fn(), | ||
| setString: jest.fn() | ||
| }, | ||
| useUserPreferences: () => [null, jest.fn()] | ||
| })); | ||
|
|
||
| jest.mock('../../i18n', () => ({ t: (key: string) => key })); | ||
|
|
||
| const mockedBiometryAuth = biometryAuth as jest.Mock; | ||
| const mockedDisenrol = biometricTrustStore.disenrol as jest.Mock; | ||
| const mockedSetBool = UserPreferences.setBool as jest.Mock; | ||
|
|
||
| describe('PasscodeEnter biometry button', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockedDisenrol.mockResolvedValue(undefined); | ||
| }); | ||
|
|
||
| it('enrollmentChanged from button press → disenrols, clears flag, hides biometry button', async () => { | ||
| mockedBiometryAuth.mockResolvedValueOnce({ kind: 'enrollmentChanged' }); | ||
| const finishProcess = jest.fn(); | ||
|
|
||
| const { getByTestId, queryByTestId } = render(<PasscodeEnter hasBiometry skipAutoBiometry finishProcess={finishProcess} />); | ||
|
|
||
| await waitFor(() => expect(getByTestId('biometry-button')).toBeTruthy()); | ||
|
|
||
| fireEvent.press(getByTestId('biometry-button')); | ||
|
|
||
| await waitFor(() => expect(mockedDisenrol).toHaveBeenCalledTimes(1)); | ||
| expect(mockedSetBool).toHaveBeenCalledWith(BIOMETRY_ENABLED_KEY, false); | ||
| expect(finishProcess).not.toHaveBeenCalled(); | ||
| await waitFor(() => expect(queryByTestId('biometry-button')).toBeNull()); | ||
| }); | ||
|
|
||
| it('success from button press → finishes process, no invalidation', async () => { | ||
| mockedBiometryAuth.mockResolvedValueOnce({ kind: 'success' }); | ||
| const finishProcess = jest.fn(); | ||
|
|
||
| const { getByTestId } = render(<PasscodeEnter hasBiometry skipAutoBiometry finishProcess={finishProcess} />); | ||
|
|
||
| await waitFor(() => expect(getByTestId('biometry-button')).toBeTruthy()); | ||
|
|
||
| fireEvent.press(getByTestId('biometry-button')); | ||
|
|
||
| await waitFor(() => expect(finishProcess).toHaveBeenCalledTimes(1)); | ||
| expect(mockedDisenrol).not.toHaveBeenCalled(); | ||
| expect(mockedSetBool).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('canceled from button press → flag untouched, biometry button stays', async () => { | ||
| mockedBiometryAuth.mockResolvedValueOnce({ kind: 'canceled' }); | ||
| const finishProcess = jest.fn(); | ||
|
|
||
| const { getByTestId } = render(<PasscodeEnter hasBiometry skipAutoBiometry finishProcess={finishProcess} />); | ||
|
|
||
| await waitFor(() => expect(getByTestId('biometry-button')).toBeTruthy()); | ||
|
|
||
| fireEvent.press(getByTestId('biometry-button')); | ||
|
|
||
| await waitFor(() => expect(mockedBiometryAuth).toHaveBeenCalled()); | ||
| expect(mockedDisenrol).not.toHaveBeenCalled(); | ||
| expect(mockedSetBool).not.toHaveBeenCalled(); | ||
| expect(finishProcess).not.toHaveBeenCalled(); | ||
| expect(getByTestId('biometry-button')).toBeTruthy(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('PasscodeEnter enrollmentChanged subtitle', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('renders explanatory subtitle when reason === "enrollmentChanged"', () => { | ||
| const { getByText } = render( | ||
| <PasscodeEnter hasBiometry={false} skipAutoBiometry reason='enrollmentChanged' finishProcess={jest.fn()} /> | ||
| ); | ||
|
|
||
| expect(getByText('Local_authentication_biometric_enrollment_changed')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('does not render subtitle when reason is undefined', () => { | ||
| const { queryByText } = render(<PasscodeEnter hasBiometry={false} skipAutoBiometry finishProcess={jest.fn()} />); | ||
|
|
||
| expect(queryByText('Local_authentication_biometric_enrollment_changed')).toBeNull(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.