Skip to content

Conversation

@OrangeAndGreen
Copy link
Contributor

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

Product Description

Not user visible

Technical Summary

Attempting to retrieve encryption key for consented channel when a message is received for that channel and the key hasn't been retrieved yet. To support this, I added code to synchronously retrieve the encryption key, and extracted the common code for handling the received key to a helper method.

Feature Flag

ConnectID Messaging

Safety Assurance

Safety story

Low risk, leverages existing code to attempt to retrieve the encryption key on the fly. If it fails, the fallback behavior is to ignore the message.

Automated test coverage

No automated tests for ConnectID yet.

QA Plan

Create an auto-consented channel for a Connect opportunity that a mobile worker is part of.
Before ever accessing the channel in the messaging channel list, send a message to the device on that channel.
The message should be received, decrypted, and notified to the user so they can view it.

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

…ssage is received for that channel and the key hasn't been retrieved yet.
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2025

📝 Walkthrough

Walkthrough

The changes modify the processing of encryption keys for messaging channels. In the MessageManager.handleReceivedMessage method, a check is added that verifies if the encryption key is missing. If so, the method calls a new synchronous method to retrieve the key. If the key still cannot be retrieved, the incoming message is ignored. Additionally, the getChannelEncryptionKey method now calls a new dedicated method to handle the response, removing the previous manual parsing of response data. In the ApiConnectId class, two new public static methods are introduced. The first, retrieveChannelEncryptionKeySync, retrieves the encryption key synchronously by acquiring a token and making a POST request. Upon receiving a valid response, it calls the second newly added method, handleReceivedEncryptionKey, which parses the encryption key from the response stream, updates the channel record, and stores it in the database.

Sequence Diagram(s)

sequenceDiagram
    participant MM as MessageManager
    participant Api as ApiConnectId
    participant DB as ConnectDatabaseHelper

    Note over MM: Message received
    MM->>MM: Check if encryption key is null/empty
    alt Key is missing
        MM->>Api: retrieveChannelEncryptionKeySync(context, channel)
        Api->>Api: retrieveConnectIdTokenSync
        Api->>Api: Construct parameter map & send POST request
        Api->>Api: Evaluate response (200-299)
        Api->>Api: handleReceivedEncryptionKey(context, stream, channel)
        Api->>DB: storeMessagingChannel(channel)
        Api-->>MM: Encryption key updated
    else Key is present
        Note over MM: Process message normally
    end
    MM->>MM: Continue message processing or ignore if key still missing
Loading

Suggested labels

cross requested

Suggested reviewers

  • pm-dimagi
  • shubham1g5
✨ Finishing Touches
  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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 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: 1

🧹 Nitpick comments (2)
app/src/org/commcare/connect/network/ApiConnectId.java (2)

514-527: Synchronous key retrieval implementation looks good but needs better error handling

The implementation of retrieveChannelEncryptionKeySync is well structured but lacks error handling for network failures.

Consider adding error logging if the response code indicates failure:

if(result.responseCode >= 200 && result.responseCode < 300) {
    handleReceivedEncryptionKey(context, result.responseStream, channel);
+} else {
+    Logger.log(LogTypes.TYPE_WARNING, "Failed to retrieve encryption key: " + result.responseCode);
}

Also, verify the key URL is not empty before attempting the request:

