Security

The current app commits its signing key and build credentials, logs and ships session headers and farmer PII to Firestore, and stores user passwords in plaintext, so several fixes must land now, before any rewrite.

No secret values appear on this page. Locations only. Paths use app/... for app/src/main/java/org/platformcommons/urvi/ and data/... for data/src/main/java/org/mindtree/app/agricultor/data/.

Findings

SeverityFindingWhereFix
CriticalRelease keystore committed to gitapp/config/keystore.jks, app/src/main/res/values/keystore.jksRotate upload key, enrol Play App Signing, purge from history
CriticalKeystore store and key passwords committedapp/build.gradle:25-28, data/build.gradle:11,13, app/config/signing.properties:1-3Rotate; move to CI secrets / local gradle.properties outside git
HighMindtree Nexus password with a personal employee email committedapp/config/publish.properties:7-9Rotate; delete the unused PlusOne publish config
HighPrivate Nexus credentials inlinebuild.gradle:16-17,49-50; account-lib/build.gradle:11-12,21-22,44-45,54-55; offline-android/build.gradle:11-12,21-22,43-44,53-54Rotate; read from env / ~/.gradle/gradle.properties
HighLogin sends the password as a plain X-PASS request header; account-lib logs the session id, the X-APPKEY value and the password length to logcat on every login (seen in a live capture on 2026-10-22). The library uses Log.d and release builds are not minified, so release builds log the sameaccount-lib/.../AuthenticatorActivity.java:425-429, AndroidSessionManager.java:46, helper/LoginHelper.java:51; bridge-security-api BridgeSessionHelper.createSessionToken-based login in the App Service; no credential or token logging; strip Log.d in release
MediumGateway CORS answers with Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true, and exposes X-USER, X-PASS and X-SESSIONID (observed on dev)Server config for dev.platformcommons.org gateway (backend)Allow-list origins; never expose credential headers
HighSync operations sent to Firestore with request headers (session), body (farmer PII) and raw responsedata/.../utils/FireStoreUtil.kt:83-110 (addUpdateOperationInFireStore); usernames at line 179Stop now; replace with redacted server-side telemetry
HighClient holds the FCM legacy server key and sends pushes from the device to fcm.googleapis.com/fcm/sendapp/.../datasource/network/responsedto/AppConstantDTO.kt:19,29,31; data/.../fcm/FcmUtil.kt:17,60-73; callers data/.../sync/rh/EndorsementPostRH.kt:27, EndorsementCommentPostRH.kt:28Remove code; revoke key; send notifications from the backend
HighRelease build not minified or obfuscatedapp/build.gradle and data/build.gradle release blockTurn on R8 + resource shrinking with keep rules
HighHttpLoggingInterceptor.Level.BODY always on, so auth headers and payloads reach logcat in releaseapp/.../view/deviceregistration/RetrofitProvider.kt:34-36Gate on BuildConfig.DEBUG; redact X-SESSIONID
MediumUser password stored in plaintext in AccountManager and reused for silent re-loginapp/.../view/home/HomeActivity.kt:384-385, SettingsActivity.kt:476-477; account-lib AuthenticatorActivity.java:553, Authenticator.getAuthTokenRewrite: refresh tokens only. Interim: nothing safe short of the auth change
MediumSession token written to Log.daccount-lib AndroidSessionManager.java:54Remove log line
MediumUngated logging of FCM token, raw responses and bodiesapp/.../fcm/FirebaseMessageReceiver.kt:36, app/.../datasource/service/SetupServiceHelper.kt:44,62,79, app/.../utility/FarmerDataUpdateWorker.kt:85; 174 printStackTrace, 35 printlnRoute through one facade that strips in release
MediumStubContentProvider exported with grantUriPermissionsapp/src/main/AndroidManifest.xml:177-182Set exported=false if the sync adapter allows; removed at rewrite
MediumSync failure broadcast is implicit and unprotected; carries status, error and accountoffline-android SyncCallbackRemoved at rewrite (use in-process state)
MediumFileProvider exposes external storage root and files rootapp/src/main/res/xml/file_paths_public.xmlRestrict to the image cache directory
MediumLocal DBs (farmer PII, land polygons) unencrypted; whole DBs zipped for support uploaddata/.../db/AppDatabase.kt, app/.../utility/ZipUtil.kt, app/.../utility/BackupUtil.ktSQLCipher or Keystore-backed encryption; redact before upload
MediumLogs written to public Downloadsapp/.../utility/LogsToSD.java:21-48Delete; use app-private storage
MediumAuth interceptor re-logs in on HTTP 400, not only 401data/.../net/AuthInterceptor.ktRefresh on 401 only
LowAPP_KEY, base URLs and federation URL in resource stringsapp/src/release/res/values/build_type_config.xml:7, app/src/debug/res/values/build_type_config.xml:7Drop APP_KEY from the client; use Play Integrity at the App Service
LowFirebase API keys committed (normal, but unrestricted)app/src/release/google-services.json:29, app/src/debug/google-services.json:21Restrict keys to package + SHA in GCP
LowDevice id is Settings.Secure.ANDROID_IDaccount-lib loginApp-scoped UUID
Low.gitignore lists app/build.gradle and gradle.properties yet both are tracked.gitignoreFix the ignore list once secrets are out
Lowjcenter() and mavenLocal() repositoriesbuild.gradleRemove
InfoCrashlytics setUserId with the login idSplashActivity.kt:150, SettingsActivity.kt:386,465Use a pseudonymous id

