Skip to content

Conversation

@shubham1g5
Copy link
Contributor

Product Description

https://dimagi.atlassian.net/browse/CCCT-1148

Technical Summary

Some context here

Labels and Review

  • [] Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@coderabbitai
Copy link

coderabbitai bot commented Jun 9, 2025

📝 Walkthrough

Walkthrough

This set of changes introduces biometric key invalidation handling and related error messaging for Personal ID authentication. The Android KeyStore handler interfaces and implementations are updated to provide a new isKeyValid() method, allowing the system to check if a biometric-linked encryption key remains valid (i.e., not invalidated by biometric changes). The Personal ID authentication flow now validates this key before allowing biometric login, and if invalid, displays an appropriate error message and logs a global error. Supporting string resources and error enums are updated, and a new key is generated and stored upon successful biometric or PIN configuration. Additionally, database helper methods are refactored to remove explicit context parameters, centralizing context management.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant PersonalIdManager
    participant EncryptionKeyProvider
    participant KeyStoreHandler
    participant UI

    User->>PersonalIdManager: Attempt biometric login
    PersonalIdManager->>EncryptionKeyProvider: isKeyValid()
    EncryptionKeyProvider->>KeyStoreHandler: isKeyValid()
    alt Key valid
        PersonalIdManager->>PersonalIdManager: Proceed with authentication
    else Key invalid
        PersonalIdManager->>UI: Show biometric invalidated error
        PersonalIdManager->>GlobalErrors: Log PERSONALID_BIOMETRIC_INVALIDATED_ERROR
        PersonalIdManager-->>User: Abort authentication
    end
Loading
sequenceDiagram
    participant User
    participant PersonalIdBiometricConfigFragment
    participant EncryptionKeyProvider
    participant KeyStoreHandler

    User->>PersonalIdBiometricConfigFragment: Configure biometric or PIN
    PersonalIdBiometricConfigFragment->>EncryptionKeyProvider: Create/retrieve BIOMETRIC_INVALIDATION_KEY
    EncryptionKeyProvider->>KeyStoreHandler: getKeyOrGenerate()
    KeyStoreHandler-->>EncryptionKeyProvider: Return key
    EncryptionKeyProvider-->>PersonalIdBiometricConfigFragment: Key stored
    PersonalIdBiometricConfigFragment->>User: Navigate forward
Loading

Suggested labels

skip-integration-tests

Suggested reviewers

  • OrangeAndGreen
  • Jignesh-dimagi
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/src/org/commcare/android/security/KeyStoreHandler.kt (1)

4-4: Remove unused import.

The javax.crypto.SecretKey import is not used anywhere in this interface file.

-import javax.crypto.SecretKey
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4502bb5 and a4a436e.

