From c1641f92633ab535e11d4e05a23d0e11e29e4d20 Mon Sep 17 00:00:00 2001 From: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:30:37 +0530 Subject: [PATCH 1/4] fix(auth): upgrade PIN hashing to Argon2id v2 with migration (#2399) Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> --- .talismanrc | 18 ++--- components/PasscodeVerify.test.tsx | 16 ++++ components/PasscodeVerify.tsx | 42 ++++++++-- machines/auth.test.ts | 6 ++ machines/auth.ts | 4 + machines/auth.typegen.ts | 120 ++++++++++++++++------------ screens/PasscodeScreen.test.tsx | 8 ++ screens/PasscodeScreen.tsx | 17 +++- screens/PasscodeScreenController.ts | 10 ++- shared/commonUtil.test.ts | 36 +++++++++ shared/commonUtil.ts | 17 ++++ shared/constants.test.ts | 20 +++++ shared/constants.ts | 15 ++++ 13 files changed, 256 insertions(+), 73 deletions(-) diff --git a/.talismanrc b/.talismanrc index a3e5b21d88..d53a481a22 100644 --- a/.talismanrc +++ b/.talismanrc @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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" diff --git a/components/PasscodeVerify.test.tsx b/components/PasscodeVerify.test.tsx index 51b56c3b89..7639b2ae6f 100644 --- a/components/PasscodeVerify.test.tsx +++ b/components/PasscodeVerify.test.tsx @@ -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 diff --git a/components/PasscodeVerify.tsx b/components/PasscodeVerify.tsx index c02a788385..279d9b8f6c 100644 --- a/components/PasscodeVerify.tsx +++ b/components/PasscodeVerify.tsx @@ -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, @@ -22,18 +22,45 @@ export const PasscodeVerify: React.FC = props => { } }, [isVerified]); - return ; + return ( + + ); 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( @@ -51,6 +78,7 @@ interface PasscodeVerifyProps { passcode: string; onSuccess: () => void; onError?: (error: string) => void; + onUpgrade?: (newHash: string) => void; salt: string; testID: string; } diff --git a/machines/auth.test.ts b/machines/auth.test.ts index 860bb2aff9..3ad3912a2b 100644 --- a/machines/auth.test.ts +++ b/machines/auth.test.ts @@ -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', + }); + }); it('should create SETUP_BIOMETRICS event', () => { expect(AuthEvents.SETUP_BIOMETRICS('bio')).toEqual({ type: 'SETUP_BIOMETRICS', diff --git a/machines/auth.ts b/machines/auth.ts index 96f0c29ee5..f62ad22e57 100644 --- a/machines/auth.ts +++ b/machines/auth.ts @@ -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, @@ -65,6 +66,9 @@ export const authMachine = model.createMachine( SET_TOUR_GUIDE: { actions: 'setTourGuide', }, + UPGRADE_PASSCODE_HASH: { + actions: ['setPasscode', 'storeContext'], + }, BIOMETRIC_CANCELLED: { target: 'init', }, diff --git a/machines/auth.typegen.ts b/machines/auth.typegen.ts index 35ce4f08da..e6b3dd6153 100644 --- a/machines/auth.typegen.ts +++ b/machines/auth.typegen.ts @@ -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; - } - \ No newline at end of file +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; +} diff --git a/screens/PasscodeScreen.test.tsx b/screens/PasscodeScreen.test.tsx index 4c9f75068a..60cf72181f 100644 --- a/screens/PasscodeScreen.test.tsx +++ b/screens/PasscodeScreen.test.tsx @@ -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', () => ({ @@ -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), diff --git a/screens/PasscodeScreen.tsx b/screens/PasscodeScreen.tsx index fc50279d72..a07bc92884 100644 --- a/screens/PasscodeScreen.tsx +++ b/screens/PasscodeScreen.tsx @@ -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, @@ -63,8 +67,12 @@ export const PasscodeScreen: React.FC = 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) => { @@ -166,6 +174,7 @@ export const PasscodeScreen: React.FC = props => { controller.LOGIN(); }} onError={handlePasscodeMismatch} + onUpgrade={controller.UPGRADE_PASSCODE_HASH} passcode={controller.storedPasscode} salt={controller.storedSalt} /> diff --git a/screens/PasscodeScreenController.ts b/screens/PasscodeScreenController.ts index 40c2edf336..6990d0e56a 100644 --- a/screens/PasscodeScreenController.ts +++ b/screens/PasscodeScreenController.ts @@ -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); @@ -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), diff --git a/shared/commonUtil.test.ts b/shared/commonUtil.test.ts index ac9fd07e43..ed852c122b 100644 --- a/shared/commonUtil.test.ts +++ b/shared/commonUtil.test.ts @@ -1,5 +1,6 @@ import testIDProps, { bytesToMB, + encodePinHash, faceMatchConfig, generateBackupEncryptionKey, generateRandomString, @@ -7,6 +8,7 @@ import testIDProps, { getDriveName, getMaskedText, hashData, + parsePinHash, removeWhiteSpace, sleep, getRandomInt, @@ -38,6 +40,40 @@ describe('generateRandomString', () => { }); }); +describe('PIN hash version helpers', () => { + it('encodePinHash prefixes the hash with the version', () => { + expect(encodePinHash('v2', 'abcdef')).toBe('v2$abcdef'); + }); + + it('parsePinHash extracts version and hash from a v2 string', () => { + expect(parsePinHash('v2$abcdef')).toEqual({version: 'v2', hash: 'abcdef'}); + }); + + it('parsePinHash treats legacy unversioned hashes as v1', () => { + expect(parsePinHash('abcdefraw')).toEqual({ + version: 'v1', + hash: 'abcdefraw', + }); + }); + + it('parsePinHash treats empty stored hash as v1 (defensive)', () => { + expect(parsePinHash('')).toEqual({version: 'v1', hash: ''}); + }); + + it('parsePinHash treats unrecognized prefixes as legacy v1', () => { + // A hex hash that happens to contain "$" must not be misparsed. + expect(parsePinHash('deadbeef$cafebabe')).toEqual({ + version: 'v1', + hash: 'deadbeef$cafebabe', + }); + }); + + it('encodePinHash + parsePinHash round-trip', () => { + const encoded = encodePinHash('v2', 'roundtrip'); + expect(parsePinHash(encoded)).toEqual({version: 'v2', hash: 'roundtrip'}); + }); +}); + describe('generateBackupEncryptionKey', () => { it('generateBackupEncryptionKey should return Encrypted key', async () => { const BackupEncryptionKey = generateBackupEncryptionKey( diff --git a/shared/commonUtil.ts b/shared/commonUtil.ts index 3b1d9d455a..d4d0686bb1 100644 --- a/shared/commonUtil.ts +++ b/shared/commonUtil.ts @@ -31,6 +31,23 @@ export const generateRandomString = async () => { ); return randomString; }; + +export const encodePinHash = (version: string, rawHash: string): string => { + return `${version}$${rawHash}`; +}; + +export const parsePinHash = ( + stored: string, +): {version: string; hash: string} => { + if (!stored) { + return {version: 'v1', hash: stored}; + } + 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)}; +}; export const getRandomInt = (min, max) => { min = Math.ceil(min); max = Math.floor(max); diff --git a/shared/constants.test.ts b/shared/constants.test.ts index 908165ea02..34e828a96f 100644 --- a/shared/constants.test.ts +++ b/shared/constants.test.ts @@ -26,6 +26,8 @@ import { argon2iConfigForBackupFileName, argon2iConfigForPasswordAndPhoneNumber, argon2iSalt, + PIN_KDF_PROFILES, + CURRENT_PIN_KDF_VERSION, TECHNICAL_ERROR, NETWORK_REQUEST_FAILED, NO_INTERNET, @@ -220,6 +222,24 @@ describe('shared/constants', () => { }); }); + describe('PIN KDF profiles', () => { + it('CURRENT_PIN_KDF_VERSION points at v2', () => { + expect(CURRENT_PIN_KDF_VERSION).toBe('v2'); + }); + + it('v1 profile is the legacy argon2iConfig (kept for verifying old PIN hashes)', () => { + expect(PIN_KDF_PROFILES.v1).toBe(argon2iConfig); + }); + + it('v2 profile follows OWASP 2024 mobile guidance (memory-hard Argon2id)', () => { + const v2 = PIN_KDF_PROFILES.v2; + expect(v2.mode).toBe('argon2id'); + expect(v2.memory).toBeGreaterThanOrEqual(19 * 1024); + expect(v2.hashLength).toBeGreaterThanOrEqual(32); + expect(v2.iterations).toBeGreaterThanOrEqual(1); + }); + }); + describe('error strings', () => { it('should have correct error message constants', () => { expect(TECHNICAL_ERROR).toBe('Technical error'); diff --git a/shared/constants.ts b/shared/constants.ts index 18dabd32c6..ae6a3c14d4 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -121,6 +121,21 @@ export const argon2iConfig: Argon2iConfig = { mode: 'argon2i', }; +export const PIN_KDF_PROFILES: Record = { + v1: argon2iConfig, + v2: { + iterations: 2, + memory: 19 * 1024, + parallelism: 1, + hashLength: 32, + mode: 'argon2id', + }, +}; + +export type PinKdfVersion = 'v1' | 'v2'; +export const CURRENT_PIN_KDF_VERSION: PinKdfVersion = 'v2'; +export const PIN_HASH_VERSION_SEPARATOR = '$'; + export const argon2iConfigForUinVid: Argon2iConfig = { iterations: 5, memory: 16 * 1024, From c8b99d42e332c5abd9b5204816ee9e4673876e6c Mon Sep 17 00:00:00 2001 From: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:57:29 +0530 Subject: [PATCH 2/4] fix(auth): Refractor changes for PIN hashing to Argon2id v2 with migration (#2399) Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> --- .talismanrc | 4 ++-- machines/auth.test.ts | 52 +++++++++++++++++++++++++++++++++++++++++++ machines/auth.ts | 9 +++++--- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/.talismanrc b/.talismanrc index d53a481a22..f7f34b3ecd 100644 --- a/.talismanrc +++ b/.talismanrc @@ -237,7 +237,7 @@ fileignoreconfig: - filename: __mocks__/react-native-keychain.mock.js checksum: cc2798f843b776e70d957a4f7cdf47e199a29d9f6547fb9f59753108e8f252fa - filename: machines/auth.ts - checksum: a392268a0fad93743aa96d7bff5e137849c694753879c21c6f488731616e8d21 + checksum: 093028d0ddcc992b2be86d7e3e9c463779ccb038b03bc38944524c6824b6982a - filename: machines/auth.typegen.ts checksum: fdfe81e775d9c2e50aec39e0018a16ac90f56c583bd8d6c31dc6f6664d919ac1 - filename: shared/commonUtil.generated.test.ts @@ -650,7 +650,7 @@ fileignoreconfig: - filename: components/VC/common/VCUtils.test.tsx checksum: 8497b309c4baf43ec1dd97cea0ab293ba5be9fcaf04e462dc561ab3fd23b86e0 - filename: machines/auth.test.ts - checksum: 3a7d8df7e4b890c408280ae2a4c56d3ae7b55ff7650c14b6768ab03b552168d6 + checksum: 16bd9e0b244267e4d416061331c87317561779888d233c37c63d4909873f0007 - filename: machines/biometrics.test.ts checksum: 323804af0ef321041e88c6d9d63db2bab3b4b5afc130ca5375158b4f7c1c1a6e - filename: screens/Home/MyVcs/__snapshots__/IdInputModal.test.tsx.snap diff --git a/machines/auth.test.ts b/machines/auth.test.ts index 3ad3912a2b..6ef49a6a83 100644 --- a/machines/auth.test.ts +++ b/machines/auth.test.ts @@ -147,6 +147,58 @@ describe('AuthEvents', () => { passcode: 'v2$newhash', }); }); + it('UPGRADE_PASSCODE_HASH updates the stored hash and persists in unauthorized', () => { + // setPasscode is left as the real assign so we can assert the context + // update; storeContext is stubbed (it is a send to the store service). + const machine = authMachine.withConfig({ + actions: { + requestStoredContext: () => {}, + storeContext: () => {}, + setContext: () => {}, + setBiometrics: () => {}, + setLanguage: () => {}, + setAppSetupComplete: () => {}, + setPasscodeSalt: () => {}, + setOnboardingDone: () => {}, + setInitialDownloadDone: () => {}, + setTourGuide: () => {}, + setIsToggleFromSettings: () => {}, + } as any, + }); + const next = machine.transition( + 'unauthorized', + AuthEvents.UPGRADE_PASSCODE_HASH('v2$newhash'), + ); + expect(next.context.passcode).toBe('v2$newhash'); + expect(next.actions.map((a: any) => a.type)).toContain('storeContext'); + expect(next.value).toBe('unauthorized'); + }); + it('UPGRADE_PASSCODE_HASH is ignored once authorized (cannot overwrite hash)', () => { + const machine = authMachine.withConfig({ + services: { + initializeFaceSdkModel: () => () => {}, + } as any, + actions: { + requestStoredContext: () => {}, + storeContext: () => {}, + setContext: () => {}, + setBiometrics: () => {}, + setLanguage: () => {}, + setAppSetupComplete: () => {}, + setPasscodeSalt: () => {}, + setOnboardingDone: () => {}, + setInitialDownloadDone: () => {}, + setTourGuide: () => {}, + setIsToggleFromSettings: () => {}, + } as any, + }); + const next = machine.transition( + 'authorized', + AuthEvents.UPGRADE_PASSCODE_HASH('v2$attacker'), + ); + expect(next.context.passcode).not.toBe('v2$attacker'); + expect(next.changed).toBe(false); + }); it('should create SETUP_BIOMETRICS event', () => { expect(AuthEvents.SETUP_BIOMETRICS('bio')).toEqual({ type: 'SETUP_BIOMETRICS', diff --git a/machines/auth.ts b/machines/auth.ts index f62ad22e57..328a2d2a5e 100644 --- a/machines/auth.ts +++ b/machines/auth.ts @@ -66,9 +66,6 @@ export const authMachine = model.createMachine( SET_TOUR_GUIDE: { actions: 'setTourGuide', }, - UPGRADE_PASSCODE_HASH: { - actions: ['setPasscode', 'storeContext'], - }, BIOMETRIC_CANCELLED: { target: 'init', }, @@ -151,6 +148,12 @@ export const authMachine = model.createMachine( unauthorized: { on: { LOGIN: 'authorized', + // Transparent KDF migration: a legacy v1 hash is re-hashed to the + // current profile during unlock (before LOGIN), so the upgrade is + // only accepted here, not from arbitrary states. + UPGRADE_PASSCODE_HASH: { + actions: ['setPasscode', 'storeContext'], + }, }, }, authorized: { From e229fea0ee53b85b1a9305179c5c4ce4c5926ac3 Mon Sep 17 00:00:00 2001 From: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:58:51 +0530 Subject: [PATCH 3/4] fix(auth): use jest.fn() stubs in auth tests (#2399) Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> --- .talismanrc | 5 +- machines/auth.test.ts | 298 +++++++++++++++++++++--------------------- 2 files changed, 151 insertions(+), 152 deletions(-) diff --git a/.talismanrc b/.talismanrc index f3b8750d9d..e54d9a1913 100644 --- a/.talismanrc +++ b/.talismanrc @@ -650,7 +650,7 @@ fileignoreconfig: - filename: components/VC/common/VCUtils.test.tsx checksum: 8497b309c4baf43ec1dd97cea0ab293ba5be9fcaf04e462dc561ab3fd23b86e0 - filename: machines/auth.test.ts - checksum: 16bd9e0b244267e4d416061331c87317561779888d233c37c63d4909873f0007 + checksum: bbae76371694db0eebd5bba9f03ab1d10ba11a501b9a0f0e5aaf23b1018fdfda - filename: machines/biometrics.test.ts checksum: 323804af0ef321041e88c6d9d63db2bab3b4b5afc130ca5375158b4f7c1c1a6e - filename: screens/Home/MyVcs/__snapshots__/IdInputModal.test.tsx.snap @@ -667,7 +667,6 @@ fileignoreconfig: checksum: def7f8defeedf75a71425be27f4dcb77511435a39fc367f313c0213f273fadb5 - filename: screens/PasscodeScreenController.ts checksum: 0b258cfcfd61f0491b0c0b723c0c5e230dcc333732d1ff891adeca2b0b8c3aba -version: "1.0" - filename: components/DeeplinkBanner.tsx checksum: 5f94125bf4ffae22ff0bf5b819008b6c727ed87565479eab8739949714398d57 - version: "1.0" +version: "1.0" diff --git a/machines/auth.test.ts b/machines/auth.test.ts index 6ef49a6a83..6d396510df 100644 --- a/machines/auth.test.ts +++ b/machines/auth.test.ts @@ -152,17 +152,17 @@ describe('AuthEvents', () => { // update; storeContext is stubbed (it is a send to the store service). const machine = authMachine.withConfig({ actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition( @@ -176,20 +176,20 @@ describe('AuthEvents', () => { it('UPGRADE_PASSCODE_HASH is ignored once authorized (cannot overwrite hash)', () => { const machine = authMachine.withConfig({ services: { - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition( @@ -314,18 +314,18 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: {} as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('languagesetup', {type: 'SELECT'}); @@ -336,21 +336,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('introSlider', {type: 'NEXT'}); @@ -361,21 +361,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('settingUp', { @@ -389,21 +389,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('settingUp', { @@ -417,21 +417,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('unauthorized', {type: 'LOGIN'}); @@ -442,21 +442,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('authorized', {type: 'LOGOUT'}); @@ -467,21 +467,21 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: { generatePasscodeSalt: () => Promise.resolve('salt'), - initializeFaceSdkModel: () => () => {}, + initializeFaceSdkModel: () => jest.fn(), } as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, }); const next = machine.transition('authorized', { @@ -495,18 +495,18 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: {} as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, guards: { hasData: () => false, @@ -525,18 +525,18 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: {} as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, guards: { hasData: () => false, @@ -555,18 +555,18 @@ describe('authMachine definition', () => { const machine = authMachine.withConfig({ services: {} as any, actions: { - requestStoredContext: () => {}, - storeContext: () => {}, - setContext: () => {}, - setPasscode: () => {}, - setBiometrics: () => {}, - setLanguage: () => {}, - setAppSetupComplete: () => {}, - setPasscodeSalt: () => {}, - setOnboardingDone: () => {}, - setInitialDownloadDone: () => {}, - setTourGuide: () => {}, - setIsToggleFromSettings: () => {}, + requestStoredContext: jest.fn(), + storeContext: jest.fn(), + setContext: jest.fn(), + setPasscode: jest.fn(), + setBiometrics: jest.fn(), + setLanguage: jest.fn(), + setAppSetupComplete: jest.fn(), + setPasscodeSalt: jest.fn(), + setOnboardingDone: jest.fn(), + setInitialDownloadDone: jest.fn(), + setTourGuide: jest.fn(), + setIsToggleFromSettings: jest.fn(), } as any, guards: { hasData: () => false, From 450896fc9a73f7b6dc68b7ea33fbd851d0c3a963 Mon Sep 17 00:00:00 2001 From: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:40:25 +0530 Subject: [PATCH 4/4] fix(auth): address review findings - separator constant, hash error handling (#2399) Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com> --- .talismanrc | 2 +- components/PasscodeVerify.tsx | 7 +++++++ screens/PasscodeScreen.tsx | 21 +++++++++++++++------ shared/commonUtil.ts | 5 +++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.talismanrc b/.talismanrc index e54d9a1913..0d7a980d4e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -153,7 +153,7 @@ fileignoreconfig: - filename: screens/Home/MyVcs/GetIdInputModal.tsx checksum: 067664e19817d3aba98b0cfa68ef57a04958a982ee23b96b612e4bf466a73890 - filename: screens/PasscodeScreen.tsx - checksum: 265108bd4e8d0ce88cac9385a3699dd560f90d398da11e79f448c6b675edb29c + checksum: 85e96225e7923b81ecb94bffbeff15406bc634313991f413d1e5d0719be6682b - filename: screens/Home/MyVcs/IdInputModal.tsx checksum: b735aedc84164d03c199fe89ccbf9df58f8997bfb4f60e7058a22bfe952fda3e - filename: .github/workflows/ui-automation.yml diff --git a/components/PasscodeVerify.tsx b/components/PasscodeVerify.tsx index 279d9b8f6c..63abee6871 100644 --- a/components/PasscodeVerify.tsx +++ b/components/PasscodeVerify.tsx @@ -69,6 +69,13 @@ export const PasscodeVerify: React.FC = props => { error, ), ); + if (props.onError) { + props.onError( + t('passcodeVerifyError', { + defaultValue: 'Something went wrong. Please try again.', + }), + ); + } console.error('error while verifying passCode ', error); } } diff --git a/screens/PasscodeScreen.tsx b/screens/PasscodeScreen.tsx index a07bc92884..4fb4d4c6cf 100644 --- a/screens/PasscodeScreen.tsx +++ b/screens/PasscodeScreen.tsx @@ -67,12 +67,21 @@ export const PasscodeScreen: React.FC = props => { }, []); const setPasscode = async (passcode: string) => { - const rawHash = await hashData( - passcode, - controller.storedSalt, - PIN_KDF_PROFILES[CURRENT_PIN_KDF_VERSION], - ); - controller.setPasscode(encodePinHash(CURRENT_PIN_KDF_VERSION, rawHash)); + try { + const rawHash = await hashData( + passcode, + controller.storedSalt, + PIN_KDF_PROFILES[CURRENT_PIN_KDF_VERSION], + ); + controller.setPasscode(encodePinHash(CURRENT_PIN_KDF_VERSION, rawHash)); + } catch (error) { + controller.setError( + t('passcodeSetupError', { + defaultValue: 'Could not save passcode. Please try again.', + }), + ); + console.error('Failed to hash passcode during setup', error); + } }; const handlePasscodeMismatch = (error: string) => { diff --git a/shared/commonUtil.ts b/shared/commonUtil.ts index d4d0686bb1..1fcbbbdbb2 100644 --- a/shared/commonUtil.ts +++ b/shared/commonUtil.ts @@ -9,6 +9,7 @@ import { APPLE, FACE_SDK_MODEL_PATH, FACE_SDK_MODEL_CHECKSUM, + PIN_HASH_VERSION_SEPARATOR, } from './constants'; import {generateSecureRandom} from 'react-native-securerandom'; import forge from 'node-forge'; @@ -33,7 +34,7 @@ export const generateRandomString = async () => { }; export const encodePinHash = (version: string, rawHash: string): string => { - return `${version}$${rawHash}`; + return `${version}${PIN_HASH_VERSION_SEPARATOR}${rawHash}`; }; export const parsePinHash = ( @@ -42,7 +43,7 @@ export const parsePinHash = ( if (!stored) { return {version: 'v1', hash: stored}; } - const sep = stored.indexOf('$'); + const sep = stored.indexOf(PIN_HASH_VERSION_SEPARATOR); if (sep === -1 || !/^v\d+$/.test(stored.slice(0, sep))) { return {version: 'v1', hash: stored}; }