Skip to content

fix(auth): upgrade PIN hashing to Argon2id v2 with migration (#2399) - #2464

Open
Kaushikgupta469 wants to merge 7 commits into
inji:developfrom
Kaushikgupta469:Pin-Lockout-Fix
Open

fix(auth): upgrade PIN hashing to Argon2id v2 with migration (#2399)#2464
Kaushikgupta469 wants to merge 7 commits into
inji:developfrom
Kaushikgupta469:Pin-Lockout-Fix

Conversation

@Kaushikgupta469

@Kaushikgupta469 Kaushikgupta469 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Replace weak Argon2i PIN hashing (iterations: 5) with Argon2id v2 (memory-hard, OWASP 2024). Existing users with legacy hashes are silently migrated to v2 on next successful login

Issue ticket number and link
Relates #2399

Summary by CodeRabbit

  • New Features
    • Passcode verification now supports versioned hashes and transparently upgrades older saved passcodes to the current hashing format.
    • Passcode setup now coordinates biometric unlock behavior during initial setup.
  • Bug Fixes
    • Improved passcode setup failure handling with clearer user-facing errors and error logging.
  • Tests
    • Expanded coverage for versioned PIN hashing/parsing, KDF profile selection, verification/upgrade flows, and related auth/passcode controller events.

Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d251ab37-0cea-4ec0-952c-371d2c6fe3d6

📥 Commits

Reviewing files that changed from the base of the PR and between e229fea and 8623675.

📒 Files selected for processing (4)
  • .talismanrc
  • components/PasscodeVerify.tsx
  • screens/PasscodeScreen.tsx
  • shared/commonUtil.ts
✅ Files skipped from review due to trivial changes (1)
  • .talismanrc
🚧 Files skipped from review as they are similar to previous changes (3)
  • shared/commonUtil.ts
  • screens/PasscodeScreen.tsx
  • components/PasscodeVerify.tsx

Walkthrough

This PR adds PIN key-derivation versioning and helpers, updates verification to detect and upgrade older KDF hashes, exposes an auth-machine event to persist upgrades, wires upgrade callbacks through the passcode screen and controller, updates tests/mocks, and refreshes talisman checksums.

Changes

PIN Hash Versioning and Upgrade Flow

Layer / File(s) Summary
PIN KDF constants and versioned hash utilities
shared/constants.ts, shared/constants.test.ts, shared/commonUtil.ts, shared/commonUtil.test.ts
PIN_KDF_PROFILES maps v1 (legacy argon2i) and v2 (argon2id) configurations; CURRENT_PIN_KDF_VERSION set to v2; PIN_HASH_VERSION_SEPARATOR and helpers encodePinHash/parsePinHash (using $) added with tests.
PasscodeVerify component hash verification and upgrade
components/PasscodeVerify.tsx, components/PasscodeVerify.test.tsx
PasscodeVerify parses versioned hashes, verifies using the profile for the stored version, returns early on mismatch (calling onError), and when verification succeeds but version is old computes an upgraded encoded hash and calls onUpgrade. Prop onUpgrade?: (newHash: string) => void added.
PasscodeScreen setup with versioned hash storage
screens/PasscodeScreen.tsx, screens/PasscodeScreen.test.tsx
PasscodeScreen hashes new passcodes with the current KDF profile and stores an encoded versioned hash; passes onUpgrade to PasscodeVerify. Test mocks extended to cover new helpers and constants.
PasscodeScreenController upgrade event wiring
screens/PasscodeScreenController.ts
Controller imports SettingsEvents, dispatches TOGGLE_BIOMETRIC_UNLOCK during setup, and exposes UPGRADE_PASSCODE_HASH(newHash) to send AuthEvents.UPGRADE_PASSCODE_HASH to authService.
Auth state machine UPGRADE_PASSCODE_HASH event and transitions
machines/auth.ts, machines/auth.test.ts, machines/auth.typegen.ts
Adds UPGRADE_PASSCODE_HASH event with passcode payload; in unauthorized state it runs setPasscode and schedules storeContext. Tests added for event shape, transition behavior, and ignored handling when already authorized. Typegen formatting updated.
Auth machine test stub updates for consistency
machines/auth.test.ts
Multiple authMachine transition tests update their withConfig action/invoke stubs from no-op functions to jest.fn() mocks for more precise test isolation.
Configuration checksum updates
.talismanrc
Updated fileignoreconfig checksums for modified files: machines/auth.ts, machines/auth.typegen.ts, screens/PasscodeScreen.test.tsx, and machines/auth.test.ts.

Sequence Diagram

sequenceDiagram
  participant User
  participant PasscodeScreen
  participant PasscodeVerify
  participant PasscodeScreenController
  participant authService
  participant commonUtil as encodePinHash/parsePinHash
  participant PIN_KDF as PIN_KDF_PROFILES

  User->>PasscodeScreen: unlock with stored hash
  PasscodeScreen->>PasscodeVerify: render with onUpgrade callback
  PasscodeVerify->>commonUtil: parsePinHash(stored)
  commonUtil-->>PasscodeVerify: {version, hash}
  PasscodeVerify->>PIN_KDF: lookup config for version
  PasscodeVerify->>PasscodeVerify: compute candidate hash via hashData
  alt candidate equals stored
    PasscodeVerify->>PIN_KDF: lookup CURRENT_PIN_KDF_VERSION
    PasscodeVerify->>PasscodeVerify: compute upgraded hash
    PasscodeVerify->>commonUtil: encodePinHash(v2, newHash)
    commonUtil-->>PasscodeVerify: v2$newHash
    PasscodeVerify->>PasscodeScreenController: onUpgrade(v2$newHash)
    PasscodeScreenController->>authService: UPGRADE_PASSCODE_HASH(v2$newHash)
    authService->>authService: setPasscode & storeContext
  else mismatch
    PasscodeVerify-->>PasscodeVerify: call onError and return
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • KiruthikaJeyashankar
  • swatigoel

Poem

🐰 I nibble on hashes, old and new,
v1 hops off as v2 hops through,
A twirl of salt, a careful tweak,
Upgrade the passcode — secure and sleek! 🔐✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: upgrading PIN hashing from Argon2i to Argon2id v2 with a migration strategy for existing users.
Description check ✅ Passed The description covers the core change and includes an issue reference, but lacks screenshots section and could be more detailed about the migration mechanism.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/PasscodeVerify.test.tsx (1)

37-70: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add behavioral tests for the new verify/upgrade branches.

These snapshots don't exercise the security-critical paths added in this PR: legacy v1 success with onUpgrade, current v2 success without upgrade, and mismatch calling onError. Capturing PinInput's onDone and asserting the callbacks would make regressions here visible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PasscodeVerify.test.tsx` around lines 37 - 70, The tests only
cover snapshots and miss behavioral branches for PasscodeVerify: add unit tests
that simulate PinInput's onDone to exercise legacy v1 success path invoking
onUpgrade, current v2 success path invoking onSuccess without upgrade, and
failure path invoking onError; locate the PasscodeVerify component tests and add
three test cases that render PasscodeVerify with appropriate props (including a
stored v1 hashed passcode and onUpgrade mock, a v2 hashed passcode and onSuccess
mock, and a mismatch case with onError mock), call or trigger the
PinInput.onDone handler (or simulate entering the pin) and assert the correct
callbacks (onUpgrade/onSuccess/onError) are called with expected arguments.
🧹 Nitpick comments (3)
shared/constants.test.ts (1)

225-240: ⚡ Quick win

Assert the exact v2 profile, not loose minimums.

This suite still passes if iterations drops to 1 or parallelism changes, which weakens the protection this regression test is supposed to give. For a security config, pin the full expected v2 object.

✅ Suggested assertion
   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);
+    expect(PIN_KDF_PROFILES.v2).toEqual({
+      iterations: 2,
+      memory: 19 * 1024,
+      parallelism: 1,
+      hashLength: 32,
+      mode: 'argon2id',
+    });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/constants.test.ts` around lines 225 - 240, The test should assert the
exact v2 KDF profile instead of loose minimums; update the 'v2 profile follows
OWASP 2024 mobile guidance' spec to compare PIN_KDF_PROFILES.v2 to the full
expected object (including mode: 'argon2id', memory, hashLength, iterations,
parallelism, and any other fields) and keep the CURRENT_PIN_KDF_VERSION and
PIN_KDF_PROFILES references intact so the assertion fails if any parameter
(e.g., iterations or parallelism) changes; use a strict equality check (or deep
equality assertion) against the canonical v2 profile object rather than
greaterThanOrEqual checks.
shared/commonUtil.test.ts (1)

43-75: ⚡ Quick win

Add a future-version regression case.

The new parser coverage never exercises an unsupported version-shaped prefix. Add a case like v3$abcdef so a future unknown version can't silently turn into a broken login path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/commonUtil.test.ts` around lines 43 - 75, Add a regression test that
ensures parsePinHash preserves unknown version-shaped prefixes: add an it case
in the "PIN hash version helpers" suite that calls parsePinHash('v3$abcdef') and
expects {version: 'v3', hash: 'abcdef'} (similar to the existing v2 test),
referencing parsePinHash (and encodePinHash if you want a round-trip) so future
unknown versions aren’t misinterpreted as legacy v1.
shared/constants.ts (1)

124-136: ⚡ Quick win

Narrow PIN_KDF_PROFILES to the declared version union.

Record<string, Argon2iConfig> throws away the finite version set introduced here, so typos and incomplete future additions won't fail at compile time. Defining the union first and typing the map as Record<PinKdfVersion, Argon2iConfig> keeps the KDF surface exhaustive.

♻️ Proposed tightening
-export const PIN_KDF_PROFILES: Record<string, Argon2iConfig> = {
+export type PinKdfVersion = 'v1' | 'v2';
+
+export const PIN_KDF_PROFILES: Record<PinKdfVersion, Argon2iConfig> = {
   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';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/constants.ts` around lines 124 - 136, Define the PinKdfVersion union
before the map and narrow PIN_KDF_PROFILES to Record<PinKdfVersion,
Argon2iConfig> so the keys are exhaustive and typos are caught; specifically,
move or declare export type PinKdfVersion = 'v1' | 'v2' prior to
PIN_KDF_PROFILES, change the type of PIN_KDF_PROFILES from Record<string,
Argon2iConfig> to Record<PinKdfVersion, Argon2iConfig>, and ensure the map
contains entries for 'v1' and 'v2' (matching the existing argon2iConfig and v2
object) while keeping CURRENT_PIN_KDF_VERSION typed as PinKdfVersion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@machines/auth.test.ts`:
- Around line 144-149: The test only validates the UPGRADE_PASSCODE_HASH event
creator (AuthEvents.UPGRADE_PASSCODE_HASH) instead of exercising the state
machine transition; update the spec to send the UPGRADE_PASSCODE_HASH event into
the authorized machine instance (call the authorized service/transition) and
then assert that the machine updated context.passcode and called storeContext
with the new context; locate references to authorized,
AuthEvents.UPGRADE_PASSCODE_HASH, context.passcode, and storeContext in the test
and replace the simple .toEqual check with a run/send of the event and
expectations on the machine context and mocked storeContext invocation.

In `@machines/auth.ts`:
- Around line 69-71: The UPGRADE_PASSCODE_HASH event is currently defined at the
machine root (UPGRADE_PASSCODE_HASH with actions
['setPasscode','storeContext']), allowing any code with authService access to
overwrite the passcode hash; move/scope that event definition into the verified
unlock state (the state where PIN checks succeed) instead of the root events so
only that state's transitions can trigger it. Locate the UPGRADE_PASSCODE_HASH
entry and remove it from the top-level events, then add the same event mapping
inside the verifiedUnlock state's events/transitions (or guard it with a
state-specific condition) so only verifiedUnlock can call actions setPasscode
and storeContext; update any tests or callers that assumed root-level handling
accordingly.

---

Outside diff comments:
In `@components/PasscodeVerify.test.tsx`:
- Around line 37-70: The tests only cover snapshots and miss behavioral branches
for PasscodeVerify: add unit tests that simulate PinInput's onDone to exercise
legacy v1 success path invoking onUpgrade, current v2 success path invoking
onSuccess without upgrade, and failure path invoking onError; locate the
PasscodeVerify component tests and add three test cases that render
PasscodeVerify with appropriate props (including a stored v1 hashed passcode and
onUpgrade mock, a v2 hashed passcode and onSuccess mock, and a mismatch case
with onError mock), call or trigger the PinInput.onDone handler (or simulate
entering the pin) and assert the correct callbacks (onUpgrade/onSuccess/onError)
are called with expected arguments.

---

Nitpick comments:
In `@shared/commonUtil.test.ts`:
- Around line 43-75: Add a regression test that ensures parsePinHash preserves
unknown version-shaped prefixes: add an it case in the "PIN hash version
helpers" suite that calls parsePinHash('v3$abcdef') and expects {version: 'v3',
hash: 'abcdef'} (similar to the existing v2 test), referencing parsePinHash (and
encodePinHash if you want a round-trip) so future unknown versions aren’t
misinterpreted as legacy v1.

In `@shared/constants.test.ts`:
- Around line 225-240: The test should assert the exact v2 KDF profile instead
of loose minimums; update the 'v2 profile follows OWASP 2024 mobile guidance'
spec to compare PIN_KDF_PROFILES.v2 to the full expected object (including mode:
'argon2id', memory, hashLength, iterations, parallelism, and any other fields)
and keep the CURRENT_PIN_KDF_VERSION and PIN_KDF_PROFILES references intact so
the assertion fails if any parameter (e.g., iterations or parallelism) changes;
use a strict equality check (or deep equality assertion) against the canonical
v2 profile object rather than greaterThanOrEqual checks.

In `@shared/constants.ts`:
- Around line 124-136: Define the PinKdfVersion union before the map and narrow
PIN_KDF_PROFILES to Record<PinKdfVersion, Argon2iConfig> so the keys are
exhaustive and typos are caught; specifically, move or declare export type
PinKdfVersion = 'v1' | 'v2' prior to PIN_KDF_PROFILES, change the type of
PIN_KDF_PROFILES from Record<string, Argon2iConfig> to Record<PinKdfVersion,
Argon2iConfig>, and ensure the map contains entries for 'v1' and 'v2' (matching
the existing argon2iConfig and v2 object) while keeping CURRENT_PIN_KDF_VERSION
typed as PinKdfVersion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef6a9767-28f0-4f6a-a2d4-87761a0c07c3

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca5f8c and c1641f9.

📒 Files selected for processing (13)
  • .talismanrc
  • components/PasscodeVerify.test.tsx
  • components/PasscodeVerify.tsx
  • machines/auth.test.ts
  • machines/auth.ts
  • machines/auth.typegen.ts
  • screens/PasscodeScreen.test.tsx
  • screens/PasscodeScreen.tsx
  • screens/PasscodeScreenController.ts
  • shared/commonUtil.test.ts
  • shared/commonUtil.ts
  • shared/constants.test.ts
  • shared/constants.ts

Comment thread machines/auth.test.ts
Comment thread machines/auth.ts Outdated
…ation (inji#2399)

Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>
Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@machines/auth.test.ts`:
- Around line 153-166: The action stubs in authMachine.withConfig (e.g.,
requestStoredContext, storeContext, setContext, setBiometrics, setLanguage,
setAppSetupComplete, setPasscodeSalt, setOnboardingDone, setInitialDownloadDone,
setTourGuide, setIsToggleFromSettings) are empty functions and trigger
`@typescript-eslint/no-empty-function`; replace each empty function with a jest
mock (jest.fn()) or a shared no-op mock to satisfy the linter and enable
assertions; update the same pattern for the corresponding action stubs around
lines 177-193 as well so all action values in the withConfig call use jest.fn()
or the shared noop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd797782-a430-48f7-8d21-ca11cbb29b27

📥 Commits

Reviewing files that changed from the base of the PR and between c1641f9 and 0a9560d.

📒 Files selected for processing (3)
  • .talismanrc
  • machines/auth.test.ts
  • machines/auth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .talismanrc

Comment thread machines/auth.test.ts
Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
machines/auth.test.ts (1)

153-166: ⚡ Quick win

Extract a shared withConfig test helper for the repeated mock maps.

These mocked actions blocks are now duplicated across most transition specs, so any future auth-machine action rename/addition will require touching a dozen tests. A small factory like buildTestMachine({services, guards, actions}) or mockAuthConfig() would keep the new UPGRADE_PASSCODE_HASH cases aligned with the rest of the suite and reduce drift.

Also applies to: 177-193, 317-329, 342-354, 367-379, 395-407, 423-435, 448-460, 473-485, 498-510, 528-540, 558-570

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@machines/auth.test.ts` around lines 153 - 166, Tests duplicate the same
mocked actions map passed to authMachine.withConfig across many specs; create a
shared test helper (e.g., mockAuthConfig or buildTestMachine) that returns
authMachine.withConfig populated with the common actions map and optional
overrides for services/guards/actions, then update each test (currently calling
authMachine.withConfig directly) to use this helper so future action renames
only need one change; reference the existing authMachine.withConfig call and the
actions mock keys (requestStoredContext, storeContext, setContext,
setBiometrics, setLanguage, setAppSetupComplete, setPasscodeSalt,
setOnboardingDone, setInitialDownloadDone, setTourGuide,
setIsToggleFromSettings) when implementing the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@machines/auth.test.ts`:
- Around line 153-166: Tests duplicate the same mocked actions map passed to
authMachine.withConfig across many specs; create a shared test helper (e.g.,
mockAuthConfig or buildTestMachine) that returns authMachine.withConfig
populated with the common actions map and optional overrides for
services/guards/actions, then update each test (currently calling
authMachine.withConfig directly) to use this helper so future action renames
only need one change; reference the existing authMachine.withConfig call and the
actions mock keys (requestStoredContext, storeContext, setContext,
setBiometrics, setLanguage, setAppSetupComplete, setPasscodeSalt,
setOnboardingDone, setInitialDownloadDone, setTourGuide,
setIsToggleFromSettings) when implementing the helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf4c599c-f2eb-44de-94d3-9286336b1a01

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9560d and e229fea.

📒 Files selected for processing (2)
  • .talismanrc
  • machines/auth.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .talismanrc

@Kaushikgupta469

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review the latest commit with you findings

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch key implementation files
echo "=== shared/constants.ts ==="
fd -e ts constants.ts shared/ --exec cat {}

echo ""
echo "=== shared/commonUtil.ts ==="
fd -e ts commonUtil.ts shared/ --exec cat {}

echo ""
echo "=== components/PasscodeVerify.tsx ==="
fd -e tsx PasscodeVerify.tsx components/ --exec cat {}

echo ""
echo "=== screens/PasscodeScreen.tsx ==="
fd -e tsx PasscodeScreen.tsx screens/ --exec cat {}

echo ""
echo "=== screens/PasscodeScreenController.ts ==="
fd -e ts PasscodeScreenController.ts screens/ --exec cat {}

echo ""
echo "=== machines/auth.ts (relevant sections) ==="
fd -e ts auth.ts machines/ --exec cat {}

Length of output: 29997



@Kaushikgupta469 Here is a detailed review of the latest commit across all changed files.


Review: PR #2464 — Argon2id v2 PIN hashing with migration

Overall, the approach is sound and the migration design is well-thought-out. The Argon2id v2 parameters (t=2, m=19456 KiB, p=1) match the OWASP 2024 minimum recommendations, and limiting UPGRADE_PASSCODE_HASH to the unauthorized state is a correct security boundary. A few issues need attention below.


🔴 High Priority

1. PIN_HASH_VERSION_SEPARATOR constant is never used

shared/constants.ts defines PIN_HASH_VERSION_SEPARATOR = '$', but both encodePinHash and parsePinHash in shared/commonUtil.ts hardcode '$' directly. The constant is dead code. If the separator ever changes, only one place would be updated.

// shared/commonUtil.ts — fix
import { PIN_HASH_VERSION_SEPARATOR } from './constants';

export const encodePinHash = (version: string, rawHash: string): string =>
  `${version}${PIN_HASH_VERSION_SEPARATOR}${rawHash}`;

export const parsePinHash = (stored: string): { version: string; hash: string } => {
  if (!stored) return { version: 'v1', hash: stored };
  const sep = stored.indexOf(PIN_HASH_VERSION_SEPARATOR);
  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) };
};

2. No error handling in setPasscode (screens/PasscodeScreen.tsx)

If hashData throws (e.g. native module crash), the error is silently dropped and the user's passcode is never saved — with no feedback.

const setPasscode = async (passcode: string) => {
  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('passcodeHashError')); // or a generic error key
    console.error('Failed to hash passcode during setup', error);
  }
};