📒 Files selected for processing (11)
  • app/res/values/strings.xml (1 hunks)
  • app/src/org/commcare/android/security/AesKeyStoreHandler.kt (2 hunks)
  • app/src/org/commcare/android/security/KeyStoreHandler.kt (1 hunks)
  • app/src/org/commcare/android/security/RsaKeyStoreHandler.kt (1 hunks)
  • app/src/org/commcare/connect/PersonalIdManager.java (7 hunks)
  • app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1 hunks)
  • app/src/org/commcare/connect/database/ConnectUserDatabaseUtil.java (2 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (5 hunks)
  • app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java (2 hunks)
  • app/src/org/commcare/utils/EncryptionKeyProvider.java (1 hunks)
  • app/src/org/commcare/utils/GlobalErrors.java (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1)
app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java (1)
  • DatabaseConnectOpenHelper (39-165)
app/src/org/commcare/utils/EncryptionKeyProvider.java (3)
app/src/org/commcare/android/security/KeyStoreHandler.kt (1)
  • isKeyValid (8-8)
app/src/org/commcare/android/security/AesKeyStoreHandler.kt (1)
  • isKeyValid (38-47)
app/src/org/commcare/android/security/RsaKeyStoreHandler.kt (1)
  • isKeyValid (33-35)
app/src/org/commcare/android/security/AesKeyStoreHandler.kt (1)
app/src/org/commcare/utils/EncryptionKeyAndTransform.java (1)
  • EncryptionKeyAndTransform (10-39)
app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java (1)
app/src/org/commcare/CommCareApplication.java (1)
  • CommCareApplication (154-1246)
🔇 Additional comments (26)
app/src/org/commcare/android/security/KeyStoreHandler.kt (1)

8-8: LGTM: Clean interface extension for key validation.

The new isKeyValid() method provides a standardized way to check key validity across different KeyStore handler implementations.

app/src/org/commcare/utils/EncryptionKeyProvider.java (1)

49-51: LGTM: Proper delegation to handler implementation.

The method correctly delegates to the appropriate KeyStore handler in decryption mode, which is the appropriate context for discovering key invalidation.

app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (1)

52-54: LGTM: Good refactoring to centralize context management.

Removing the Context parameter and delegating to the static DatabaseConnectOpenHelper.dbExists() method centralizes context access and simplifies the API.

app/src/org/commcare/android/security/RsaKeyStoreHandler.kt (1)

33-35: LGTM: Appropriate implementation for RSA keys.

For RSA keys, checking existence is sufficient for validity since RSA keys don't get invalidated by biometric changes like AES keys do. This implementation correctly delegates to the existing doesKeyExist() method.

app/src/org/commcare/utils/GlobalErrors.java (1)

7-8: LGTM: Error constants properly updated for biometric invalidation feature.

The addition of PERSONALID_BIOMETRIC_INVALIDATED_ERROR and the update to use a more specific message for PERSONALID_LOST_CONFIGURATION_ERROR correctly support the biometric key invalidation functionality. The implementation follows the existing enum pattern.

app/res/values/strings.xml (1)

333-334: LGTM: User-facing messages are clear and actionable.

The updated recovery message provides specific guidance about Personal ID account recovery, and the new biometric invalidation message clearly explains the issue and next steps to users. Both messages support good user experience.

app/src/org/commcare/connect/database/ConnectUserDatabaseUtil.java (2)

17-17:

Details

✅ Verification successful

Verify consistency of database helper context management.

The removal of the context parameter from ConnectDatabaseHelper.dbExists() suggests that context management has been centralized within the helper class. Ensure this pattern is consistently applied across all database operations.


🏁 Script executed:

#!/bin/bash
# Description: Verify that ConnectDatabaseHelper.dbExists() method signature and usage is consistent across the codebase

# Check for any remaining calls to dbExists() with context parameter
rg -A 3 -B 3 "ConnectDatabaseHelper\.dbExists\(" --type java

# Check the actual method signature in ConnectDatabaseHelper
ast-grep --pattern $'class ConnectDatabaseHelper {
  $$$
  public static $_ dbExists($_) {
    $$$
  }
  $$$
}'

Length of output: 1456


🏁 Script executed:

#!/bin/bash
# Locate ConnectDatabaseHelper.java and inspect the dbExists method signature
file=$(rg -l "class ConnectDatabaseHelper" -g "*.java")
echo "File: $file"
rg -n "public static .*dbExists" "$file"
rg -n "dbExists(" "$file"

Length of output: 569


🏁 Script executed:

#!/bin/bash
# List all occurrences of dbExists( across the Java codebase to verify no calls with parameters
rg -n "dbExists\\(" --type java

Length of output: 605


Consistency Verified: Context Management Centralized in dbExists

All references to ConnectDatabaseHelper.dbExists() have been updated to the no-argument version, and the underlying helper also manages context internally:

  • ConnectDatabaseHelper.java (line 52): public static boolean dbExists() { … }
  • DatabaseConnectOpenHelper.java (line 72): public static boolean dbExists() { … }
  • ConnectUserDatabaseUtil.java (line 17): if (!ConnectDatabaseHelper.dbExists())
  • PersonalIdManager.java (line 247): if (ConnectDatabaseHelper.dbExists())

No further changes are needed—the refactoring is consistent and complete.


50-66:

Details

✅ Verification successful

Verify database deletion method consistency.

The forgetUser() method has been changed to remove the context parameter and calls DatabaseConnectOpenHelper.deleteDb() without arguments. Ensure this change is consistent with the helper class implementation.


🏁 Script executed:

#!/bin/bash
# Description: Verify DatabaseConnectOpenHelper.deleteDb() method signature and usage consistency

# Check all calls to deleteDb() to ensure they match the new signature
rg -A 3 -B 3 "DatabaseConnectOpenHelper\.deleteDb\(" --type java

# Check the actual method signature in DatabaseConnectOpenHelper
ast-grep --pattern $'class DatabaseConnectOpenHelper {
  $$$
  public static $_ deleteDb($_) {
    $$$
  }
  $$$
}'

Length of output: 950


🏁 Script executed:

