Skip to content
Open
18 changes: 8 additions & 10 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ fileignoreconfig:
checksum: 23a560448f0b79f7cc399f042501c7517e9e341eff3d6f671fa3c892e030a0cc
- filename: machines/bleShare/scan/scanMachine.ts
checksum: 5f4a183db610fe0ffe36e3e1129d67221535335e4a866341a5b8759228ddee15
- filename: screens/PasscodeScreen.tsx
checksum: 5d3003027b245234f8c00bfc98836f1fb90a5d9525ffacf61c53f3d50954aa6a
- filename: screens/PasscodeScreen.tsx
checksum: 3330b9db9cfce407782e82b4fe3c0b60a9ecb0f9327af2b69971e254f9e52921
- filename: screens/Home/MyVcs/OtpVerificationModal.tsx
checksum: a99fe136d9d1348c541d413324d266e179ecad6a1fe6b6bd1e0b2856a5a6d6b5
- filename: screens/QrLogin/QrConsent.tsx
Expand Down Expand Up @@ -157,7 +153,7 @@ fileignoreconfig:
- filename: screens/Home/MyVcs/GetIdInputModal.tsx
checksum: 067664e19817d3aba98b0cfa68ef57a04958a982ee23b96b612e4bf466a73890
- filename: screens/PasscodeScreen.tsx
checksum: 9af4308001e7e260ffffd26955ca49df3b388e34686509371e23dd837b11d030
checksum: 265108bd4e8d0ce88cac9385a3699dd560f90d398da11e79f448c6b675edb29c
- filename: screens/Home/MyVcs/IdInputModal.tsx
checksum: b735aedc84164d03c199fe89ccbf9df58f8997bfb4f60e7058a22bfe952fda3e
- filename: .github/workflows/ui-automation.yml
Expand Down Expand Up @@ -241,9 +237,9 @@ fileignoreconfig:
- filename: __mocks__/react-native-keychain.mock.js
checksum: cc2798f843b776e70d957a4f7cdf47e199a29d9f6547fb9f59753108e8f252fa
- filename: machines/auth.ts
checksum: 46195ee93f9b7ac6794a06eeef5dee5ea242047a557af8bf95df04e1343d7b9e
checksum: a392268a0fad93743aa96d7bff5e137849c694753879c21c6f488731616e8d21
- filename: machines/auth.typegen.ts
checksum: f93996f750549f6de91c2a35c5817cfa85ad7b255ef36dea85224a0a154db188
checksum: fdfe81e775d9c2e50aec39e0018a16ac90f56c583bd8d6c31dc6f6664d919ac1
- filename: shared/commonUtil.generated.test.ts
checksum: 4ca5983bb133f8f02bfd52fd0cc49ce37b24b164f3ca78a03ad97bb353ed00a4
- filename: __mocks__/en.mock.json
Expand Down Expand Up @@ -612,7 +608,7 @@ fileignoreconfig:
- filename: shared/vcjs/verifyCredential.test.ts
checksum: 8fc85e2fa18bbbd78479e94bb763e3438c5c5b2d658ea8017013e34f64d221d0
- filename: screens/PasscodeScreen.test.tsx
checksum: d1c160afafad97a7c2f63c57dae99f4b541794d867029aed0148194f30d4575c
checksum: c9bb2108cb1d929bf2cbc1d872f7b410581db13ade05b8c06aab8725c26f9e9d
- filename: machines/Issuers/IssuersGuards.test.ts
checksum: 4a6042bbc9b5bb185561d36d91577f82daae98aaf9370644b5a7dd830a720fe1
- filename: screens/PasscodeScreenController.test.ts
Expand Down Expand Up @@ -654,7 +650,7 @@ fileignoreconfig:
- filename: components/VC/common/VCUtils.test.tsx
checksum: 8497b309c4baf43ec1dd97cea0ab293ba5be9fcaf04e462dc561ab3fd23b86e0
- filename: machines/auth.test.ts
checksum: bfdb750e84438326deff93d1e4165134b9e5205f9d38eb28866ea359bbd45235
checksum: 3a7d8df7e4b890c408280ae2a4c56d3ae7b55ff7650c14b6768ab03b552168d6
- filename: machines/biometrics.test.ts
checksum: 323804af0ef321041e88c6d9d63db2bab3b4b5afc130ca5375158b4f7c1c1a6e
- filename: screens/Home/MyVcs/__snapshots__/IdInputModal.test.tsx.snap
Expand All @@ -669,4 +665,6 @@ fileignoreconfig:
checksum: 45b35c6fe899aa7c7f1c76dfea43e76acd939c1752bfa94b9539d2a56c721647
- filename: components/Passcode.test.tsx
checksum: def7f8defeedf75a71425be27f4dcb77511435a39fc367f313c0213f273fadb5
version: "1.0"
- filename: screens/PasscodeScreenController.ts
checksum: 0b258cfcfd61f0491b0c0b723c0c5e230dcc333732d1ff891adeca2b0b8c3aba
version: "1.0"
16 changes: 16 additions & 0 deletions components/PasscodeVerify.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ jest.mock('./PinInput', () => ({
// Mock commonUtil
jest.mock('../shared/commonUtil', () => ({
hashData: jest.fn(() => Promise.resolve('hashed-value')),
encodePinHash: jest.fn(
(version: string, hash: string) => `${version}$${hash}`,
),
parsePinHash: jest.fn((stored: string) => {
const sep = stored.indexOf('$');
if (sep === -1 || !/^v\d+$/.test(stored.slice(0, sep))) {
return {version: 'v1', hash: stored};
}
return {version: stored.slice(0, sep), hash: stored.slice(sep + 1)};
}),
}));

// Mock constants
jest.mock('../shared/constants', () => ({
CURRENT_PIN_KDF_VERSION: 'v2',
PIN_KDF_PROFILES: {v1: {iterations: 5}, v2: {iterations: 2}},
}));

// Mock telemetry
Expand Down
42 changes: 35 additions & 7 deletions components/PasscodeVerify.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {PinInput} from './PinInput';
import {hashData} from '../shared/commonUtil';
import {argon2iConfig} from '../shared/constants';
import {encodePinHash, hashData, parsePinHash} from '../shared/commonUtil';
import {CURRENT_PIN_KDF_VERSION, PIN_KDF_PROFILES} from '../shared/constants';
import {
getErrorEventData,
sendErrorEvent,
Expand All @@ -22,18 +22,45 @@ export const PasscodeVerify: React.FC<PasscodeVerifyProps> = props => {
}
}, [isVerified]);

return <PinInput testID={props.testID} length={MAX_PIN} onDone={verify} autosubmit={true} />;
return (
<PinInput
testID={props.testID}
length={MAX_PIN}
onDone={verify}
autosubmit={true}
/>
);

async function verify(value: string) {
try {
const hashedPasscode = await hashData(value, props.salt, argon2iConfig);
if (props.passcode === hashedPasscode) {
setIsVerified(true);
} else {
const {version, hash: storedHash} = parsePinHash(props.passcode);
const config = PIN_KDF_PROFILES[version] ?? PIN_KDF_PROFILES.v1;
const candidate = await hashData(value, props.salt, config);

if (storedHash !== candidate) {
if (props.onError) {
props.onError(t('passcodeMismatchError'));
}
return;
}

if (version !== CURRENT_PIN_KDF_VERSION && props.onUpgrade) {
try {
const upgradedHash = await hashData(
value,
props.salt,
PIN_KDF_PROFILES[CURRENT_PIN_KDF_VERSION],
);
props.onUpgrade(encodePinHash(CURRENT_PIN_KDF_VERSION, upgradedHash));
} catch (upgradeError) {
console.warn(
'PIN hash upgrade failed, will retry next login',
upgradeError,
);
}
}

setIsVerified(true);
} catch (error) {
sendErrorEvent(
getErrorEventData(
Expand All @@ -51,6 +78,7 @@ interface PasscodeVerifyProps {
passcode: string;
onSuccess: () => void;
onError?: (error: string) => void;
onUpgrade?: (newHash: string) => void;
salt: string;
testID: string;
}
6 changes: 6 additions & 0 deletions machines/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ describe('AuthEvents', () => {
passcode: '9999',
});
});
it('should create UPGRADE_PASSCODE_HASH event with new versioned hash', () => {
expect(AuthEvents.UPGRADE_PASSCODE_HASH('v2$newhash')).toEqual({
type: 'UPGRADE_PASSCODE_HASH',
passcode: 'v2$newhash',
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
it('should create SETUP_BIOMETRICS event', () => {
expect(AuthEvents.SETUP_BIOMETRICS('bio')).toEqual({
type: 'SETUP_BIOMETRICS',
Expand Down
4 changes: 4 additions & 0 deletions machines/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const model = createModel(
{
events: {
SETUP_PASSCODE: (passcode: string) => ({passcode}),
UPGRADE_PASSCODE_HASH: (passcode: string) => ({passcode}),
SETUP_BIOMETRICS: (biometrics: string) => ({biometrics}),
CHANGE_METHOD: (isToggleFromSettings: boolean) => ({
isToggleFromSettings,
Expand Down Expand Up @@ -65,6 +66,9 @@ export const authMachine = model.createMachine(
SET_TOUR_GUIDE: {
actions: 'setTourGuide',
},
UPGRADE_PASSCODE_HASH: {
actions: ['setPasscode', 'storeContext'],
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
BIOMETRIC_CANCELLED: {
target: 'init',
},
Expand Down
120 changes: 70 additions & 50 deletions machines/auth.typegen.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,71 @@
// This file was automatically generated. Edits will be overwritten

// This file was automatically generated. Edits will be overwritten

export interface Typegen0 {
'@@xstate/typegen': true;
internalEvents: {
"": { type: "" };
"done.invoke.auth.authorized:invocation[0]": { type: "done.invoke.auth.authorized:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"done.invoke.auth.introSlider:invocation[0]": { type: "done.invoke.auth.introSlider:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." };
"xstate.init": { type: "xstate.init" };
};
invokeSrcNameMap: {
"generatePasscodeSalt": "done.invoke.auth.introSlider:invocation[0]";
"initializeFaceSdkModel": "done.invoke.auth.authorized:invocation[0]";
};
missingImplementations: {
actions: never;
delays: never;
guards: never;
services: never;
};
eventsCausingActions: {
"requestStoredContext": "BIOMETRIC_CANCELLED" | "xstate.init";
"setBiometrics": "SETUP_BIOMETRICS";
"setContext": "STORE_RESPONSE";
"setInitialDownloadDone": "INITIAL_DOWNLOAD_DONE";
"setIsToggleFromSettings": "CHANGE_METHOD";
"setLanguage": "SETUP_BIOMETRICS" | "SETUP_PASSCODE";
"setOnboardingDone": "ONBOARDING_DONE";
"setPasscode": "SETUP_PASSCODE";
"setPasscodeSalt": "done.invoke.auth.introSlider:invocation[0]";
"setTourGuide": "SET_TOUR_GUIDE";
"storeContext": "INITIAL_DOWNLOAD_DONE" | "ONBOARDING_DONE" | "SETUP_BIOMETRICS" | "SETUP_PASSCODE" | "STORE_RESPONSE" | "done.invoke.auth.authorized:invocation[0]" | "done.invoke.auth.introSlider:invocation[0]";
};
eventsCausingDelays: {

};
eventsCausingGuards: {
"hasBiometricSet": "";
"hasData": "STORE_RESPONSE";
"hasLanguageset": "";
"hasPasscodeSet": "";
};
eventsCausingServices: {
"generatePasscodeSalt": "SELECT";
"initializeFaceSdkModel": "LOGIN" | "SETUP_BIOMETRICS" | "SETUP_PASSCODE";
};
matchesStates: "authorized" | "checkingAuth" | "init" | "introSlider" | "languagesetup" | "savingDefaults" | "settingUp" | "unauthorized";
tags: never;
}

export interface Typegen0 {
'@@xstate/typegen': true;
internalEvents: {
'': {type: ''};
'done.invoke.auth.authorized:invocation[0]': {
type: 'done.invoke.auth.authorized:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.auth.introSlider:invocation[0]': {
type: 'done.invoke.auth.introSlider:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'xstate.init': {type: 'xstate.init'};
};
invokeSrcNameMap: {
generatePasscodeSalt: 'done.invoke.auth.introSlider:invocation[0]';
initializeFaceSdkModel: 'done.invoke.auth.authorized:invocation[0]';
};
missingImplementations: {
actions: never;
delays: never;
guards: never;
services: never;
};
eventsCausingActions: {
requestStoredContext: 'BIOMETRIC_CANCELLED' | 'xstate.init';
setBiometrics: 'SETUP_BIOMETRICS';
setContext: 'STORE_RESPONSE';
setInitialDownloadDone: 'INITIAL_DOWNLOAD_DONE';
setIsToggleFromSettings: 'CHANGE_METHOD';
setLanguage: 'SETUP_BIOMETRICS' | 'SETUP_PASSCODE';
setOnboardingDone: 'ONBOARDING_DONE';
setPasscode: 'SETUP_PASSCODE' | 'UPGRADE_PASSCODE_HASH';
setPasscodeSalt: 'done.invoke.auth.introSlider:invocation[0]';
setTourGuide: 'SET_TOUR_GUIDE';
storeContext:
| 'INITIAL_DOWNLOAD_DONE'
| 'ONBOARDING_DONE'
| 'SETUP_BIOMETRICS'
| 'SETUP_PASSCODE'
| 'STORE_RESPONSE'
| 'UPGRADE_PASSCODE_HASH'
| 'done.invoke.auth.authorized:invocation[0]'
| 'done.invoke.auth.introSlider:invocation[0]';
};
eventsCausingDelays: {};
eventsCausingGuards: {
hasBiometricSet: '';
hasData: 'STORE_RESPONSE';
hasLanguageset: '';
hasPasscodeSet: '';
};
eventsCausingServices: {
generatePasscodeSalt: 'SELECT';
initializeFaceSdkModel: 'LOGIN' | 'SETUP_BIOMETRICS' | 'SETUP_PASSCODE';
};
matchesStates:
| 'authorized'
| 'checkingAuth'
| 'init'
| 'introSlider'
| 'languagesetup'
| 'savingDefaults'
| 'settingUp'
| 'unauthorized';
tags: never;
}
8 changes: 8 additions & 0 deletions screens/PasscodeScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ jest.mock('./PasscodeScreenController', () => ({
setError: jest.fn(),
SETUP_PASSCODE: jest.fn(),
LOGIN: jest.fn(),
UPGRADE_PASSCODE_HASH: jest.fn(),
})),
}));
jest.mock('../shared/commonUtil', () => ({
hashData: jest.fn().mockResolvedValue('hashed'),
encodePinHash: jest.fn(
(version: string, hash: string) => `${version}$${hash}`,
),
parsePinHash: jest.fn((stored: string) => ({version: 'v1', hash: stored})),
__esModule: true,
default: (id: string) => ({accessibilityLabel: id, accessible: true}),
}));
jest.mock('../shared/constants', () => ({
argon2iConfig: {},
CURRENT_PIN_KDF_VERSION: 'v2',
PIN_KDF_PROFILES: {v1: {iterations: 5}, v2: {iterations: 2}},
isIOS: () => false,
}));
jest.mock('../shared/telemetry/TelemetryUtils', () => ({
Expand Down Expand Up @@ -110,6 +117,7 @@ describe('PasscodeScreen', () => {
setError: jest.fn(),
SETUP_PASSCODE: jest.fn(),
LOGIN: jest.fn(),
UPGRADE_PASSCODE_HASH: jest.fn(),
});
const {getByLabelText} = render(
React.createElement(PasscodeScreen, defaultProps),
Expand Down
17 changes: 13 additions & 4 deletions screens/PasscodeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import {Column, Text} from '../components/ui';
import {Theme} from '../components/ui/styleUtils';
import {PasscodeRouteProps} from '../routes';
import {usePasscodeScreen} from './PasscodeScreenController';
import {hashData} from '../shared/commonUtil';
import {argon2iConfig, isIOS} from '../shared/constants';
import {encodePinHash, hashData} from '../shared/commonUtil';
import {
CURRENT_PIN_KDF_VERSION,
PIN_KDF_PROFILES,
isIOS,
} from '../shared/constants';
import {
getEndEventData,
getEventType,
Expand Down Expand Up @@ -63,8 +67,12 @@ export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
}, []);

const setPasscode = async (passcode: string) => {
const data = await hashData(passcode, controller.storedSalt, argon2iConfig);
controller.setPasscode(data);
const rawHash = await hashData(
passcode,
controller.storedSalt,
PIN_KDF_PROFILES[CURRENT_PIN_KDF_VERSION],
);
controller.setPasscode(encodePinHash(CURRENT_PIN_KDF_VERSION, rawHash));
};

const handlePasscodeMismatch = (error: string) => {
Expand Down Expand Up @@ -166,6 +174,7 @@ export const PasscodeScreen: React.FC<PasscodeRouteProps> = props => {
controller.LOGIN();
}}
onError={handlePasscodeMismatch}
onUpgrade={controller.UPGRADE_PASSCODE_HASH}
passcode={controller.storedPasscode}
salt={controller.storedSalt}
/>
Expand Down
10 changes: 8 additions & 2 deletions screens/PasscodeScreenController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
sendEndEvent,
} from '../shared/telemetry/TelemetryUtils';
import {TelemetryConstants} from '../shared/telemetry/TelemetryConstants';
import { SettingsEvents } from '../machines/settings';
import {SettingsEvents} from '../machines/settings';

export function usePasscodeScreen(props: PasscodeRouteProps) {
const {appService} = useContext(GlobalContext);
Expand Down Expand Up @@ -57,7 +57,13 @@ export function usePasscodeScreen(props: PasscodeRouteProps) {

SETUP_PASSCODE: () => {
authService.send(AuthEvents.SETUP_PASSCODE(passcode));
settingsService?.send(SettingsEvents.TOGGLE_BIOMETRIC_UNLOCK(false,true))
settingsService?.send(
SettingsEvents.TOGGLE_BIOMETRIC_UNLOCK(false, true),
);
},

UPGRADE_PASSCODE_HASH: (newHash: string) => {
authService.send(AuthEvents.UPGRADE_PASSCODE_HASH(newHash));
},

storedSalt: useSelector(authService, selectPasscodeSalt),
Expand Down
Loading
Loading