3. SETUP_PASSCODE unconditionally disables biometrics (PasscodeScreenController.ts)

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

This runs for all passcode setups, including a brand-new user's first setup. If a user has never had biometrics enabled, this is a no-op with a side-effect cost. More importantly, if there's a state-machine guard in the settings machine that interprets this differently for new users, it could cause an unintended state change. Verify this is gated to the biometric-toggle flow, or add a guard:

SETUP_PASSCODE: () => {
  authService.send(AuthEvents.SETUP_PASSCODE(passcode));
  if (controller.toggleUnlock) { // only when switching from biometrics
    settingsService?.send(SettingsEvents.TOGGLE_BIOMETRIC_UNLOCK(false, true));
  }
},

🟡 Medium Priority

4. Weak typing on PIN_KDF_PROFILES

// current
export const PIN_KDF_PROFILES: Record<string, Argon2iConfig> = { ... };

The key type should be PinKdfVersion to enforce that only valid versions are accessed:

export const PIN_KDF_PROFILES: Record<PinKdfVersion, Argon2iConfig> = { ... };

This also enables the TypeScript compiler to catch stale version references automatically.

5. Magic string 'v1' in parsePinHash

The fallback {version: 'v1', hash: stored} hard-codes a version string that has no corresponding named constant. Define a LEGACY_PIN_KDF_VERSION = 'v1' constant and use it in both parsePinHash and as the v1 key in PIN_KDF_PROFILES.