#!/bin/bash
# Locate the deleteDb method definition in DatabaseConnectOpenHelper
rg -n "deleteDb" app/src/org/commcare/connect/database/DatabaseConnectOpenHelper.java

Length of output: 194


🏁 Script executed:

#!/bin/bash
# 1. Find the file defining DatabaseConnectOpenHelper
fd -e java DatabaseConnectOpenHelper.java

# 2. Search for the class declaration if the filename differs
rg -n "class DatabaseConnectOpenHelper" --type java

# 3. Locate the deleteDb method signature across all Java files
rg -n "public static .* deleteDb" --type java

Length of output: 481


deleteDb() signature update verified

Verified that DatabaseConnectOpenHelper.deleteDb() is now defined as a parameterless static method in
app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java:76, and its invocation in
ConnectUserDatabaseUtil.forgetUser() matches this signature. No further changes required.

app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (2)

246-246: LGTM: Biometric key generation properly placed.

The call to storeBiometricInvalidationKey() is correctly placed after confirming that biometric or PIN authentication is configured, ensuring the key is only generated when needed.


260-267:

Details

✅ Verification successful

LGTM: Secure biometric key generation implementation.

The implementation correctly:

  • Checks for minimum API level (Nougat) for biometric key invalidation support
  • Uses EncryptionKeyProvider with biometric flag enabled for hardware-backed security
  • Only generates the key when authentication is properly configured

🏁 Script executed:

#!/bin/bash
# Description: Verify that the biometric key validation logic properly integrates with this key generation

# Search for usage of BIOMETRIC_INVALIDATION_KEY constant
rg -A 5 -B 5 "BIOMETRIC_INVALIDATION_KEY" --type java

# Search for key validation logic that might use this key
rg -A 5 -B 5 "isKeyValid" --type java

Length of output: 7837


Secure biometric key generation and validation are correctly integrated

  • BIOMETRIC_INVALIDATION_KEY is used consistently in storeBiometricInvalidationKey() and in PersonalIdManager.isKeyValid().
  • EncryptionKeyProvider(requireContext(), true, BIOMETRIC_INVALIDATION_KEY) ensures hardware-backed security and invalidation on biometric enrollment.
  • API-level check (>= N) guards against unsupported platforms.

No further changes required.

app/src/org/commcare/models/database/connect/DatabaseConnectOpenHelper.java (4)

9-9: LGTM: Import supports the refactoring.

The import of CommCareApplication is necessary for the singleton-based database path access.


68-70: LGTM: Clean refactoring to use singleton pattern.

The method correctly uses CommCareApplication.instance().getDatabasePath() to centralize database path management. This eliminates the need for passing Context parameters while maintaining the same functionality.


72-74: LGTM: Consistent with the refactoring pattern.

The method signature change aligns with the centralized database path approach and correctly delegates to the refactored getDbFile() method.


76-78: LGTM: Consistent with the refactoring pattern.

The method signature change aligns with the centralized database path approach and correctly delegates to the refactored getDbFile() method.

app/src/org/commcare/android/security/AesKeyStoreHandler.kt (6)

5-5: LGTM: Required import for key validation.

The KeyPermanentlyInvalidatedException import is necessary for detecting invalidated biometric keys.


10-10: LGTM: Required import for cipher operations.

The Cipher import is necessary for the key validation implementation.


23-25: LGTM: Good practice to use constants.

Extracting the transformation string to a constant improves maintainability and reduces the risk of typos.


27-36: LGTM: Clean refactoring with helper method.

The refactored method improves readability by separating key retrieval logic from key generation. The use of the TRANSFORM constant is consistent.


38-47: LGTM: Correct implementation of key validation.

The method properly implements biometric key validation by:

  • Checking if the key exists first
  • Attempting to initialize a cipher with the key
  • Catching KeyPermanentlyInvalidatedException to detect invalidated keys
  • Returning appropriate boolean values

This follows Android KeyStore best practices for biometric key validation.


49-58: LGTM: Proper keystore key retrieval implementation.

The helper method correctly:

  • Checks if the alias exists in the keystore
  • Verifies the entry type is SecretKeyEntry
  • Returns the secret key or null appropriately

The implementation is safe and follows Android KeyStore patterns.

app/src/org/commcare/connect/PersonalIdManager.java (6)

3-3: LGTM: Required import for error handling.

The static import of PERSONALID_BIOMETRIC_INVALIDATED_ERROR is necessary for the new error handling functionality.