if(auth != null) {
    HashMap<String, Object> params = new HashMap<>();
    params.put("channel_id", channel.getChannelId());
+    
+    if (Strings.isNullOrEmpty(channel.getKeyUrl())) {
+        Logger.log(LogTypes.TYPE_WARNING, "Missing key URL for channel: " + channel.getChannelId());
+        return;
+    }

    ConnectNetworkHelper.PostResult result = ConnectNetworkHelper.postSync(context,
            channel.getKeyUrl(), null, auth, params, true, true);

540-553: Improve JSON handling in encryption key processing

The method properly extracts the encryption key from the response, but could use improved validation.

Add explicit checking for the existence of the "key" field in the JSON response:

if(responseAsString.length() > 0) {
    JSONObject json = new JSONObject(responseAsString);
-    channel.setKey(json.getString("key"));
-    ConnectDatabaseHelper.storeMessagingChannel(context, channel);
+    if(json.has("key")) {
+        channel.setKey(json.getString("key"));
+        ConnectDatabaseHelper.storeMessagingChannel(context, channel);
+    } else {
+        Logger.log(LogTypes.TYPE_WARNING, "Encryption key response missing 'key' field");
+    }
}

Also, consider adding specific exception handling for JSON parsing:

try {
    String responseAsString = new String(
            StreamsUtil.inputStreamToByteArray(stream));

    if(responseAsString.length() > 0) {
        JSONObject json = new JSONObject(responseAsString);
        channel.setKey(json.getString("key"));
        ConnectDatabaseHelper.storeMessagingChannel(context, channel);
    }
-} catch(Exception e) {
+} catch(JSONException e) {
+    Logger.exception("Error parsing encryption key JSON", e);
+} catch(IOException e) {
    Logger.exception("Messaging channel encryption key", e);
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 15334bf and 466a632.

📒 Files selected for processing (2)
  • app/src/org/commcare/connect/MessageManager.java (3 hunks)
  • app/src/org/commcare/connect/network/ApiConnectId.java (3 hunks)
🔇 Additional comments (4)
app/src/org/commcare/connect/MessageManager.java (4)

7-7: Appropriate import addition

The addition of com.google.common.base.Strings is appropriate for using the isNullOrEmpty utility method.


51-52: Good fallback comment explaining behavior

The comment clearly documents the fallback behavior that if we still don't have a key after the retrieval attempt, the message will be ignored. This aligns with the safety assurance mentioned in the PR objectives.


214-219: Code improvement: Better encapsulation of encryption key handling

Good refactoring to extract the encryption key handling logic into a common method. This improves code maintainability by centralizing the logic for processing encryption keys.


46-50:

Details

✅ Verification successful

Automatic encryption key retrieval looks good but consider potential blocking

The implementation to automatically retrieve missing encryption keys when a message is received is well designed. This makes the code more robust by ensuring messages can be properly processed even if keys weren't previously available.

One thing to verify is whether this method is called on a background thread, as synchronous network operations could potentially block the UI thread:


🏁 Script executed:

#!/bin/bash
# Find where handleReceivedMessage is called to verify it's not called directly from UI operations
rg -B 5 -A 2 "handleReceivedMessage" --type java

Length of output: 1602


Encryption key retrieval verified against UI blocking concerns

The automatic retrieval logic is robust. I verified via the shell script that the handleReceivedMessage method is invoked from CommCareFirebaseMessagingService, which operates on a background thread. As a result, the synchronous call to retrieve the encryption key should not block the UI thread.

  • Location: app/src/org/commcare/connect/MessageManager.java (Lines 46-50)
    if(Strings.isNullOrEmpty(channel.getKey())) {
        //Attempt to get the encryption key now if we don't have it yet
        ApiConnectId.retrieveChannelEncryptionKeySync(context, channel);
    }
  • Invocation Verified In: app/src/org/commcare/services/CommCareFirebaseMessagingService.java

Please keep an eye on any similar synchronous network calls elsewhere to maintain non-blocking UI performance.

Comment on lines +523 to +525
if(result.responseCode >= 200 && result.responseCode < 300) {
handleReceivedEncryptionKey(context, result.responseStream, channel);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential resource leak in response stream handling

The response stream might not be closed if an exception occurs during processing.

Ensure the response stream is properly closed:

if(result.responseCode >= 200 && result.responseCode < 300) {
-    handleReceivedEncryptionKey(context, result.responseStream, channel);
+    try {
+        handleReceivedEncryptionKey(context, result.responseStream, channel);
+    } finally {
+        if(result.responseStream != null) {
+            try {
+                result.responseStream.close();
+            } catch(IOException e) {
+                Logger.exception("Error closing response stream", e);
+            }
+        }
+    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(result.responseCode >= 200 && result.responseCode < 300) {
handleReceivedEncryptionKey(context, result.responseStream, channel);
}
if(result.responseCode >= 200 && result.responseCode < 300) {
try {
handleReceivedEncryptionKey(context, result.responseStream, channel);
} finally {
if(result.responseStream != null) {
try {
result.responseStream.close();
} catch(IOException e) {
Logger.exception("Error closing response stream", e);
}
}
}
}

Comment on lines 550 to 552
} catch(Exception e) {
Logger.exception("Messaging channel encryption key", e);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We must move away from practice of catching generalised exceptions in Connect unless there is strong reason to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

channel.setKey(json.getString("key"));
ConnectDatabaseHelper.storeMessagingChannel(context, channel);
}
} catch(IOException | JSONException e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Given a JSONException here signifies a coding error here, I think this needs to hard crash the application.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, okay, that makes sense to me too. The compiler won't let me leave those exceptions uncaught so I converted to a RuntimeException (b2b465f), is that the best way to handle this?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that's what we want but only for JSONException and not IOException as IOException can happen for external reasons (network/disk I/O etc) out of our control.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The call to StreamsUtil.inputStreamToByteArray requires that we handle IOException somehow. Would it be better to silently log an exception in that case? I can also bubble up the exception but then it just needs to be handled by the two callers instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking into inputStreamFromByteArray a bit more, I see Connect code using it quite a bit so whatever we decide to do will need to be applied in other spots as well. But that's probably work for a new ticket...

When I look at existing usage in the legacy code I see a few ways this has been handled:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's the change for silently logging the IOException if that's acceptable. 0caff08

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks, yeah that's acceptable for IOException as they don't necessarily signify a code error on our end but think it's better for them to bubble up to UI level method calls and show a related error on UI corresponding to the error message.

@OrangeAndGreen OrangeAndGreen merged commit 35ddf02 into connect_qa Mar 7, 2025
1 of 2 checks passed
@OrangeAndGreen OrangeAndGreen deleted the dv/get_encryption_key branch March 7, 2025 18:19
@coderabbitai coderabbitai bot mentioned this pull request Jun 2, 2025
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jul 17, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants