Android Security: Kotlin Coding Practices & Intent Handling
π§ Kotlin & Android Coding Terms You Must Know
?.let {}
: Kotlin null-safe scope function. Prevents crashes if an object is null; not a security check.getCallingPackage()
: Identifies the app that sent an intent. Use this to verify the intent source.checkCallingOrSelfPermission()
: Checks if a permission is declared. Does not verify the actual identity of the sender.resolveActivity(intent, 0)
: Checks if the intent can be handled. Prevents app crashes, but not a security check.putExtra()
/getStringExtra()
: Used to send/get data via intents. Can be spoofed if not validated.android:exported="true"
: Allows external apps to access a component. Must be paired with permissions or signature checks.android:permission="..."
: Declares the required permission for component access. Best practice for services and receivers.Explicit Intent: Targets a known component. Safer, no hijacking.
Implicit Intent: System matches via filters. Can be hijacked or intercepted.
BroadcastReceiver: Listens for system/app events. Can be abused if exported & unprotected.
Service: Background task handler. Must verify who sends commands (
onStartCommand()
).
π Encryption & Authentication Distinctions
FDE (Full-Disk Encryption): Encrypts all at once with a single key. Unlocks everything at once; no file-level control.
FBE (File-Based Encryption): Encrypts each file with its own key. Allows selective access (e.g., alarms before unlock).
HMAC: Ensures integrity, not confidentiality. Cannot encrypt or decrypt data.
AES: Symmetric encryption for data protection. Used to encrypt/decrypt entire payloads.
JWT: Signed token used in authentication. Stateless, hard to revoke without server state.
Nonce: One-time-use number. Stops replay attacks.
π‘οΈ Secure Intent Handling Rules
Always verify:
getCallingPackage()
orgetCallingUid()
Permissions via
checkCallingOrSelfPermission()
Whether the intent can be handled via
resolveActivity()
Secure intent pattern example:
incomingIntent?.let { intent ->
if (packageManager.resolveActivity(intent, 0) != null &&
checkCallingOrSelfPermission("com.example.SAFE_PERMISSION") == PackageManager.PERMISSION_GRANTED &&
getCallingPackage() == "com.trusted.app") {
processIntent(intent)
}
}
Donβt:
Rely on
?.let {}
for securityTrust extras without checking the source
Export components without protection
β οΈ High-Risk Misconceptions to Avoid
β HMAC encrypts data β HMAC only checks integrity
β Kotlin
let{}
validates sender β It just checks for nullβ Implicit intents are always safe β They can be hijacked by any app with a matching filter
β A signed JWT cannot be reused β It can, unless short-lived or bound to a nonce
β Logging tokens to Logcat is fine β Logcat can be accessed on rooted/debug builds
β Certificate pinning is optional β Without it, MITM with a rogue CA is possible
β Bootloader fails β kernel loads anyway β Secure boot halts the process on any failure
β RASP replaces other layers β It complements OS/hardware protections, doesnβt replace them
π§ Biometric & Hardware Memory Aids
TEE (Trusted Execution Environment): Handles biometric processing and secure key storage.
HSM (Hardware Security Module): Offloads crypto, prevents physical key extraction.
Secure Boot Chain: Must validate Bootloader β Kernel β System.
MAC Address Randomization: Prevents device tracking via Wi-Fi/Bluetooth.
π± Intent-Related Attacks to Know
Intent Spoofing: A fake app sends a malicious intent to a component.
Intent Hijacking: Another app intercepts an intent meant for your component.
Broadcast Injection: A malicious app sends a broadcast your app reacts to.
Service Hijack: A malicious app invokes exported service methods (e.g., deleteUser()).
π§ͺ Replay Attack Mitigation Checklist
β Use short token lifetimes
β Include a nonce or timestamp
β Store tokens in hardware-backed keystore
β Donβt store in plain text
β Donβt trust tokens after logout or timeout
β Key Kotlin Coding Lessons
Validate All Incoming Intents
incomingIntent?.let { intent ->
if (packageManager.resolveActivity(intent, 0) != null &&
checkCallingOrSelfPermission("com.example.SAFE_PERMISSION") == PackageManager.PERMISSION_GRANTED &&
getCallingPackage() == "com.trusted.sender") {
processIntent(intent)
}
}
Defends against spoofed intents, broadcast injection, and hijacking.
Avoid Sending Sensitive Data via
putExtra()
Unless Secured
val intent = Intent(this, DashboardActivity::class.java).apply {
putExtra("session_token", sessionToken) // Risky if not encrypted or validated
}
Extras can be read by malicious apps if intercepted.
Explicit vs. Implicit Intents
// Explicit (Safe)
val intent = Intent(this, TargetActivity::class.java)
startActivity(intent)
// Implicit (Risky)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://nyu.edu"))
startActivity(intent)
Use explicit for internal navigation.
Validate implicit before processing.
π§± Intent Verification Checklist
Confirm
intent
is not null.Use
resolveActivity()
to check if the component exists.Use
checkCallingOrSelfPermission()
to check permissions.Use
getCallingPackage()
orgetCallingUid()
to verify the sender.
π‘οΈ Signature-Level Permissions
Only apps signed with the same certificate can access the component.
π Token Handling Best Practices
Store tokens in
Android Keystore
orEncryptedSharedPreferences
Never log tokens (
Log.d()
,println()
)Use short-lived and refreshable tokens
π‘ Secure BroadcastReceiver Pattern
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.getStringExtra("msg") != null &&
context.checkCallingOrSelfPermission("com.example.ALARM_PERMISSION") == PackageManager.PERMISSION_GRANTED) {
showAlarm(intent.getStringExtra("msg")!!)
}
}
}
Add the corresponding
android:permission
in the manifest to protect access.
β οΈ Common Mistakes to Spot on the Exam
Using
intent.getStringExtra()
without checking the sender β SpoofableLogging tokens or user data with
Log.d()
β Sensitive info leakStoring tokens in plain
SharedPreferences
β Easily accessibleCalling
startActivity()
on a null intent β App crash risk