Building an Android application is not only about creating a good UI and
connecting it to an API.
Modern Android applications handle sensitive information such as
authentication tokens, personal data, financial information, files,
location data, and business-critical operations.
That makes security an architectural concern --- not something that
should be added just before release.
One principle every Android developer should understand is:
Never treat the Android client as a trusted environment.
The application runs on a device that the developer does not control.
The APK can be inspected, application behavior can be analyzed, network
requests can be observed, and locally stored data can potentially be
accessed on a compromised device.
So, what should we consider when building a secure Android application?
1. Never Store Secrets in the APK
One of the most common mistakes is putting secrets directly into
application code.
For example:
const val SECRET_KEY = "my-secret-key"
const val API_KEY = "123456789"
At first glance, this might look harmless.
But Android applications are distributed to users as APKs or app bundles
that ultimately produce installable application code. An attacker can
obtain an APK and analyze its resources, strings, bytecode, and
behavior.
Even if you move the value into another file:
object AppConfig {
const val API_SECRET = "super-secret-value"
}
it is still part of the application.
Changing the location of the secret does not change the security model.
What should stay out of the application?
Examples include:
- Database passwords
- Private API credentials
- Backend service credentials
- Private cryptographic keys
- Cloud service secrets
- Signing credentials
- Administrative credentials
- Internal service authentication tokens
These should generally be kept on infrastructure controlled by the
backend.
What about API keys?
Not every API key is a secret.
Some public client identifiers are intentionally distributed with
applications. Certain SDKs require an application identifier that is not
designed to be confidential.
The important question is:
Does possession of this value allow an attacker to perform a
privileged operation?
If yes, don't treat the Android APK as a secure place to store it.
2. BuildConfig Is Not a Secret Store
A common approach is:
buildConfigField(
"String",
"API_KEY",
"\"my-secret-key\""
)
and then:
BuildConfig.API_KEY
This can be useful for configuration, but it does not make a secret
secure.
The value is still packaged into the application.
The same applies to:
-
local.properties -
gradle.properties -
strings.xml -
res/raw -
assets/ -
BuildConfig - Kotlin constants
These can help with configuration management during development, but
they shouldn't be considered a secure runtime secret store.
A better architecture is:
┌──────────────────┐
│ Android App │
└────────┬─────────┘
│
│ Authenticated Request
▼
┌──────────────────┐
│ Backend │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Secret / Private │
│ Configuration │
└──────────────────┘
The Android application receives only the information it actually needs.
3. Never Put Backend Credentials in the Mobile App
Consider an application that needs to communicate with:
Android App
↓
Backend API
↓
Internal Service
↓
Database
A dangerous design would be:
Android App
↓
Internal Service
↓
Database
with the Android application containing credentials for the internal
service.
If the APK is compromised, those credentials can potentially be
extracted and reused outside the application.
A better approach is:
Android App
↓
Authenticated Backend API
↓
Internal Services
↓
Database
The backend becomes the security boundary.
4. Authentication Is Not Authorization
These two concepts are often confused.
Authentication
Authentication answers:
Who are you?
Examples include:
- Username and password
- OTP
- Passkeys
- OAuth
- Access tokens
- Biometric unlock
Authorization
Authorization answers:
What are you allowed to do?
For example:
User A → Can access User A's profile
User A → Cannot access User B's profile
A user can be successfully authenticated and still be unauthorized to
access a particular resource.
A secure architecture should therefore look like:
Android App
|
| Request
▼
Backend
|
├── Authenticate
|
├── Authorize
|
├── Validate Input
|
├── Check Resource Ownership
|
▼
Response
Never depend exclusively on Android UI restrictions.
For example:
if (user.isAdmin) {
showAdminPanel()
}
This controls the UI.
It does not provide backend authorization.
An attacker can potentially bypass the UI and call the API directly.
The backend must enforce the permission.
5. Protect Access and Refresh Tokens
Modern applications commonly use access and refresh tokens.
A simplified model is:
Login
↓
Access Token
↓
API Requests
Refresh Token
↓
New Access Token
The access token should be treated as sensitive information.
Avoid storing tokens carelessly:
sharedPreferences
.edit()
.putString("token", token)
.apply()
The appropriate storage strategy depends on the application's threat
model and token design.
For sensitive cryptographic keys, Android provides the Android
Keystore system.
The architecture should consider:
Token
↓
Where is it stored?
↓
Who can access it?
↓
How long is it valid?
↓
Can it be revoked?
↓
What happens after logout?
Security isn't simply about where the string is stored.
Token lifetime, revocation, rotation, server-side validation, and
session management are equally important.
6. Android Keystore
When cryptographic keys are required on Android, the Android Keystore
provides a platform mechanism for key management.
Conceptually:
Application
|
v
Crypto Operation
|
v
Android Keystore
|
v
Protected Key Material
The application can use the key for cryptographic operations without
treating the key as an ordinary string that is freely passed around the
application.
For example, an application may generate a key:
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
"app_key",
KeyProperties.PURPOSE_ENCRYPT or
KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_NONE
)
.build()
)
val secretKey = keyGenerator.generateKey()
The exact cryptographic configuration should be chosen according to the
application's requirements.
The important lesson is:
Don't invent your own key-storage mechanism when the platform
already provides one.
7. Encryption Alone Does Not Make an Application Secure
Developers sometimes say:
"We use AES-256, so our data is secure."
That's incomplete.
You also need to understand:
- How is the key generated?
- Where is the key stored?
- How is the key exchanged?
- How is the key rotated?
- What encryption mode is being used?
- How are IVs/nonces generated?
- How is integrity/authentication provided?
- What happens if the key is compromised?
- Can old data still be decrypted?
- Is encryption being applied at the correct layer?
For example:
Data
↓
Encryption
↓
Storage
is only part of the design.
You also need:
Key Management
+
Access Control
+
Integrity Protection
+
Secure Storage
+
Lifecycle Management
Cryptography is a system, not simply an algorithm name.
8. Use Authenticated Encryption Where Appropriate
When encrypting application data, confidentiality alone is not always
enough.
You generally also need protection against unauthorized modification.
Conceptually:
Plaintext
↓
Encryption + Integrity Protection
↓
Ciphertext
Authenticated encryption modes such as AES-GCM can provide
confidentiality and integrity together when used correctly.
Avoid designing your own encryption protocol.
For example, don't create a custom encryption scheme without
understanding the cryptographic requirements.
A custom cryptographic design can introduce vulnerabilities even when
the underlying algorithm is strong.
9. HTTPS Is Necessary --- But It Is Not Enough
Sensitive application traffic should use secure transport.
Instead of:
http://api.example.com
production applications should use:
https://api.example.com
But HTTPS does not automatically make the API secure.
Consider:
POST /api/users/123/profile
Even with HTTPS, the backend must still determine:
Is the user authenticated?
↓
Is the user authorized?
↓
Does the resource belong to the user?
↓
Is this operation allowed?
↓
Is the request valid?
So think of security as layers:
TLS
↓
Authentication
↓
Authorization
↓
Input Validation
↓
Business Rules
↓
Data Protection
10. Don't Disable TLS Verification to Fix Development Problems
One of the most dangerous shortcuts is disabling certificate or hostname
validation because a development server isn't configured correctly.
Avoid production configurations that effectively say:
Accept any certificate
Accept any hostname
Trust everything
This may make a development environment appear to work, but it removes
important security guarantees.
Instead:
Development Environment
↓
Proper Development Certificate
Production Environment
↓
Proper Production Certificate
If your development environment uses self-signed certificates, configure
development trust intentionally rather than disabling validation
globally.
11. WebView Is an Important Attack Surface
WebView is extremely useful for applications that need to display web
content.
But WebView also introduces additional security considerations.
Potential areas include:
- JavaScript
- JavaScript interfaces
- URL navigation
- File access
- Cookies
- Redirects
- Authentication
- Local storage
- External intents
If your application only needs to display trusted content, don't give
the WebView unnecessary capabilities.
For example, avoid blindly loading arbitrary URLs:
webView.loadUrl(userProvidedUrl)
Instead, validate the URL before loading it.
Conceptually:
Requested URL
|
▼
Is domain trusted?
|
┌────┴────┐
YES NO
| |
▼ ▼
WebView Reject
12. JavaScript Interfaces Need Extra Care
Android allows applications to expose native functionality to
JavaScript.
For example:
webView.addJavascriptInterface(
bridge,
"AndroidBridge"
)
This can be useful.
But it also creates a bridge between web content and native application
functionality.
If untrusted content can interact with that bridge, the security impact
can be significant.
Before exposing a JavaScript interface, ask:
Who controls the loaded page?
Can an attacker influence the URL?
What methods are exposed?
What data can those methods access?
Can those methods perform privileged operations?
Only expose the minimum functionality required.
13. Deep Links Are Untrusted Input
Deep links are convenient:
myapp://article/123
or:
https://example.com/article/123
But the values coming from a deep link should be treated as untrusted
input.
Validate:
- Scheme
- Host
- Path
- Parameters
- Authentication state
- Authorization
For example:
Deep Link
↓
Parse
↓
Validate
↓
Authenticate
↓
Authorize
↓
Navigate
Don't let a deep link directly perform a sensitive operation simply
because the application was launched using a trusted-looking URL.
14. Validate Server Responses Too
Security isn't only about validating input from users.
Applications should also carefully process server responses.
For example, don't assume that every response field will always contain
the expected value.
Instead of:
val amount = response.amount!!
consider appropriate validation and error handling.
External systems can fail.
Servers can be misconfigured.
APIs can change.
Attackers can manipulate requests.
Defensive programming is an important part of application security.
15. Review Exported Android Components
Android applications contain components such as:
- Activities
- Services
- Broadcast Receivers
- Content Providers
For each component, ask:
Does this component need to be externally accessible?
Who can invoke it?
What input does it receive?
What permissions are required?
Does it expose sensitive functionality?
If a component doesn't need to be externally accessible, don't expose it
unnecessarily.
Security configuration should be intentional.
16. Be Careful With Broadcast Receivers
Broadcast receivers can receive system or application broadcasts.
If sensitive functionality is exposed through a receiver, carefully
consider:
Who can send the broadcast?
What data does it contain?
Can another application trigger it?
Does it require permission?
A receiver that performs privileged actions should not blindly trust
external broadcast data.
17. Don't Leak Sensitive Data Through Logs
Logging is essential during development.
But this is dangerous:
Log.d("AUTH", "Access Token = $accessToken")
Log.d("USER", "OTP = $otp")
Log.d("CRYPTO", "Key = $key")
Avoid logging:
- Access tokens
- Refresh tokens
- Passwords
- OTP codes
- Session IDs
- Encryption keys
- Personal information
- Payment information
- Sensitive API responses
A useful rule is:
If an attacker could use the information in your logs, don't log
it.
Also review logs before production release.
18. Be Careful With Screenshots and Sensitive UI
Some applications display sensitive information:
- Banking information
- Payment details
- OTP
- Private messages
- Documents
- Medical information
For particularly sensitive screens, consider whether screenshots or
screen capture should be allowed.
Android provides mechanisms such as:
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
But this should be applied based on actual requirements rather than
blindly across the entire application.
Security controls should match the sensitivity of the data.
19. Third-Party SDKs Are Part of Your Attack Surface
Your application isn't composed only of your own code.
A modern application might include:
Application Code
+
Analytics SDK
+
Crash Reporting
+
Payment SDK
+
Advertising SDK
+
Networking Libraries
+
Image Libraries
+
Authentication SDK
+
Build Plugins
Every dependency introduces additional code and permissions.
Regularly review:
- Dependency versions
- Transitive dependencies
- Known vulnerabilities
- SDK permissions
- Network behavior
- Data collection
- Build plugins
- Unused dependencies
A useful mindset is:
If it ships inside my application, it is part of my attack
surface.
20. Secure the CI/CD Pipeline
Security doesn't end when application development is complete.
Consider the complete pipeline:
Developer
↓
Git Repository
↓
Pull Request
↓
CI/CD
↓
Dependencies
↓
Build
↓
Signing
↓
Release Artifact
↓
Distribution
Every stage needs appropriate protection.
Important areas include:
- Protected branches
- Secret management
- Dependency scanning
- Secret scanning
- Code review
- CI/CD permissions
- Signing key protection
- Release access
- Artifact integrity
For example, your signing credentials should never be committed to Git.
Use secure secret management in your CI/CD environment.
21. Protect the Android Signing Key
The signing key is extremely important.
If an attacker gains access to your production signing credentials, the
consequences can be serious.
Therefore:
Signing Key
↓
Secure Storage
↓
Restricted Access
↓
Controlled CI/CD Usage
Don't distribute the production keystore unnecessarily across developer
machines.
Keep access limited to the people and systems that actually need it.
22. Assume the APK Can Be Reverse-Engineered
An APK is distributed to users.
Therefore:
Assume that someone can inspect your application.
A possible attack path is:
APK
↓
Decompilation
↓
Resource Analysis
↓
Code Analysis
↓
Runtime Analysis
↓
Application Behavior
R8 can reduce and obfuscate application code:
minifyEnabled = true
But obfuscation is not a replacement for security.
Don't depend on:
Obfuscation = Security
Instead:
Secure Architecture
+
Backend Authorization
+
Secure Storage
+
Cryptography
+
Obfuscation
+
Runtime Protections
Each layer addresses a different problem.
23. Client-Side Security Checks Can Be Bypassed
Consider:
if (isPremiumUser) {
showPremiumFeature()
}
This controls the UI.
It does not necessarily protect the feature.
An attacker could potentially modify application behavior or directly
call the backend.
A secure architecture looks like:
Android Client
|
| Request
▼
Backend
|
| Verify entitlement
▼
Allow / Reject
The client can improve user experience.
The backend should enforce business-critical security rules.
24. Don't Trust Client-Side Values
Never blindly trust values such as:
-
isAdmin -
isPremium -
isVerified -
isAuthenticated -
userId -
role -
price -
discount -
permission
For example, this is dangerous:
{
"userId": "123",
"isAdmin": true
}
if the server simply trusts whatever the client sends.
Instead, security-sensitive properties should be determined and
validated by trusted backend systems.
25. Input Validation Matters
Anything coming from outside your trusted application boundary should be
considered untrusted.
Examples include:
- User input
- Deep links
- Intent extras
- Network responses
- WebView URLs
- Push notification data
- Files
- QR codes
- Clipboard data
Validate:
- Type
- Length
- Format
- Range
- Allowed characters
- Business rules
- Authorization
For example, if an API expects:
page = positive integer
don't blindly accept unexpected or excessively large values.
Validation helps reduce both security issues and unexpected application
behavior.
26. Handle Sensitive Errors Carefully
Avoid exposing internal implementation details to users.
Instead of:
SQLException: connection failed at database server 10.0.0.4
show:
Something went wrong. Please try again.
Detailed information can be useful internally, but production users
should generally receive appropriate, non-sensitive error messages.
27. App Integrity Should Be Part of the Threat Model
For applications with higher security requirements, app-integrity
mechanisms can provide additional signals about the application and
environment.
Conceptually:
Android Application
|
| Integrity Evidence
▼
Backend
|
+-- Validate
|
+-- Evaluate Risk
|
+-- Enforce Policy
The important principle is:
Don't rely solely on a client-side boolean to determine whether the
application is trustworthy.
For sensitive operations, the backend should be able to enforce the
security decision.
28. Security Testing Should Be Continuous
Security testing shouldn't happen only before production release.
A useful development lifecycle is:
Design
↓
Threat Modeling
↓
Implementation
↓
Code Review
↓
Automated Security Checks
↓
Testing
↓
Release
↓
Monitoring
↓
Incident Response
Consider testing:
- Authentication
- Authorization
- Local storage
- Network communication
- WebView
- Deep links
- Exported components
- Cryptography
- Dependencies
- Release configuration
The earlier a security problem is discovered, the easier it generally is
to fix.
29. Threat Modeling Before Writing Code
One of the most valuable security practices is threat modeling.
Before implementing a feature, ask:
What am I protecting?
Who might attack it?
What can the attacker control?
What happens if the attacker succeeds?
What security control reduces that risk?
For example:
Asset:
Authentication Token
Threat:
Token Theft
Attack Surface:
Local Storage
Logs
Network
Memory
Backup
Impact:
Account Takeover
Controls:
Secure Storage
TLS
Token Expiration
Token Rotation
Backend Validation
Logging Restrictions
This is much more useful than simply adding a security library because
someone recommended it.
A Practical Android Security Checklist
Before releasing an Android application, ask:
[ ] Are production secrets outside the APK?
[ ] Is authentication implemented securely?
[ ] Is authorization enforced by the backend?
[ ] Are access and refresh tokens protected?
[ ] Is sensitive local data protected?
[ ] Are cryptographic keys managed securely?
[ ] Is HTTPS configured correctly?
[ ] Is TLS verification enabled?
[ ] Are WebViews restricted?
[ ] Are JavaScript interfaces necessary and controlled?
[ ] Are deep links validated?
[ ] Are exported components intentional?
[ ] Are sensitive values excluded from logs?
[ ] Are screenshots restricted where necessary?
[ ] Have third-party dependencies been reviewed?
[ ] Are CI/CD secrets protected?
[ ] Are signing keys securely managed?
[ ] Has reverse engineering been considered?
[ ] Are client-side security checks backed by server-side enforcement?
[ ] Is user input validated?
[ ] Is the backend independently secure?
[ ] Are security tests part of the development lifecycle?
[ ] Has the application been threat-modeled?
The Bigger Picture
Android security isn't a single library.
It isn't simply:
AES
+
HTTPS
+
R8
A secure Android application requires multiple layers:
Security
│
┌───────────────┼───────────────┐
│ │ │
Application Platform Backend
Security Security Security
│ │ │
├─ Auth ├─ Keystore ├─ Authorization
├─ WebView ├─ Permissions ├─ API Security
├─ Deep Links ├─ Components ├─ Rate Limiting
├─ Storage ├─ IPC ├─ Validation
└─ Logging └─ Integrity └─ Monitoring
The most important mindset shift is this:
Don't ask only, "How do I make my Android app secure?"
Ask:
"What happens if someone controls the device running my
application?"
That question changes the way you design authentication, APIs, storage,
cryptography, WebViews, deep links, and even your CI/CD pipeline.
Security should not be the final layer added to an Android application.
Security should be one of the foundations on which the application is
built.
Final Thoughts
As Android developers, we spend a lot of time thinking about:
UI
Performance
Architecture
Networking
Database
Testing
Security deserves the same level of attention.
A production application should be designed with the assumption that:
- The client can be inspected.
- User input can be manipulated.
- Network requests can be replayed.
- APK logic can be analyzed.
- Dependencies can contain vulnerabilities.
- Local data can be exposed on compromised devices.
- Client-side checks can be bypassed.
The goal isn't to make an application impossible to attack.
The goal is to make the architecture resistant, observable, and
recoverable when attacks happen.
For developers who want to go deeper into mobile security, a good
learning path is:
Android Security Fundamentals
↓
Cryptography Basics
↓
Authentication & Authorization
↓
Secure Storage
↓
Network Security
↓
WebView & Deep Link Security
↓
Reverse Engineering
↓
Threat Modeling
↓
OWASP MASVS
↓
OWASP MASTG
↓
Mobile Security Testing
The best security engineers don't just know how to use security APIs.
They understand why the security control exists, what threat it
addresses, and what happens when that control fails.
Secure coding is a development skill.\
Security architecture is an engineering skill.
Android #Security #Kotlin #Cybersecurity
About the Author
Padmakar Garg is an Android Developer focused on Kotlin, Jetpack Compose, scalable application architecture, mobile security, and modern Android development. He writes about practical Android engineering, security, architecture, and real-world development challenges.
Connect with me on DEV to follow more articles about Android development, Kotlin, Jetpack Compose, Android security, and mobile architecture.
Top comments (0)