PII flows

Data leaves the device through channels other than the domain APIs.

ChannelWhat is sentTriggerWhere
Firestore SYNC_LOGFull queued operation: URL, headers incl. session, farmer body, raw responseEvery 10th failed attemptdata/.../utils/FireStoreUtil.kt:83-110
Firestore user migration logUsernamesAP-gov migrationFireStoreUtil.kt:179
Firestore PLI_IMAGE, URVI_ATTACHMENT_ISSUEImage metadata, farmer / PLI idsImage upload fallback after N failuresdata/.../fcm/FirebaseStorageUtil.kt
Firebase StorageFarm images at userId/farmerId/pliId/pliId.<ext>Same fallbackFirebaseStorageUtil.kt
Firestore / Storage DB zipZipped app databasesClear app data, logoutapp/.../utility/ZipUtil.kt, HomeActivity.kt:957-959
POST sync-service/api/v1/data/soWhole sync-queue table (syncQueueData), cadre login, app versionEvery 6 h (SendSyncDetailsDataWorker), Settings "send data", before clear dataapp/.../datasource/service/SendSyncDetailsDataWorker.kt
POST sync-service/api/v1/data/clickstreamScreen, action, API endpointEvery 12 h (SendUserAnalyticsWorker)app/.../utility/AnalyticsManager.kt:67-97
PATCH urvi-app-service/.../remote-app-actions/{id}/executionZip of app DBsRemote LOGOUT / CLEAR_APP_DATADeviceRegistrationViewModel.kt:285-325
Room ErrorLogEntity (local)URL, body, headers, raw response of each failureEvery failureBaseResponseHandler.logErrorInDb
FCM topicsFarmer name, entity status, reviewerEndorsement events, to every device on the topicdata/.../fcm/FCMConstant.kt

Queued operations hold a session token stamped at enqueue time, so every dump of the queue (Firestore, so, DB zip) also ships a token.

Do now regardless of rewrite

  • Confirm whether Play App Signing is on. Rotate the upload key (or the signing key if not).
  • Rotate keystore passwords and move them to CI secrets.
  • Rotate all Nexus credentials (platformcommons Nexus and legacy Mindtree Nexus).
  • Purge keystore.jks, signing.properties, publish.properties and inline credentials from git history.
  • Stop writing headers and bodies to Firestore SYNC_LOG; redact or disable.
  • Strip X-SESSIONID and request bodies from the sync-service/so payload.
  • Gate HttpLoggingInterceptor BODY level on debug builds.
  • Remove the token Log.d / Log.i lines and public-Downloads log file.
  • Revoke the FCM legacy server key and delete the client-side send path (it is already dead).
  • Restrict Firebase API keys in GCP.
  • Narrow file_paths_public.xml.
  • Enable R8 on release (needs keep rules for reflected handler and integrator classes in the sync queue).

Target security model for the rewrite

AreaTarget
LoginOIDC Authorization Code + PKCE, or POST /auth/login on the App Service. Bridge session creation and device registration run server-side; the Bridge session never reaches the phone
TokensShort-lived access token + refresh token. Refresh on 401 only, once, then sign out
StorageRefresh token in EncryptedDataStore / Keystore (Compose) or flutter_secure_storage (Flutter). Never store the password
App identityNo APP_KEY in the APK. Play Integrity (and App Attest if iOS) verified by the App Service
Device idApp-scoped UUID in secure storage
Local dataEncrypted DB for farmer PII and polygons
TelemetryOne logging facade with PII redaction; telemetry to the App Service, redacted, no headers
PushBackend sends targeted notifications using registered FCM tokens; no topic fan-out of farmer data
UploadsPresigned URLs from the App Service; no Firebase Storage fallback
BuildR8 + resource shrinking, signing keys in CI only, build flavors per environment, no jcenter / mavenLocal
TransportHTTPS only (default at targetSdk 36); consider certificate pinning to the App Service host
Remote controlKeep remote LOGOUT / CLEAR_APP_DATA, but upload only redacted diagnostics