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
| Severity | Finding | Where | Fix |
|---|---|---|---|
| Critical | Release keystore committed to git | app/config/keystore.jks, app/src/main/res/values/keystore.jks | Rotate upload key, enrol Play App Signing, purge from history |
| Critical | Keystore store and key passwords committed | app/build.gradle:25-28, data/build.gradle:11,13, app/config/signing.properties:1-3 | Rotate; move to CI secrets / local gradle.properties outside git |
| High | Mindtree Nexus password with a personal employee email committed | app/config/publish.properties:7-9 | Rotate; delete the unused PlusOne publish config |
| High | Private Nexus credentials inline | build.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-54 | Rotate; read from env / ~/.gradle/gradle.properties |
| High | Login 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 same | account-lib/.../AuthenticatorActivity.java:425-429, AndroidSessionManager.java:46, helper/LoginHelper.java:51; bridge-security-api BridgeSessionHelper.createSession | Token-based login in the App Service; no credential or token logging; strip Log.d in release |
| Medium | Gateway 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 |
| High | Sync operations sent to Firestore with request headers (session), body (farmer PII) and raw response | data/.../utils/FireStoreUtil.kt:83-110 (addUpdateOperationInFireStore); usernames at line 179 | Stop now; replace with redacted server-side telemetry |
| High | Client holds the FCM legacy server key and sends pushes from the device to fcm.googleapis.com/fcm/send | app/.../datasource/network/responsedto/AppConstantDTO.kt:19,29,31; data/.../fcm/FcmUtil.kt:17,60-73; callers data/.../sync/rh/EndorsementPostRH.kt:27, EndorsementCommentPostRH.kt:28 | Remove code; revoke key; send notifications from the backend |
| High | Release build not minified or obfuscated | app/build.gradle and data/build.gradle release block | Turn on R8 + resource shrinking with keep rules |
| High | HttpLoggingInterceptor.Level.BODY always on, so auth headers and payloads reach logcat in release | app/.../view/deviceregistration/RetrofitProvider.kt:34-36 | Gate on BuildConfig.DEBUG; redact X-SESSIONID |
| Medium | User password stored in plaintext in AccountManager and reused for silent re-login | app/.../view/home/HomeActivity.kt:384-385, SettingsActivity.kt:476-477; account-lib AuthenticatorActivity.java:553, Authenticator.getAuthToken | Rewrite: refresh tokens only. Interim: nothing safe short of the auth change |
| Medium | Session token written to Log.d | account-lib AndroidSessionManager.java:54 | Remove log line |
| Medium | Ungated logging of FCM token, raw responses and bodies | app/.../fcm/FirebaseMessageReceiver.kt:36, app/.../datasource/service/SetupServiceHelper.kt:44,62,79, app/.../utility/FarmerDataUpdateWorker.kt:85; 174 printStackTrace, 35 println | Route through one facade that strips in release |
| Medium | StubContentProvider exported with grantUriPermissions | app/src/main/AndroidManifest.xml:177-182 | Set exported=false if the sync adapter allows; removed at rewrite |
| Medium | Sync failure broadcast is implicit and unprotected; carries status, error and account | offline-android SyncCallback | Removed at rewrite (use in-process state) |
| Medium | FileProvider exposes external storage root and files root | app/src/main/res/xml/file_paths_public.xml | Restrict to the image cache directory |
| Medium | Local DBs (farmer PII, land polygons) unencrypted; whole DBs zipped for support upload | data/.../db/AppDatabase.kt, app/.../utility/ZipUtil.kt, app/.../utility/BackupUtil.kt | SQLCipher or Keystore-backed encryption; redact before upload |
| Medium | Logs written to public Downloads | app/.../utility/LogsToSD.java:21-48 | Delete; use app-private storage |
| Medium | Auth interceptor re-logs in on HTTP 400, not only 401 | data/.../net/AuthInterceptor.kt | Refresh on 401 only |
| Low | APP_KEY, base URLs and federation URL in resource strings | app/src/release/res/values/build_type_config.xml:7, app/src/debug/res/values/build_type_config.xml:7 | Drop APP_KEY from the client; use Play Integrity at the App Service |
| Low | Firebase API keys committed (normal, but unrestricted) | app/src/release/google-services.json:29, app/src/debug/google-services.json:21 | Restrict keys to package + SHA in GCP |
| Low | Device id is Settings.Secure.ANDROID_ID | account-lib login | App-scoped UUID |
| Low | .gitignore lists app/build.gradle and gradle.properties yet both are tracked | .gitignore | Fix the ignore list once secrets are out |
| Low | jcenter() and mavenLocal() repositories | build.gradle | Remove |
| Info | Crashlytics setUserId with the login id | SplashActivity.kt:150, SettingsActivity.kt:386,465 | Use a pseudonymous id |
PII flows
Data leaves the device through channels other than the domain APIs.
| Channel | What is sent | Trigger | Where |
|---|---|---|---|
Firestore SYNC_LOG | Full queued operation: URL, headers incl. session, farmer body, raw response | Every 10th failed attempt | data/.../utils/FireStoreUtil.kt:83-110 |
| Firestore user migration log | Usernames | AP-gov migration | FireStoreUtil.kt:179 |
Firestore PLI_IMAGE, URVI_ATTACHMENT_ISSUE | Image metadata, farmer / PLI ids | Image upload fallback after N failures | data/.../fcm/FirebaseStorageUtil.kt |
| Firebase Storage | Farm images at userId/farmerId/pliId/pliId.<ext> | Same fallback | FirebaseStorageUtil.kt |
| Firestore / Storage DB zip | Zipped app databases | Clear app data, logout | app/.../utility/ZipUtil.kt, HomeActivity.kt:957-959 |
POST sync-service/api/v1/data/so | Whole sync-queue table (syncQueueData), cadre login, app version | Every 6 h (SendSyncDetailsDataWorker), Settings "send data", before clear data | app/.../datasource/service/SendSyncDetailsDataWorker.kt |
POST sync-service/api/v1/data/clickstream | Screen, action, API endpoint | Every 12 h (SendUserAnalyticsWorker) | app/.../utility/AnalyticsManager.kt:67-97 |
PATCH urvi-app-service/.../remote-app-actions/{id}/execution | Zip of app DBs | Remote LOGOUT / CLEAR_APP_DATA | DeviceRegistrationViewModel.kt:285-325 |
Room ErrorLogEntity (local) | URL, body, headers, raw response of each failure | Every failure | BaseResponseHandler.logErrorInDb |
| FCM topics | Farmer name, entity status, reviewer | Endorsement events, to every device on the topic | data/.../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.propertiesand inline credentials from git history. - Stop writing headers and bodies to Firestore
SYNC_LOG; redact or disable. - Strip
X-SESSIONIDand request bodies from thesync-service/sopayload. - Gate
HttpLoggingInterceptorBODY level on debug builds. - Remove the token
Log.d/Log.ilines 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
| Area | Target |
|---|---|
| Login | OIDC 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 |
| Tokens | Short-lived access token + refresh token. Refresh on 401 only, once, then sign out |
| Storage | Refresh token in EncryptedDataStore / Keystore (Compose) or flutter_secure_storage (Flutter). Never store the password |
| App identity | No APP_KEY in the APK. Play Integrity (and App Attest if iOS) verified by the App Service |
| Device id | App-scoped UUID in secure storage |
| Local data | Encrypted DB for farmer PII and polygons |
| Telemetry | One logging facade with PII redaction; telemetry to the App Service, redacted, no headers |
| Push | Backend sends targeted notifications using registered FCM tokens; no topic fan-out of farmer data |
| Uploads | Presigned URLs from the App Service; no Firebase Storage fallback |
| Build | R8 + resource shrinking, signing keys in CI only, build flavors per environment, no jcenter / mavenLocal |
| Transport | HTTPS only (default at targetSdk 36); consider certificate pinning to the App Service host |
| Remote control | Keep remote LOGOUT / CLEAR_APP_DATA, but upload only redacted diagnostics |