API keys are crucial for integrating services into our app, enabling us to access information about our app or utilise their paid/free services. During this process, we need to provide these keys through various methods, which will be discussed in detail. This allows us to access and use their services effectively.
But, there is a problem!
Once our app is deployed, there is a risk that these keys may be exposed, especially during reverse-engineering of the APK, leading to potential security vulnerabilities.
Tool for reverse-engineering to converting the Dex to java https://github.com/skylot/jadx
also Online decompilers available http://www.javadecompilers.com/apk
Its very important to restrict your keys so that it can only be used within the context of our app.
Frequent places where keys were stored in app:
1. build.gradle: We store keys in the build.gradle
file to configure different keys based on build type and product flavor. This includes third-party SDK keys, client IDs, and other relevant data, which can be accessed in the code using BuildConfig.{key name}
. This approach allows us to provide the necessary data at runtime to utilise the service.
When decompiling the APK file, these keys can be found in the BuildConfig file. This means all the information we intended to keep secret is exposed, allowing anyone with access to misuse this information and potentially exploit.
2. AndroidManifest.xml: We store keys like com.google.geo.API_KEY
in the AndroidManifest.xml file as metadata. But, they can be easily found in the manifest file after decompiling the APK. An alternative approach is to declare these keys as manifest placeholders in the build.gradle
file, and this method also presents similar risks.
The Google Maps Platform offers the secrets-gradle-plugin
to prevent keys from being exposed in open-source projects and version control. By adding them to the local.properties file, which should be excluded via .gitignore, the keys are kept secure. Ultimately, these keys will be added to the BuildConfig file in the APK.
3. gradle.properties: Storing keys in the gradle.properties
file is more secure, as they are not included in the app directly and cannot be accessed within the app itself. However, to use these reference in gradle file. which brings us back to the initial problem of potential exposure.
4. fabric.properties: If you are still using fabric.properties
, it means your project hasn’t been updated or you forgot to remove them. These were migrated by Firebase a long time ago and are no longer supported.
5. google-services.json: The goole-services.json
file is generated for Firebase services like Crashlytics, in-app messaging, A/B testing, remote config, and many others, in combination with the app’s product flavour and build types. All required fields are converted and stored in the strings.xml file, which were exposed under common names and can be exploited to create fraudulent crashes, analytics, or fake reports, significantly affecting the crash-free ratio.
An alternative approach is to dynamically initial the FirebaseApp and remove the google-services.json
file in the Application class using FirebaseApp.initializeApp
with the FirebaseOptions
builder.
Sounds promising!
the drawback is that Firebase Analytics ignores these runtime values and looks for values in the strings.xml provided by google-services.json, leading to a failure in recording the information.
StackOverflowDiscussion of why this it is not supported, I don’t see a workaround for this if you require Firebase Analytics.
Solution
A widely recommended solution is to store all such values in native code (C/C++) and access them using the Java Native Interface (JNI). This can be achieved by configuring CMake
, which generates a .so
file.
Here comes the big twist…! even this is not secure enough.
There’s no need to decompile the APK file to extract the keys. Android Studio offers an “Analyze APK” option, which allows us to open the .so file and read the constant values. With some patience, it is possible to fully decipher the information.
No matter where you store your keys, there’s always a risk of exposure.
We can make this harder, by encrypt the data so that even if it is exposed, it cannot be directly used. Deciphering the encrypted data takes time, during which we can update the encryption keys and, if possible, the access keys as well.
One straightforward method is to use the app’s signing key. This approach addresses an often unmentioned issue: if someone modifies or tampers the APK, the signing key will differ, causing the app to crash at runtime.
Shreyas Patil have created a bytemask plugin to simplify this process for you which also supports your custom key and algorithm.
If you know any other secure methods, please feel free to suggest them in the comments. Arigato!!!