Use when handling sensitive data, authentication, network communication, or before shipping to the Play Store. Three-tier framework (Always Do, Ask First, Never Do) with Android-specific security patterns, plus data privacy and compliance (GDPR/CCPA, Play Data safety, account deletion).
Installs into .claude/skills of the current project.
Are you the author of Security And Hardening?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/guillemroca-security-and-hardening)
---
name: security-and-hardening
description: >-
Use when handling sensitive data, authentication, network communication,
or before shipping to the Play Store. Three-tier framework (Always Do,
Ask First, Never Do) with Android-specific security patterns, plus data
privacy and compliance (GDPR/CCPA, Play Data safety, account deletion).
---
# Security and Hardening
## Overview
Security is a development constraint, not an afterthought. This skill provides a three-tier framework — Always Do, Ask First, Never Do — covering the OWASP Mobile Top 10, Android-specific attack vectors, and hardening patterns for production apps.
## When to Use
- Handling user credentials, tokens, or personal data
- Implementing network communication
- Before shipping any release to the Play Store
- Reviewing code that touches authentication or authorization
- Setting up ProGuard/R8 rules
- Implementing WebView features
- Collecting personal data, adding analytics/ads SDKs, or answering privacy requirements (GDPR, CCPA, Play Data safety)
**Skip when:** Changes are purely cosmetic with no data or network impact.
## Core Process: Three-Tier Framework
### Always Do
1. **Secure data storage.** Jetpack Security Crypto (`EncryptedSharedPreferences`/`MasterKey`) is deprecated and unmaintained — do not add it to new code. Encrypt with a key held in the Android Keystore and persist the ciphertext (DataStore or a file):
```kotlin
// Key lives in the Android Keystore — never leaves secure hardware
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder("auth_token_key", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
val secretKey = keyGenerator.generateKey()
// Encrypt, then persist iv + ciphertext (e.g. in Proto DataStore)
val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { init(Cipher.ENCRYPT_MODE, secretKey) }
val encrypted = cipher.iv + cipher.doFinal(token.toByteArray())
```
Existing apps already on `EncryptedSharedPreferences` can keep it (the format is stable), but plan a migration and never store new categories of secrets with it.
2. **Network Security Config:**
```xml
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<!-- Enforce HTTPS for all connections -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Certificate pinning for your API -->
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2025-12-31">
<pin digest="SHA-256">base64EncodedPin=</pin>
<!-- Backup pin -->
<pin digest="SHA-256">base64EncodedBackupPin=</pin>
</pin-set>
</domain-config>
</network-security-config>
```
```xml
<!-- AndroidManifest.xml -->
<application android:networkSecurityConfig="@xml/network_security_config">
```
Targeting API 37 (Android 17) also changes the network/auth surface: LAN access requires
the `ACCESS_LOCAL_NETWORK` permission, standard SMS OTPs are delayed 3 hours (use the SMS
Retriever API or SMS User Consent instead of reading raw SMS), and Encrypted Client Hello +
Certificate Transparency are on by default for TLS — verify pinning still works.
3. **Input validation:**
```kotlin
// Validate all external input
fun processDeepLink(uri: Uri): DeepLinkResult {
val taskId = uri.getQueryParameter("taskId")
?: return DeepLinkResult.Invalid("Missing taskId")
// Validate format
if (!taskId.matches(Regex("^[a-zA-Z0-9-]{1,36}$"))) {
return DeepLinkResult.Invalid("Invalid taskId format")
}
return DeepLinkResult.Valid(taskId)
}
```
4. **Intent validation for exported components:**
```kotlin
// Validate intents for exported Activities/Receivers
class TaskDeepLinkActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent ?: run { finish(); return }
// Validate the source and data
val taskId = intent.data?.getQueryParameter("taskId")
if (taskId == null || !isValidTaskId(taskId)) {
finish()
return
}
// Process valid deep link
}
}
```
Deeper audits: `android-skills:android-intent-security` and `android-skills:android-permissions-security` (optional Google companion plugin, see README).
5. **Secrets management:**
```properties
# local.properties (NEVER committed to git)
API_KEY=your_secret_key_here
```
```kotlin
// build.gradle.kts
val localProperties = Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) load(file.inputStream())
}
android {
defaultConfig {
buildConfigField("String", "API_KEY",
"\"${localProperties.getProperty("API_KEY", "")}\"")
}
}
```
```gitignore
# .gitignore
local.properties
*.jks
*.keystore
```
6. **Remove debug code for release:**
```kotlin
// build.gradle.kts — strip Log calls in release
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
// proguard-rules.pro
-assumenosideeffects class android.util.Log {
public static int d(...);
public static int v(...);
}
```
### Ask First
7. **Changes that require review:**
- Modifying authentication or authorization logic
- Adding new exported components (Activities, Receivers, Providers)
- Changing certificate pinning configuration
- Adding new permissions to AndroidManifest
- Integrating new third-party SDKs
- Implementing WebView with JavaScript enabled
- Modifying ProGuard/R8 rules
- Changing data encryption or storage approach
### Never Do
8. **Absolute prohibitions:**
| Never | Why |
|-------|-----|
| Commit secrets to git | Secrets in git history are permanent, even after removal |
| Log sensitive data (`Log.d` with tokens) | Logcat is accessible to other apps on rooted devices |
| Trust client-side validation alone | All client validation can be bypassed |
| Use `MODE_WORLD_READABLE` | Any app can read your files |
| Enable `cleartext` traffic in production | Data interceptable on network |
| Store passwords in plain text | Use EncryptedSharedPreferences or Android Keystore |
| Use `WebView.setJavaScriptEnabled(true)` without sanitizing | XSS attacks via injected content |
| Export components without `android:permission` | Any app can invoke them |
| Disable certificate verification | Man-in-the-middle attacks |
| Use `FLAG_SECURE` to prevent screenshots AND log sensitive data | Inconsistent security posture |
### WebView Security
9. **If you must use WebView:**
```kotlin
webView.settings.apply {
javaScriptEnabled = true // Only if necessary
allowFileAccess = false
allowContentAccess = false
domStorageEnabled = true
// Restrict to your domain
setSupportMultipleWindows(false)
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
// Only allow your trusted domains
return url.host != "trusted.example.com"
}
}
// Never add JavaScript interfaces that expose sensitive operations
// webView.addJavascriptInterface(dangerousObject, "Android") // DON'T
```
### Data Privacy & Compliance
10. **Hold less, for less time.** Security asks "can an attacker read it?"; privacy asks "should we hold it at all, and for how long?" Data never collected cannot be breached, mis-declared, or missed in a deletion. Classify every field as you add it:
| Class | Android examples | Handling |
|-------|------------------|----------|
| **Non-personal** | Aggregate counts, app version, crash-free rate | Normal handling |
| **Personal (PII)** | Name, email, IP, user ID, advertising ID, Android ID / other device IDs | Minimize, access-control, include in export/delete, declare in Data safety |
| **Sensitive** | Precise location, health, biometrics, contacts, photos/media, financial data, anything about minors | Explicit basis or consent, Keystore-backed encryption, never in cloud backup or logs |
**Operating rules:**
- **Collect against a stated purpose.** "Might be useful later" is breach scope, not a purpose. Don't log PII — `observability-and-instrumentation` covers log hygiene.
- **Minimize permissions.** Prefer the Photo Picker (`ActivityResultContracts.PickVisualMedia`) over `READ_MEDIA_IMAGES`, `ACCESS_COARSE_LOCATION` over `ACCESS_FINE_LOCATION`, and system pickers/intents over broad read permissions. Every permission you don't hold is data you can't leak.
- **Gate SDKs on consent.** Analytics, ads, and attribution SDKs must not start collecting in `Application.onCreate` before the user has chosen. Default collection off, then enable from the consent result:
```kotlin
// AndroidManifest.xml: <meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
fun applyConsent(analytics: FirebaseAnalytics, granted: Boolean) {
val status = if (granted) ConsentStatus.GRANTED else ConsentStatus.DENIED
analytics.setConsent(mapOf(ConsentType.ANALYTICS_STORAGE to status, ConsentType.AD_STORAGE to status))
analytics.setAnalyticsCollectionEnabled(granted)
}
```
- **Declare what you actually ship.** The Play Data safety form must match real collection, *including every third-party SDK* (analytics, ads, crash reporting, attribution). Check each SDK's own data-disclosure docs and re-check the form whenever an SDK is added or upgraded.
- **Account deletion is a Play requirement.** Apps that let users create an account must offer deletion both in-app and via a web link listed in Play Console, and deleting the account must delete (or anonymize) its associated data — not just flip an `isDeleted` flag.
- **Set retention and make deletion reach every copy.** On sign-out or account deletion, clear Room tables, DataStore, files and caches, cancel WorkManager jobs whose input data carries personal payloads, and trigger server-side and analytics-vendor deletion. Exclude tokens, PII, and anything bound to a Keystore key (undecryptable after restore anyway) from Auto Backup — `android:dataExtractionRules` for API 31+, `android:fullBackupContent` for API 30 and below. Set both if `minSdk` < 31.
- **Design data-subject rights into the schema.** GDPR/CCPA export, correction, and deletion are engineering features: key personal data by user ID so it is *findable* and *erasable*, not smeared across denormalized tables, blobs, and logs.
- **Make policy configurable by region.** Consent requirements and residency rules differ by user location — keep them behind a config boundary rather than hardcoding one jurisdiction.
### OWASP Mobile Top 10 Quick Reference
| # | Risk | Android Mitigation |
|---|------|-------------------|
| M1 | Improper Credential Usage | Android Keystore, EncryptedSharedPreferences |
| M2 | Inadequate Supply Chain Security | Dependency verification, version pinning |
| M3 | Insecure Authentication | BiometricPrompt, OAuth2 with PKCE |
| M4 | Insufficient Input/Output Validation | Validate deep links, intents, API responses |
| M5 | Insecure Communication | Network Security Config, cert pinning |
| M6 | Inadequate Privacy Controls | Minimize data collection, respect permissions |
| M7 | Insufficient Binary Protections | R8/ProGuard, tamper detection |
| M8 | Security Misconfiguration | Review manifest, disable debuggable in release |
| M9 | Insecure Data Storage | EncryptedSharedPreferences, Room with encryption |
| M10 | Insufficient Cryptography | Android Keystore for key management |
## Common Rationalizations
| Shortcut | Why It Fails |
|----------|-------------|
| "It's just a debug key, I'll change it later" | Debug keys committed to git stay in history forever. |
| "Our API already validates input" | Client validation improves UX; server validation prevents exploits. Both needed. |
| "We don't need cert pinning for v1" | v1 is when MITM attacks are most damaging — no monitoring to detect them. |
| "ProGuard breaks too many things" | Configure it properly with `@Keep` annotations. The security cost of skipping is higher. |
| "Collect it now, we might need it later" | Data you don't hold can't be breached or mis-declared. "Might need it" is breach scope, not a purpose. |
| "The SDK handles its own privacy" | You declare its collection in Data safety and you answer for it. Gate it on consent and read its disclosure docs. |
| "Delete account = mark the user deleted" | Play requires the associated data to be deleted. Local DBs, backups, queued work, and vendor copies all hold it. |
## Red Flags
- Secrets (API keys, tokens) in source code
- `android:debuggable="true"` in release manifest
- `clearTextTrafficPermitted="true"` in production
- No Network Security Config
- `Log.d` with user data or tokens
- `MODE_WORLD_READABLE` or `MODE_WORLD_WRITEABLE`
- Exported components without permission checks
- WebView with unrestricted JavaScript
- No ProGuard/R8 in release builds
- Disabled certificate verification
- Analytics/ads SDK initialized in `Application.onCreate` before consent is captured
- New SDK or permission added without a Data safety form update
- Account deletion that only flips a flag, or no web deletion link
- `READ_MEDIA_IMAGES` / `ACCESS_FINE_LOCATION` where the Photo Picker / coarse location would do
- Auth tokens or PII included in Auto Backup (no `dataExtractionRules` / `fullBackupContent`)
## Verification
- [ ] No secrets in source code (grep for API keys, tokens, passwords)
- [ ] Network Security Config present and enforces HTTPS
- [ ] Certificate pinning for production API
- [ ] EncryptedSharedPreferences for sensitive data
- [ ] Input validated at all system boundaries
- [ ] Exported components have permission checks
- [ ] ProGuard/R8 enabled for release builds
- [ ] Debug logging stripped in release (ProGuard rules)
- [ ] `local.properties` in `.gitignore`
- [ ] `android:debuggable` not set (defaults to false for release)
- [ ] WebView (if used) restricts domains and JavaScript exposure
- [ ] Every personal-data field classified and tied to a stated purpose
- [ ] Analytics/ads SDKs collect nothing before consent (verify with a network inspector on a fresh install)
- [ ] Data safety form matches actual collection, including third-party SDKs
- [ ] Account deletion works in-app and via web link, and removes local, backup, queued, server, and vendor copies
- [ ] Backup rules exclude tokens/PII for both API 31+ and API 30-and-below paths