Security Best Practices
This lesson is a roundup rather than a single deep dive — a reference for the security habits worth carrying into every TrailLog feature you build for the rest of this track, from storing an API key to displaying a screen with sensitive data on it.
Never Hardcode Secrets in Source Code
An API key or backend URL typed directly into a Kotlin file gets compiled straight into the APK —
anyone can decompile the app and read it back out in plain text. At minimum, secrets like TrailLog's
weather API key belong in local.properties (which stays out of version control) and
get exposed to your code as generated BuildConfig fields instead:
| Instead of... | Do this |
|---|---|
val apiKey = "sk_live_abc123" in a .kt file | Read it from local.properties at build time and expose it as BuildConfig.WEATHER_API_KEY |
Committing local.properties to Git | Keep it listed in .gitignore — it should never reach version control at all |
This isn't a complete defense — anything shipped in the APK can eventually be extracted by a determined attacker — but it keeps secrets out of source history and off of every developer's screen who doesn't strictly need them.
EncryptedSharedPreferences for Genuinely Sensitive Local Data
DataStore Preferences covered Jetpack DataStore as
the modern replacement for SharedPreferences — but it's worth being clear that plain DataStore is
not encrypted. It's fine for ordinary app preferences (a chosen distance unit, a
"dark mode enabled" flag), but it's the wrong place for something genuinely sensitive, like a
cached auth token. For that rare case, the Jetpack Security library's
EncryptedSharedPreferences transparently encrypts both keys and values on disk:
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"traillog_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
Reach for this only when the data genuinely warrants it — encrypting everything by default adds overhead for no benefit when the data was never sensitive in the first place.
Network Security Config: Enforcing HTTPS-Only
A Network Security Config file lets you declare, outside of code, exactly which network behavior your app permits — most importantly, blocking any accidental plain HTTP traffic and optionally pinning the exact certificate your backend must present.
res/xml/network_security_config.xml
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config>
<domain includeSubdomains="true">api.traillog.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
</pin-set>
</domain-config>
</network-security-config>
cleartextTrafficPermitted="false" refuses any unencrypted HTTP connection outright, and
the pin-set block adds certificate pinning — the app will only trust connections to
api.traillog.example.com presenting the exact certificate matching that pinned hash,
which blocks even a compromised or forged trusted certificate authority from intercepting traffic.
This file is referenced from the manifest via
android:networkSecurityConfig="@xml/network_security_config". Set a real expiration and
rotate pins before they lapse, or the app will refuse to connect once the pin set expires.
HTTPS Is the Non-Negotiable Baseline
Every Retrofit base URL built in Retrofit + Coroutines
should start with https://, no exceptions — plain HTTP sends trail data, auth tokens,
and everything else across the network in the clear, readable by anyone on the same network. The
Network Security Config above is a backstop that catches accidental cleartext traffic; it isn't a
substitute for simply always using HTTPS URLs in the first place.
FLAG_SECURE for Screens Showing Sensitive Info
If TrailLog ever shows something genuinely sensitive on screen — an account recovery code, a
payment detail — setting FLAG_SECURE on that Activity's window blocks screenshots and
screen recordings of it, and blanks it out of the recent-apps app switcher preview:
override fun onCreate(savedInstanceState: Bundle?) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_account_recovery)
}
Apply this narrowly, only on the specific screen that needs it — setting it app-wide would also block the user from taking a perfectly harmless screenshot of their own trail list.
Input Validation: A UX Nicety, Never a Substitute
Checking that a trail name isn't empty or a distance field contains a valid number before submitting is good UX — it gives the user fast, friendly feedback. But client-side validation can always be bypassed by anyone calling your API directly, so every one of those same checks has to be re-enforced on the actual backend. Treat client-side validation purely as a convenience for honest users, never as your app's real line of defense.
ProGuard/R8: One More Layer, Not the Whole Answer
Everything above protects data in transit and at rest. It's worth flagging one more layer that protects the compiled app itself: ProGuard/R8 shrinks and obfuscates your code at build time, making a decompiled APK meaningfully harder to reverse-engineer. It's not a substitute for anything covered in this lesson — an obfuscated hardcoded secret is still a leaked secret — but it's a real, worthwhile layer on top. The next lesson, ProGuard / R8, covers it in full.