30-30: LGTM: Required imports for new functionality.

The imports support the new biometric key validation and error handling features:

  • GlobalErrorRecord for error logging
  • EncryptionKeyProvider for key validation
  • GlobalErrorUtil for error management
  • GlobalErrors for error definitions

Also applies to: 49-51


71-71: LGTM: Well-defined constant.

The BIOMETRIC_INVALIDATION_KEY constant provides a clear identifier for the biometric key used for validation purposes.


187-193: LGTM: Proper key validation integration.

The key validation check is correctly placed before biometric authentication and properly handles the failure case by:

  • Recording a global error for tracking
  • Displaying a user-friendly error message
  • Aborting the authentication flow

This prevents authentication attempts with invalidated biometric keys.


227-230: LGTM: Clean key validation implementation.

The private method properly delegates to EncryptionKeyProvider with the appropriate parameters:

  • Uses the parent activity context
  • Enables user authentication requirement
  • Uses the defined biometric invalidation key identifier

247-247: LGTM: Consistent with database helper refactoring.

The method calls are updated to match the new parameterless signatures in the database helper classes, maintaining consistency with the overall refactoring.

Also applies to: 250-250

Comment on lines 188 to 189
GlobalErrorUtil.addError(
new GlobalErrorRecord(new Date(), PERSONALID_BIOMETRIC_INVALIDATED_ERROR.ordinal()));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@OrangeAndGreen I am not sure what's the behaviour we want here, in other places of global errors we are crashing the CommCare but changing biometric should not really cause a crash.

I am also not sure if we want this to be a global error like this. Furthermore, do we want to call forgetUser for this error ? I think not, because then there is no point of having a different key. But if we don't then the behaviour is weird as we are asking user to recover without really signing out of the device.

Copy link
Contributor

Choose a reason for hiding this comment

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

I had a conversation with Clayton about this last week and it sounds like there are two approaches we could take:

  1. If we want to be safe, we can just silently log the error for now so we can see how prevalent this is before deciding on how to react to it. If we do that though, the token will need to automatically be re-established once invalidated so we're able to see if the same user is triggering the event more than once (otherwise the token would only be configured during the device configuration workflow)
  2. The alternative would be some kind of deconfiguration so the user has to recover their account to regain trust. In the long run I think this is one place where we might use "soft recovery" (i.e. a simpler recovery process like just OTP or backup code). But for this release, the way we're handling those is with crashDb and an error message when the app starts up next time.

My preference is for the first option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#1 doesn't seem to add any security value and I would avoid doing more immediate work here at the end of the QA cycle for something that doesn't add any definite value to the release

#2 Given changing biometric is a practical alid scenario, I doin't really want to crash here.

I am more inclined to call forget User when this happens and direct user to Login page thereafter in that case. Any concerns with that ?

Copy link
Contributor

Choose a reason for hiding this comment

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

But if we're going with the second option, I think we should use crashDb and let it add the global error, rather than simply adding the error directly here (we also want the user to be deconfigured and kicked out of whatever activity they're currently doing)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't understand why would we like to crash app in a genuine case that we can handle without crashing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this could be worth bringing to the team as a whole to make sure we have a decision everyone is on board with.

Regarding the latest commit, it seems that once the Keystore gets invalidated the code will then log an analytics event every time the user unlocks going forward, correct? Seems like we need a reset mechanism, to setup a new keystore entry so we only log another event if the new entry gets invalidated (i.e. logging each newly invalidated token rather than logging one invalidated entry a bunch of times).

@shubham1g5 shubham1g5 added the skip-integration-tests Skip android tests. label Jun 9, 2025
Comment on lines 188 to 189
GlobalErrorUtil.addError(
new GlobalErrorRecord(new Date(), PERSONALID_BIOMETRIC_INVALIDATED_ERROR.ordinal()));
Copy link
Contributor

Choose a reason for hiding this comment

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

But if we're going with the second option, I think we should use crashDb and let it add the global error, rather than simply adding the error directly here (we also want the user to be deconfigured and kicked out of whatever activity they're currently doing)

@shubham1g5 shubham1g5 requested a review from OrangeAndGreen June 9, 2025 17:11
@shubham1g5 shubham1g5 merged commit 4a9a7cb into commcare_2.57 Jun 10, 2025
2 checks passed
@shubham1g5 shubham1g5 deleted the biometricSecret branch June 10, 2025 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-integration-tests Skip android tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants