Code Security · Obfuscation · Crypto (Android)
Generated 2026-05-11 · Costco Android
Executive summary
Solid baseline: R8 + resource shrinking enabled in release, AES-256-GCM via Android Keystore for token encryption, BiometricPrompt for member-card unlock, NowSecure scans in CI. Notable gaps: API keys committed in gradle.properties, no Play Integrity / SafetyNet, no root detection, FLAG_SECURE absent on payment screens, R8 keep-rules likely over-broad (Hilt/Room/Retrofit annotations require careful exclusions), and one new Random() use in a non-test path.
1. Code obfuscation — R8 / ProGuard
Configuration today
| Setting | Value | Status |
|---|---|---|
minifyEnabled (release) | true | PASS |
shrinkResources (release) | true | PASS |
| ProGuard rules file | Costco/proguard-rules.pro + proguardConfigs/shrink-obfuscate.txt | REVIEW |
| Test ProGuard rules | Costco/test-progaurd-rules.pro | FILENAME TYPO |
Per-module consumer-rules.pro | Present in feature/* and shared/* | PASS |
| R8 mode | Default (full mode in AGP 8+) | PASS |
Common R8 keep-rule gotchas to verify
R8 in full mode is aggressive. The following classes / patterns must be kept or your release builds break in subtle ways:
# Hilt-generated code
-keep class dagger.hilt.** { *; }
-keep class * extends dagger.hilt.android.internal.lifecycle.HiltViewModelFactory { *; }
# Retrofit + Moshi/kotlinx-serialization
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
# kotlinx-serialization adapter requires @Serializable types kept
-keepclasseswithmembers class * {
@kotlinx.serialization.Serializable <methods>;
}
# Room — entities + DAOs cannot be obfuscated
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-keep @androidx.room.Dao interface *
# Glide / Coil — modules and registered components
-keep public class * implements com.bumptech.glide.module.GlideModule
# Firebase Crashlytics — symbolication needs un-mangled stack frames
-keepattributes SourceFile,LineNumberTable
-keep class com.google.firebase.crashlytics.** { *; }
# WebView JS interface methods
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
Findings
Verify R8 doesn't strip Hilt-generated factories
NoSuchFieldException: dagger.hilt.android.internal.modules, an R8 rule has gone too far. ./gradlew :Costco:assembleRelease + smoke test on every release. Capture R8's configuration.txt + seeds.txt in CI artifacts so any keep-rule diff is reviewable.Crashlytics symbolication
-keepattributes SourceFile,LineNumberTable + the Crashlytics gradle plugin upload mapping file. Verify crashlytics.uploadMappingFileEnabled = true in release flavor.mapping.txt as a build artifact for retention beyond Crashlytics' window.Filename typo
test-progaurd-rules.pro — typo (progaurd → proguard). Cosmetic but shows up in every grep.2. Cryptographic primitives — what's used, where
Inventory of crypto-relevant code paths identified in static review:
| Primitive | File | Purpose | Status |
|---|---|---|---|
| AES-256-GCM (AndroidKeyStore) | shared/storage/.../KeyStoreManagerImpl.kt | Token encryption | PASS — recommended algorithm + hardware-backed key |
| BouncyCastle 1.70 | libs catalog | JWT signing / verification | PASS — well-maintained version |
| Nimbus JOSE+JWT 10.8 | libs catalog | JWT handling | PASS |
| NokNok FIDO2 9.2.0 | shared/auth | Passkey / WebAuthn | PASS |
| BiometricPrompt | Costco/.../BiometricUtilImpl.java | Member card unlock | VERIFY — confirm bound to keystore CryptoObject |
java.util.Random | Costco/.../VolleyManagerImpl.java:88 | Likely jitter / retry — should be SecureRandom for any token use | REVIEW |
| EncryptDecryptUtils | shared/storage/.../EncryptDecryptUtils.kt | Wraps AES-GCM | PASS — verify key rotation policy is documented |
Findings
BiometricPrompt should be bound to a keystore-backed key
setAuthenticationParameters(BIOMETRIC_STRONG) + bind to a Keystore-resident SecretKey with setUserAuthenticationRequired(true). The biometric success unlocks a CryptoObject; cryptographic operations on protected data fail without a successful biometric.new Random() in production path
new Random(). Even if it's just retry jitter, the cumulative effect is that the codebase has a non-secure RNG visible — if anyone copies the pattern for a nonce/token, you have a real bug.java.security.SecureRandom. Add a Detekt custom rule banning new Random().Document key rotation policy
3. Secret management — hardcoded keys audit
The Maps API key problem
Google Maps API key committed in gradle.properties
local.properties (gitignored) or a CI secret. Read at build time via Properties. Restrict the key in Google Cloud Console to the production package + production SHA-1; have a separate key for debug.// local.properties (gitignored)
MAPS_API_KEY=AIza...
// Costco/build.gradle
android {
defaultConfig {
def localProps = new Properties()
rootProject.file("local.properties").withInputStream { localProps.load(it) }
manifestPlaceholders = [MAPS_API_KEY: localProps['MAPS_API_KEY'] ?: ""]
}
}
<!-- AndroidManifest.xml -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}"/>Other hardcoded URL audit
CategoryLandingAnalyticsHelperImpl.kt:189 and CategoryLandingViewModel.kt:600 hardcode https://www.costco.com / .ca. Not a secret, but configuration that should come from BuildConfig or remote config.
4. Anti-tampering / anti-debugging / root detection
| Control | Status | Note |
|---|---|---|
| Play Integrity API | ABSENT | No IntegrityManager imports — recommended for fraud-sensitive flows |
| SafetyNet (legacy) | N/A | Deprecated; Play Integrity is the replacement |
| Root detection (RootBeer / custom) | ABSENT | App doesn't detect rooted devices |
| Debugger detection | N/A | android:debuggable="false" in release manifest, but no runtime check |
| Native code obfuscation | N/A | App ships ThreatMetrix native libs — verify those are stripped |
| FLAG_SECURE on sensitive screens | ABSENT | Membership card / DMC / payment screens not protected from screenshot/recents |
| App tamper detection | PARTIAL | ThreatMetrix provides risk score; no app-self integrity |
Adopt Play Integrity API for sensitive flows
MEETS_DEVICE_INTEGRITY, MEETS_BASIC_INTEGRITY, MEETS_STRONG_INTEGRITY) feed into your fraud / authentication backend. For a retail app handling payments, this is table-stakes.MEETS_DEVICE_INTEGRITY = false, deny sensitive operations (payment, member-card display) or step up authentication.Add FLAG_SECURE on sensitive screens
FLAG_SECURE, recent-apps screenshots include the membership barcode. Anyone with physical access can lift it from the carousel.
// MainActivity / each sensitive Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isSensitiveScreen()) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
Root detection via Play Integrity verdict
MEETS_DEVICE_INTEGRITY verdict server-side. The verdict change is also a fraud signal worth feeding into ThreatMetrix.5. Token & session security
| Asset | Storage | Status |
|---|---|---|
| Access token (JWT) | EncryptDecryptUtils → DataStore (AES-256-GCM Keystore) | PASS |
| Refresh token | Same as above | PASS |
| Membership ID | SharedMembershipService → DataStore | VERIFY ENCRYPTION |
| DataStore (plain) — F— flagged earlier | ENCRYPT | |
| Cached lat/lng | DataStore (plain) | CONSIDER ENCRYPTING |
| Device ID for analytics | Volatile — re-fetched per session | PASS |
| Token-revocation hook | Logout flow — VERIFY all tokens cleared | — |
Logout token cleanup
6. Memory safety — wiping sensitive data
Java/Kotlin Strings are immutable and live until garbage collection. After decryption, sensitive data should land in char[] or byte[] and be zeroed when done. Sample of patterns to use:
// Better than String for passwords/tokens
fun authenticate(username: String, password: CharArray): Result {
try {
return doAuthenticate(username, password)
} finally {
Arrays.fill(password, ' ') // zero the array
}
}
// For byte arrays
val tokenBytes = decryptToken()
try {
network.send(buildRequest(tokenBytes))
} finally {
java.util.Arrays.fill(tokenBytes, 0.toByte())
}
Recommendation: Audit the auth flow for String usage of decrypted tokens; convert to CharArray + zero in finally.
7. Secure logging
The Lint Report identifies 231 Log.* calls and 24 print/Log.d sites in production code. Without a release-build filter, these can leak PII to logcat — accessible to any app with diagnostic entitlements (or ADB on USB-debug devices).
// Application.onCreate — install a release tree that scrubs and forwards to Crashlytics
class CostcoApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
Timber.plant(ReleaseTree())
}
}
}
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority < Log.WARN) return // drop verbose/debug/info entirely
val scrubbed = scrubPii(message)
if (t != null) {
FirebaseCrashlytics.getInstance().recordException(t)
}
FirebaseCrashlytics.getInstance().log("${'$'}{tag ?: ""}: ${'$'}scrubbed")
}
private fun scrubPii(s: String): String =
s.replace(EMAIL_REGEX, "[email]")
.replace(MEMBER_ID_REGEX, "[member]")
.replace(PHONE_REGEX, "[phone]")
}
8. Code signing & distribution integrity
| Control | Status |
|---|---|
| Release APK / Bundle signed with v2/v3 signature scheme | VERIFY |
| Play App Signing enrolled | VERIFY (recommended for upload-key separation) |
| Signing keys stored securely | VERIFY (HSM / Cloud KMS / Vault) |
| Release builds run minified | PASS |
| Reproducible builds | N/A for typical Android — Play App Signing covers integrity |
9. Recommendations summary (prioritized)
| Priority | Action | Effort |
|---|---|---|
| NOW | Move Maps API key out of gradle.properties | 1 hour |
| NOW | Add FLAG_SECURE to membership / payment screens | 2 hours |
| NEXT | Adopt Play Integrity for sensitive flows; gate payments / member-card on verdict | 1-2 weeks |
| NEXT | BiometricPrompt → CryptoObject binding | 3-5 days |
| NEXT | Encrypt email + lat/lng at rest | 1 day |
| NEXT | Replace new Random() with SecureRandom; add Detekt rule | 30 min |
| LATER | ReleaseTree with PII scrubbing + Crashlytics forwarding | 1 day |
| LATER | Document key rotation policy; capture R8 mapping in CI artifacts | 1 day |
| LATER | Memory-safety pass on auth flow — convert String → CharArray for sensitive data | 2-3 days |
| ONGOING | Re-baseline NowSecure findings each release; maintain signed CI artifact retention | continuous |