Security And Hardening
CommunityGuillemRoca
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).
Overview
- Repository
- GuillemRoca/agent-skills-android
- Publisher
- GuillemRoca
- Stars
- 6
- Forks
- 3
- Bundled files
- 0
- License
- MIT
- Updated
- Oct 3, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/GuillemRoca/agent-skills-android.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/security-and-hardening" ~/.claude/skills/security-and-hardening
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/GuillemRoca/agent-skills-android.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skills/security-and-hardening" ~/.codex/skills/security-and-hardening
rm -rf "$tmp"Install the Security And Hardening agent skill from https://github.com/GuillemRoca/agent-skills-android/tree/main/skills/security-and-hardening into this project's skills folder.Instructions
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
- 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):
// 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.
- Network Security Config:
<!-- 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><!-- 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.
- Input validation:
// 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)
}- Intent validation for exported components:
// 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).
- Secrets management:
# local.properties (NEVER committed to git)
API_KEY=your_secret_key_here// 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
local.properties
*.jks
*.keystore
- Remove debug code for release:
// 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
- 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
- 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
- If you must use WebView:
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'TData Privacy & Compliance
- 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-instrumentationcovers log hygiene. - Minimize permissions. Prefer the Photo Picker (
ActivityResultContracts.PickVisualMedia) overREAD_MEDIA_IMAGES,ACCESS_COARSE_LOCATIONoverACCESS_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.onCreatebefore the user has chosen. Default collection off, then enable from the consent result:
// 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
isDeletedflag. - 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:dataExtractionRulesfor API 31+,android:fullBackupContentfor API 30 and below. Set both ifminSdk< 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 manifestclearTextTrafficPermitted="true"in production- No Network Security Config
Log.dwith user data or tokensMODE_WORLD_READABLEorMODE_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.onCreatebefore 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_LOCATIONwhere 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.propertiesin.gitignore -
android:debuggablenot 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
FAQ
What does Security And Hardening do?
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).
Who made Security And Hardening?
GuillemRoca (a community member) publishes it in the GuillemRoca/agent-skills-android repository on GitHub.
Is Security And Hardening free?
Yes. Security And Hardening is open source, published by GuillemRoca under the MIT license.
What comes with Security And Hardening?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Security And Hardening?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.