6. onError not called on unexpected errors in PasscodeVerify

In the outer catch block, sendErrorEvent fires but props.onError is never called. The user receives no UI feedback if there is an unexpected hashData failure during verification:

} catch (error) {
  sendErrorEvent(...);
  if (props.onError) {
    props.onError(t('passcodeVerifyError'));
  }
  console.error('error while verifying passCode ', error);
}

🟢 Low / Informational

7. Non-constant-time comparison in verify

if (storedHash !== candidate) { ... }

JavaScript string comparison is not constant-time. In a mobile app where the attacker would need physical device access this is very low risk, but worth noting for security audit completeness. A constant-time comparison utility (e.g. using crypto.timingSafeEqual on the underlying bytes) would be ideal if the argon2 library exposes raw buffers.

8. Argon2iConfig type name is misleading

The type is named Argon2iConfig but v2 uses mode: 'argon2id'. Consider renaming to ArgonHashConfig or PinHashConfig to avoid confusion.

9. v2 Argon2id parameters are at OWASP minimum

{t:2, m:19456, p:1} satisfies the OWASP 2024 floor. For a longer-lived setting consider m=46080 KiB (45 MB) with t=1, which is also in the OWASP recommendation set and provides more headroom against future GPU cost reductions.


Positive Observations ✅

  • Migration is transparent, non-blocking, and retried on next login — exactly the right UX.
  • UPGRADE_PASSCODE_HASH is correctly scoped to the unauthorized state only, preventing replay from arbitrary states.
  • parsePinHash safely handles all legacy hash formats (no separator, unrecognized prefix, empty string) by falling back to v1.
  • The 16-byte (128-bit) random salt generation is sufficient for Argon2id.
  • Upgrade failure is silently swallowed and logged — login proceeds unblocked, consistent with the "no lockout" promise.

