Application security is essential in any mobile app development strategy, especially on Android, where protecting user data and app integrity is paramount. This guide explores practical security measures, like app signing, certificate checksum verification, authorized install checks, code obfuscation, and secure distribution. We’ll walk through each step with hands-on examples in Kotlin, making complex security practices straightforward and actionable. By following along, you’ll learn how to apply these methods to enhance the security of your Android app effectively.
Why Application Security Matters
Application security is crucial for protecting user data, maintaining app integrity, and building trust with users. As the risk of app tampering, unauthorized installs, and reverse engineering continues to rise, developers must embrace best practices to safeguard their apps from the inside out.
By implementing these security techniques, we can reduce vulnerabilities, block unauthorized access, and ensure that user data remains safe and secure. It’s all about keeping your app resilient, trustworthy, and user-friendly in a world where security threats are ever-evolving.
Let’s go through them one by one and secure the Android app.
App Signing
App signing is the process of associating your app with a cryptographic key. This step is mandatory for Android apps, as it ensures the app’s authenticity and integrity. Signed apps guarantee to the operating system that the code comes from a verified source. App signing is a crucial security measure that allows users and devices to verify the app’s origin and integrity. Before publishing, you must sign your app with a private key, which acts as a unique identifier for the developer.
Imagine sending a sealed package to someone. When you put your personal signature on the seal, it acts as proof that the package is from you and hasn’t been tampered with. If the seal is broken or the signature is missing, the recipient would know something went wrong during delivery.
App signing works in a similar way in the digital world. When you develop an app, you “seal” it by signing it with a private key. This private key is unique to you as a developer, much like your personal signature. Once the app is signed, it receives a special “certificate” that helps devices and app stores confirm two things:
- Integrity: Has the app been tampered with? The certificate lets app stores and devices check if the app is exactly as you released it. If anyone tries to alter the code (like a hacker inserting malicious content), the certificate won’t match anymore, signaling tampering.
- Authenticity: Did the app really come from you? Your private key is unique to you, so the certificate proves that the app is genuinely yours. Without the correct signature, no one else can publish an update that would be accepted as an official version of your app.
Take a banking app, for example. When a bank releases its app, they sign it with their private key so customers know it’s genuine. If a counterfeit app appeared in the app store, it wouldn’t have that signature, helping protect users from downloading a fake app and risking their personal data.
In short, app signing builds trust. It reassures users that the app they’re downloading hasn’t been tampered with and genuinely comes from the original developer — just like your friend knows your letter is legit when they see your signature on the seal.
Steps for App Signing in Android Studio
1. Generate a Signing Key:
- In Android Studio, go to Build > Generate Signed Bundle / APK…
- Follow the prompts to create a new keystore, choosing a password and providing details.
2. Sign Your App:
- After creating the keystore, Android Studio will prompt you to select it for signing the app.
- Select your key alias and password, and proceed with the build.
Code Snippet: Configuring Signing in build.gradle
In app/build.gradle
, add the following code under the android
section to configure the signing process:
android {
signingConfigs {
release {
keyAlias 'your-key-alias'
keyPassword 'your-key-password'
storeFile file('path/to/keystore.jks')
storePassword 'your-keystore-password'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
In Kotlin script (build.gradle.kts
), the syntax is slightly different from the Groovy syntax used in build.gradle
. Here’s how you can define the signing configuration in build.gradle.kts
:
android {
signingConfigs {
create("release") {
keyAlias = "your-key-alias"
keyPassword = "your-key-password"
storeFile = file("path/to/keystore.jks")
storePassword = "your-keystore-password"
}
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
}
Build and Sign: Once configured, you can build a signed APK or App Bundle for distribution.
Note: Android apps are signed with custom CA certificates. Google offers the Play App Signing service, which is now mandatory for new apps and updates on the Google Play Store. This service allows you to securely manage and store your app signing key using Google’s infrastructure.
So, app signing guarantees that users receive authentic, untampered versions of your app.
App Certificate Checksum Verification
To add an extra layer of security, we can verify the app’s certificate checksum. This ensures the app hasn’t been tampered with since it was signed. Think of the checksum as a digital fingerprint — it confirms the app’s integrity and ensures it’s the original, untampered version.
By using the app signing certificate’s checksum, we can detect any tampering with the app’s code. If an attacker tries to alter the application, the original checksum will no longer match, serving as a red flag that something has been compromised. This verification helps us catch tampering early and prevent malicious code from executing, keeping both the app and its users secure.
To check your app’s signature in Android, you can retrieve and verify the certificate checksum using the following method.
import android.content.pm.PackageManager
import android.util.Base64
import java.security.MessageDigest
fun getCertificateChecksum(): String? {
try {
val packageInfo = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNING_CERTIFICATES
)
val signatures = packageInfo.signingInfo.apkContentsSigners
val cert = signatures[0].toByteArray() // Getting the certificate's byte array
val md = MessageDigest.getInstance("SHA-256") // Using SHA-256 for the checksum
val checksum = md.digest(cert) // Generating the checksum
return Base64.encodeToString(checksum, Base64.NO_WRAP) // Encoding the checksum in Base64
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
To verify the certificate, simply compare the checksum with the expected value. This helps protect against tampering, as any change in the code will result in a different checksum.
Authorized Install Verification
To ensure your app is installed from a trusted source, like the Google Play Store, Android allows developers to verify the app’s integrity and security. You can use Google’s Play Integrity API (which we will cover in more detail in another blog; here we focus on the basics) to check if the app is running in a legitimate environment and hasn’t been tampered with, helping to prevent unauthorized installs.
import android.content.pm.PackageManager
fun isInstalledFromPlayStore(): Boolean {
val installer = context.packageManager.getInstallerPackageName(context.packageName)
return installer == "com.android.vending" // Checks if installed from Google Play Store
}
This method checks whether the app was installed from the Google Play Store. If isInstalledFromPlayStore()
returns false, it could mean the app was installed from an unofficial or unauthorized source.
Wait a minute… What would a simple client-server design look like for verifying authorized installations?
As our app is distributed exclusively through the App Store and Play Store, we verify the installation source on each app launch to detect counterfeit or sideloaded versions. If an unauthorized installation source is detected, a predetermined information packet is sent to the server instead of a simple flag. This allows the server to assess the authenticity of the installation source and take preventive actions, if necessary (such as terminating the app instance).
The following algorithm is used to derive strategic information (i.e., whether the installation is authorized or not) at both the client and server ends:
- If the app is installed from an unauthorized source, we send the server a SHA-256 hash generated from a unique device identifier that is securely shared between the client and server.
- If the app is installed from an authorized source, we send a 32-byte random number generated using Java’s SecureRandom, ensuring high security.
This approach enables the server to accurately distinguish between authorized and unauthorized installation sources, helping to prevent unauthorized app usage.
Code Obfuscation
Code Obfuscation is the practice of making source code difficult for humans (and automated tools) to understand by transforming it into a non-syntactical and non-natural language format. It is deliberately done to protect intellectual property and to prevent attackers or malicious entities from reverse-engineering proprietary software logic.
Increasing internal complexity through obfuscation makes it harder for attackers to understand how the app operates, thus reducing potential attack vectors.
Obfuscation is generally achieved by applying some of the following techniques:
- Renaming classes, methods, and variables to meaningless or random labels to hide the original intent of the code.
- Encrypting sensitive pieces of the code, such as strings or critical functions, to prevent them from being easily understood.
- Removing revealing metadata such as debug information and stack traces that could help reverse engineers understand the code’s structure.
Advantages:
- Code Bloat: Adding unused or meaningless code to the application increases complexity and can confuse reverse engineers.
- Prevents Reverse Engineering: Obfuscation makes it more difficult to reverse-engineer the source code, providing an added layer of protection.
- Protects Sensitive Information: By obscuring payment algorithms and other sensitive logic, obfuscation helps prevent fraud.
- IP Protection: Obfuscation safeguards proprietary code from theft, reducing the risk of cloning and unauthorized use.
- Secure Communication: It helps protect critical communication credentials (e.g., API keys, server communication details) by making them harder to extract.
How does it work?
Advanced code obfuscation in modern software development is typically achieved using automated tools called obfuscators. These tools apply various obfuscation techniques to the code, making it more difficult to analyze or reverse-engineer. When it comes to optimizing and securing Android apps, three primary tools stand out: R8, ProGuard, and DexGuard.
- R8: A code shrinker and obfuscator that comes bundled with Android Studio. It replaces ProGuard in Android projects starting from Android Gradle Plugin version 3.4 and beyond. R8 performs code shrinking, optimization, and obfuscation, making it more efficient than ProGuard in many cases.
- ProGuard: Originally designed as an optimization tool, ProGuard also provides obfuscation features. While it remains widely used, it’s primarily known for reducing the size of the app and optimizing bytecode, with obfuscation being an optional feature.
- DexGuard: A more advanced, proprietary obfuscator specifically designed for Android applications. DexGuard offers stronger obfuscation techniques and more comprehensive protection than ProGuard or R8, making it suitable for apps that require higher levels of security.
Setting Up ProGuard/R8
To enable code obfuscation in your Android app, you’ll need to configure ProGuard/R8 in your build.gradle
file.
1. Enable Minification and Obfuscation:
In your android
block, ensure that the minification and obfuscation are enabled for the release build type:
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}
2. Add Custom Rules (Optional):
You can customize the behavior of ProGuard/R8 by adding rules to the proguard-rules.pro
file. For example:
// It's in the ProGuard file, not in the Kotlin file. Due to the limitation of selecting a ProGuard file, I added it here.
# Keep specific classes
-keep class com.yourpackage.** { *; }
# Remove logging statements
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
3. Obfuscate and Test:
After configuring the build.gradle and rules file, build the release version of your app. This will obfuscate the code, making it more difficult for attackers to reverse engineer. Make sure to test the release version to ensure the obfuscation works correctly and that your app functions as expected.
Obfuscation protects sensitive parts of your code and can significantly reduce the likelihood of reverse engineering, adding an important layer of security for proprietary software.
Secure App Distribution
Our app should only be downloaded from official marketplaces — the Play Store for Android and the App Store for iOS. For security reasons, we don’t offer it through other channels like private marketplaces, direct links, emails, or corporate portals. Using a trusted distribution channel helps protect your app from being tampered with or repackaged. Google Play, for example, offers features like Play Protect, automatic updates, and full control over distribution, making it one of the most secure options.
Tips for Secure Distribution
- Use the Google Play Console: It offers extra security with app signing and Play Protect.
- Enable Play App Signing: When you upload your app, go to App Integrity and select Manage your app signing key. Google will manage your app’s signing key, making it more secure and reducing the risk of key compromise.
- Use App Bundles: App Bundles not only help reduce APK size but also provide extra protection through Google’s secure servers.
- Avoid Third-Party App Stores: Stick to trusted platforms to keep your app safe.
Other Secure Distribution Options
- In-House Distribution: For private app distribution, use secure enterprise app stores.
- Encrypted File Transfer: If you’re sharing the APK manually, consider encrypting it before sending.
By distributing your app through Google Play, you’re making sure users get a secure, legitimate version of your app.
Conclusion
Securing an Android app is a process that requires attention to detail at every stage, from app signing and checksum verification to ensuring secure distribution. By following the practices outlined in this guide—like app signing, certificate checksum verification, authorized install checks, code obfuscation, and secure distribution—you can significantly improve your app’s defense against common security threats.
By applying these techniques, you’ll not only meet industry standards but also build trust with your users by protecting their data and providing a safe experience. Just remember, app security isn’t a one-time thing—it’s an ongoing effort. Staying up to date with the latest security practices is key to long-term success.