Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions app/src/org/commcare/android/integrity/IntegrityReporterWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.google.android.play.core.integrity.StandardIntegrityException
import com.google.common.base.Strings
import org.commcare.android.database.connect.models.PersonalIdSessionData
import org.commcare.android.integrity.IntegrityTokenApiRequestHelper.Companion.fetchIntegrityToken
import org.commcare.android.logging.ReportingUtils
import org.commcare.connect.network.connectId.PersonalIdApiHandler
Expand Down Expand Up @@ -63,13 +61,8 @@ class IntegrityReporterWorker(appContext: Context, workerParams: WorkerParameter
val requestHash = org.commcare.utils.HashUtils.computeHash(jsonBody, org.commcare.utils.HashUtils.HashAlgorithm.SHA256)
val tokenResult = fetchIntegrityToken(requestHash)
val (integrityToken, hash) = tokenResult.getOrElse {
val errorCode: String = if(it is StandardIntegrityException) {
it.errorCode.toString();
} else {
it.message ?: "Unknown error"
}

FirebaseAnalyticsUtil.reportPersonalIdIntegritySubmission(requestId, errorCode)
val errorCode = IntegrityTokenApiRequestHelper.getCodeForException(it)
FirebaseAnalyticsUtil.reportPersonalIdHeartbeatIntegritySubmission(requestId, errorCode)

body["device_error"] = errorCode
makeReportIntegrityCall(context, null, null, body, requestId)
Expand Down Expand Up @@ -100,7 +93,7 @@ class IntegrityReporterWorker(appContext: Context, workerParams: WorkerParameter
cont.resume(success)
}
override fun onFailure(errorCode: PersonalIdOrConnectApiErrorCodes, t: Throwable?) {
FirebaseAnalyticsUtil.reportPersonalIdIntegritySubmission(requestId, "SendError")
FirebaseAnalyticsUtil.reportPersonalIdHeartbeatIntegritySubmission(requestId, "SendError")
cont.resume(false)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,13 @@ class IntegrityTokenApiRequestHelper(
Result.failure(e)
}
}

fun getCodeForException(exception: Throwable): String {
return if (exception is StandardIntegrityException) {
exception.errorCode.toString()
} else {
exception.message ?: "UnknownError"
}
}
Comment on lines +156 to +162
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid sending raw exception messages to analytics

Using exception.message risks leaking details; prefer a sanitized, stable code.

-        fun getCodeForException(exception: Throwable): String {
-            return if (exception is StandardIntegrityException) {
-                exception.errorCode.toString()
-            } else {
-                exception.message ?: "UnknownError"
-            }
-        }
+        fun getCodeForException(exception: Throwable): String {
+            return if (exception is StandardIntegrityException) {
+                exception.errorCode.toString()
+            } else {
+                exception::class.simpleName ?: "UnknownError"
+            }
+        }
📝 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
fun getCodeForException(exception: Throwable): String {
return if (exception is StandardIntegrityException) {
exception.errorCode.toString()
} else {
exception.message ?: "UnknownError"
}
}
fun getCodeForException(exception: Throwable): String {
return if (exception is StandardIntegrityException) {
exception.errorCode.toString()
} else {
exception::class.simpleName ?: "UnknownError"
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/android/integrity/IntegrityTokenApiRequestHelper.kt
around lines 156 to 162, the method currently returns exception.message for
non-StandardIntegrityException cases which may leak sensitive info; change it to
return a sanitized, stable identifier instead (for example
exception::class.java.simpleName or a fixed token like
"UnknownError"/"UnhandledException") so raw messages are never sent to
analytics, keeping the StandardIntegrityException branch unchanged.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class ReportIntegrityResponseParser<T> implements BaseApiResponseParser<T
@Override
public T parse(int responseCode, @NonNull InputStream responseData, @Nullable Object anyInputObject) throws IOException,JSONException {
JSONObject json = new JSONObject(new String(StreamsUtil.inputStreamToByteArray(responseData)));
FirebaseAnalyticsUtil.reportPersonalIdIntegritySubmission(((String)anyInputObject),
FirebaseAnalyticsUtil.reportPersonalIdHeartbeatIntegritySubmission(((String)anyInputObject),
json.optString("result_code", "NoCodeFromServer"));
return (T)Boolean.TRUE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import android.Manifest;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.location.Location;
import android.os.Bundle;
Expand Down Expand Up @@ -268,8 +267,10 @@ public void onTokenReceived(@NotNull String requestHash,

@Override
public void onTokenFailure(@NotNull Exception exception) {
onConfigurationFailure(AnalyticsParamValue.START_CONFIGURATION_INTEGRITY_DEVICE_FAILURE,
integrityTokenApiRequestHelper.getErrorForException(requireActivity(), exception));
String errorCode = IntegrityTokenApiRequestHelper.Companion.getCodeForException(exception);
FirebaseAnalyticsUtil.reportPersonalIdConfigurationIntegritySubmission(errorCode);

makeStartConfigurationCall(null, body, null);
}
Comment on lines +270 to 274
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Possible NPE path: handleIntegritySubError can receive null tokenResponse

After onTokenFailure you call makeStartConfigurationCall(null, body, null). If the API then returns INTEGRITY_ERROR, the subsequent handleIntegritySubError(integrityTokenResponse, …) dereferences a null tokenResponse in showIntegrityCheckDialog(...). Add a null guard and avoid fall-through.

-                    case INTEGRITY_ERROR:
-                        handleIntegritySubError(integrityTokenResponse,
-                                personalIdSessionDataViewModel.getPersonalIdSessionData().getSessionFailureSubcode());
-                    default:
-                        navigateFailure(failureCode, t);
-                        break;
+                    case INTEGRITY_ERROR:
+                        if (integrityTokenResponse != null) {
+                            handleIntegritySubError(
+                                    integrityTokenResponse,
+                                    personalIdSessionDataViewModel.getPersonalIdSessionData().getSessionFailureSubcode());
+                            break;
+                        } else {
+                            onConfigurationFailure(
+                                    AnalyticsParamValue.START_CONFIGURATION_INTEGRITY_CHECK_FAILURE,
+                                    getString(R.string.personalid_configuration_process_failed_subtitle));
+                            break;
+                        }
+                    default:
+                        navigateFailure(failureCode, t);
+                        break;

Also applies to: 389-394

🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/personalId/PersonalIdPhoneFragment.java around
lines 270-274 (and similarly around lines 389-394), handleIntegritySubError may
be called with a null integrityTokenResponse because you call
makeStartConfigurationCall(null, body, null) after onTokenFailure; add a null
guard to avoid dereferencing a null tokenResponse: before calling
makeStartConfigurationCall or before delegating to handleIntegritySubError,
check if the integrity token response is null and either log/report the
condition and return early or call the error path that does not call
showIntegrityCheckDialog; apply the same null-check logic to the other
occurrence at lines 389-394 to prevent the NPE.

});
}
Expand Down Expand Up @@ -385,7 +386,12 @@ private void registerLauncher() {

private void makeStartConfigurationCall(String requestHash,
HashMap<String, String> body,
StandardIntegrityManager.@NotNull StandardIntegrityToken integrityTokenResponse) {
StandardIntegrityManager.StandardIntegrityToken integrityTokenResponse) {
String token = integrityTokenResponse != null ? integrityTokenResponse.token() : "";
if(requestHash == null) {
requestHash = "";
}

new PersonalIdApiHandler<PersonalIdSessionData>() {
@Override
public void onSuccess(PersonalIdSessionData sessionData) {
Expand Down Expand Up @@ -425,7 +431,7 @@ public void onFailure(@androidx.annotation.NonNull PersonalIdOrConnectApiErrorCo
break;
}
}
}.makeStartConfigurationCall(requireActivity(), body, integrityTokenResponse.token(), requestHash);
}.makeStartConfigurationCall(requireActivity(), body, token, requestHash);
}

private void handleIntegritySubError(StandardIntegrityManager.StandardIntegrityToken tokenResponse,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class CCAnalyticsParam {
static final String FORM_ID = "form_id";
static final String REQUEST_ID = "request_id";
static final String RESULT_CODE = "result_code";
static final String TRIGGER = "trigger";

static final String USER_RETURNED = "user_returned";
static final String NOTIFICATION_TYPE = "notification_type";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,19 @@ public static void reportFormUploadAttempt(FormUploadResult first, Integer secon
new String[]{String.valueOf(first), String.valueOf(second)});
}

public static void reportPersonalIdIntegritySubmission(String requestId, String responseCode) {
public static void reportPersonalIdConfigurationIntegritySubmission(String responseCode) {
String label = "PersonalID Configuration";
reportPersonalIdIntegritySubmission(label, label, responseCode);
}

public static void reportPersonalIdHeartbeatIntegritySubmission(String requestId, String responseCode) {
reportPersonalIdIntegritySubmission("Heartbeat", requestId, responseCode);
}

public static void reportPersonalIdIntegritySubmission(String trigger, String requestId, String responseCode) {
Bundle b = new Bundle();
b.putString(CCAnalyticsParam.REQUEST_ID, requestId);
b.putString(CCAnalyticsParam.TRIGGER, trigger);
b.putString(CCAnalyticsParam.RESULT_CODE, responseCode);
reportEvent(CCAnalyticsEvent.PERSONAL_ID_INTEGRITY_REPORTED, b);
}
Expand Down
Loading