…andling (inji#2399)

Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>
@Kaushikgupta469

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review the latest commit. If anything is wrong or can be improved, suggest a solution. Also verify the changes align with the security fix described in issue and pr Description.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Issue `#2399` ==="
gh issue view 2399 --repo inji/inji-wallet 2>/dev/null || gh issue view 2399 --repo mosip/inji-wallet 2>/dev/null || echo "Could not fetch issue"

echo ""
echo "=== PR `#2464` latest commit info ==="
gh pr view 2464 --repo mosip/inji-wallet --json headRefName,headRefOid,commits 2>/dev/null | jq '.commits[-1]' 2>/dev/null || echo "Could not fetch PR info"

echo ""
echo "=== PR `#2464` changed files ==="
gh pr diff 2464 --repo mosip/inji-wallet --name-only 2>/dev/null || echo "Could not fetch diff"

Length of output: 6143


🏁 Script executed:

#!/bin/bash
echo "=== shared/constants.ts ==="
fd -e ts -g 'constants.ts' shared/ --exec cat {} 2>/dev/null || find . -path '*/shared/constants.ts' -exec cat {} \;

echo ""
echo "=== shared/commonUtil.ts ==="
fd -e ts -g 'commonUtil.ts' shared/ --exec cat {} 2>/dev/null || find . -path '*/shared/commonUtil.ts' -exec cat {} \;

echo ""
echo "=== components/PasscodeVerify.tsx ==="
find . -path '*/components/PasscodeVerify.tsx' -exec cat {} \;

echo ""
echo "=== screens/PasscodeScreen.tsx ==="
find . -path '*/screens/PasscodeScreen.tsx' -exec cat {} \;

echo ""
echo "=== screens/PasscodeScreenController.ts ==="
find . -path '*/screens/PasscodeScreenController.ts' -exec cat {} \;

echo ""
echo "=== machines/auth.ts ==="
find . -path '*/machines/auth.ts' -exec cat {} \;

echo ""
echo "=== shared/commonUtil.test.ts (PIN hash helpers section) ==="
find . -path '*/shared/commonUtil.test.ts' -exec cat {} \;

echo ""
echo "=== shared/constants.test.ts ==="
find . -path '*/shared/constants.test.ts' -exec cat {} \;

Length of output: 52465



@Kaushikgupta469 Here is a detailed review of the latest commit, including alignment with issue #2399 and the PR description.


Latest Commit Review — PR #2464

Commit: 450896fc"fix(auth): address review findings - separator constant, hash error handling"


✅ Issues Resolved Since Last Review

# Finding Status
1 PIN_HASH_VERSION_SEPARATOR was dead code Fixed — now imported and used in both encodePinHash and parsePinHash
2 No error handling in setPasscode Fixedtry/catch with controller.setError(...) added
6 onError not called on unexpected errors in PasscodeVerify Fixedprops.onError(t('passcodeVerifyError', ...)) is now called in the outer catch

🔴 Critical: Scope Mismatch with Issue #2399

Issue #2399 is specifically about argon2iConfigForPasswordAndPhoneNumber (backup/restore path) using 500 iterations instead of the recommended 600,000. The issue title, description, and code location all point to this config:

// shared/constants.ts — lines ~145-151 — UNCHANGED in this PR
export const argon2iConfigForPasswordAndPhoneNumber: Argon2iConfig = {
  // TODO: expected iterations for hashing password and phone Number is 600000
  iterations: 500,   // ← still critically weak; NOT fixed by this PR
  memory: 16 * 1024,
  parallelism: 2,
  hashLength: 30,
  mode: 'argon2id',
};

This PR instead fixes PIN/passcode hashing (argon2iConfig with 5 iterations → Argon2id v2), which is a valid and important fix, but it is a different vulnerability from the one described in #2399. The backup encryption path remains unaddressed.

The test in shared/constants.test.ts even mislabels the weakness:

it('argon2iConfigForPasswordAndPhoneNumber should have high iterations', () => {
  expect(argon2iConfigForPasswordAndPhoneNumber.iterations).toBe(500); // 500 is NOT high

Recommendation: Either update the PR description to correctly scope the fix (PIN hashing, not backup hashing), or open a follow-up issue/PR for argon2iConfigForPasswordAndPhoneNumber. The current PR should not close #2399, as the root issue remains open.


🟡 Medium: Remaining Open Items

1. SETUP_PASSCODE still unconditionally disables biometrics

In screens/PasscodeScreenController.ts:

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

A brand-new user setting up their PIN for the first time will also trigger TOGGLE_BIOMETRIC_UNLOCK(false, true), which is a side-effect that doesn't apply to them. Gate it:

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

2. PIN_KDF_PROFILES key type should be PinKdfVersion

// current — allows any string key, no compiler enforcement
export const PIN_KDF_PROFILES: Record<string, Argon2iConfig> = { ... };

// recommended
export const PIN_KDF_PROFILES: Record<PinKdfVersion, Argon2iConfig> = { ... };

3. Magic string 'v1' in parsePinHash

The fallback { version: 'v1', hash: stored } hardcodes a string with no named constant. Add:

// shared/constants.ts
export const LEGACY_PIN_KDF_VERSION: PinKdfVersion = 'v1';

// shared/commonUtil.ts
import { LEGACY_PIN_KDF_VERSION, PIN_HASH_VERSION_SEPARATOR } from './constants';

export const parsePinHash = (stored: string): { version: string; hash: string } => {
  if (!stored) return { version: LEGACY_PIN_KDF_VERSION, hash: stored };
  const sep = stored.indexOf(PIN_HASH_VERSION_SEPARATOR);
  if (sep === -1 || !/^v\d+$/.test(stored.slice(0, sep))) {
    return { version: LEGACY_PIN_KDF_VERSION, hash: stored };
  }
  return { version: stored.slice(0, sep), hash: stored.slice(sep + 1) };
};

🟢 Verified Correct

  • Argon2id v2 parameters {t=2, m=19456 KiB, p=1, len=32} satisfy OWASP 2024 mobile minimums ✅
  • Migration is non-blocking — upgrade failure is swallowed/logged and login proceeds ✅
  • UPGRADE_PASSCODE_HASH is scoped to unauthorized state only — prevents replay from other states ✅
  • parsePinHash defensive handling — empty string, no separator, non-v<N> prefix all fall back to v1
  • 16-byte random salt via generateSecureRandom — sufficient for Argon2id ✅
  • Setup confirmation path uses PasscodeVerify without onUpgrade — correct, since a new hash is always the current version ✅
  • Test suite covers round-trip, legacy, empty, and malformed inputs ✅

Summary Table

Priority Item Status
🔴 Critical argon2iConfigForPasswordAndPhoneNumber (500 iter, backup path) — the actual #2399 issue — still unaddressed Open
🟡 Medium SETUP_PASSCODE unconditionally disables biometrics Open
🟡 Medium PIN_KDF_PROFILES key type should be PinKdfVersion Open
🟡 Medium Magic 'v1' string in parsePinHash Open
✅ Fixed PIN_HASH_VERSION_SEPARATOR dead code Resolved
✅ Fixed Error handling in setPasscode Resolved
✅ Fixed onError in PasscodeVerify catch block Resolved

The PIN hashing upgrade itself is well-implemented. The main action needed is clarifying the relationship to issue #2399, since the backup encryption weakness (argon2iConfigForPasswordAndPhoneNumber) is still present. Would you like me to open a separate issue to track the backup path fix?

Signed-off-by: Kaushik Gupta <146950347+Kaushikgupta469@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant