Modern Android Security Development
An Opinionated Ultimate Security Toolbox with Insights
- Featured in Kotlin Weekly - Issue #215:
- Featured in Android Newsletter - Issue #11:
- Featured in onCreate Digest - Issue #21:
1st (Mistake) Practice
Given the “find out how to en-decrypt data in Android” requirement, what would you do?
Unless you are a security expert or developer with a security career dedication that writing the cryptographic code from scratch is just a trivial task, it’s very common for us to start “Googling”, deep dive in “Stack Overflow” to find security-related implementation answers until we probably find below similar snippet:
Problem:
So what‘s wrong with the snippet? The encrypt
and decrypt
APIs look solid and have our work done, don’t they?
Well, there’s absolutely nothing wrong with that argument. The interchangeability between the plain and the cipher is proven goes smoothly. But the such old, common, and unsafe to use of the snippet is the most concerning issue we’re really facing now. That issue factors could be the reason that our thought-have-secured assets might be available or easily accessible to an attacker.
2nd (Mistake) Practice
Let’s have a short scenario:
Backend engineer: “Keep this JWT token, will you? We need it for some later authorizations.”
Probably us: “No problem (better say hi to SharedPreferences now).”
Problem:
By having a rooted phone/device, please note that everyone literally could access our app XML preference file, located inside the internal storage. Talking about JWT token, the token is literally composed of algorithm-token header type, payload, and verify signature parts, each concatenated by .
. And based on this recommended writing,
You might want to read off what are some potential vulnerabilities, how the attackers can forge, manipulate the token parts, and log in as someone else. The only moral story here we can learn to help to prevent such an issue is to not store the token in raw.
FAQ:
“But wait a minute… how are we supposed to act now? We understand if we could encrypt the token before storing it in the preference file or somewhere else should satisfy the issue prevention. But the API we use for the encryption scheme is issued on its own as well at the 1st problem.”
Well, if we could have our own set of security arsenal that we can rely on, I personally can state, just go ahead. However, if we have less knowledge of any robust cryptographic algorithm or looking for an official and standardized solution, we may want to refer to the next section.
Jetpack Security
Or “JetSec” for short, introduced at last Android Dev Summit 2019 provides us a high-level abstraction to allow encrypting data, file, until shared preferences easily without having to really understand the ins and outs of security.
JetSec features Android KeyStore¹ which is the mastermind of every cryptographic operation and we may assume all data secured is done via it. Of course, every secured data associates with a private key which is a primary material used for any cryptographic op. In JetSec, these private keys called keyset. Android KeyStore stores these keyset materials in a container hardware-backed which makes accessing them very hard and it’s not exportable.
Note: JetSec also employs Tink, a cross-platform, open-source library from Google to leverage the
MasterKey
concept for securing all keysets.
Gradle Import
Now let’s jump into practical data encryption with JetSec.
Note: The library requires API 23/Marshmallow at minimum.
I personally wouldn’t mind raising the
minSdkVersion
bar as a trade-off since some of my projects hold some critical sensitive information.
MasterKey
Before leveraging any toolbox from JetSec, the generation of the MasterKey
is a prerequisite. Since the minSdkVersion
has been set to 23, we’re allowed to use the default AES-based spec for the key generation:
Though it’s fine for most use cases, we’re also allowed to customize our own spec to secure some of the sensitive data clusters in advance, in case.
EncryptedFile & EncryptedSharedPreferences
I wouldn’t write too far all about these tools, while we can find their functionalities and implementation details here². Here are some results of the encrypted content along the generated keyset stored inside files:
Note: It’s quite interesting when we find that we literally don’t need to provide our own custom private key string either in a master key generation or when taking both
EncryptedFile
andEncryptedSharedPreferences
in some actions. Hence, we won’t be concerned with another issue about how to secure the private key per se, declared inside the source code. Thanks to the Android KeyStore.
In addition to JetSec, below are other security tools compliment which I equally highly recommend to strengthen the security factors:
SealedObject
At some point, we may want to have our serialized objects resided in some untrusted mediums³. JavaX’s SealedObject
encapsulates the original object, in serialized format, and encrypts the content to protect its confidentiality.
SealedObject
will cover us in order to prevent someone from tampering with our serialized object, but the reconstituted object may be lacking transient fields or other information.
Let’s break-down:
- The
sealObject
API is for encapsulating the original object as theSealedObject
contract states. We then finally store the encapsulatedSerializable
result in a somewhere medium. - We decide to retrieve back the serialized object. To verify if the serialization is modified, we testify it via
unsealObject
API. The API should unwrap back to the original one if the object is proven the same still (of course with the corresponding secret key), else an exception will be thrown.
Note: We may consider
SignedObject
andGuardedObject
as part of the compliments.
R8
“One Shot Two Kills”
It isn’t only reducing the APK/AAB size significantly, R8 contributes to a matter of security context as well. Today, cracking the source code along with embedded resources/assets can be done easily with some simple tools, like the dex2jar converter, Java Decompiler (JD) & friends, even the Android Studio (3.6+) facilitates the APK/AAB analyzer.
To reduce risk at a minimum of attackers from stealing, recompiling, until publishing back to the store again but with extra profit elements, such as ads, R8’s obfuscation feature does its best to transform any reverse engineering activity much more difficult starting at a point. We can enable R8 via app-level build.gradle
file:
Now an example, let’s compare the decompiled source code between the one which doesn’t benefit the R8 and the one with R8 enabled:
Though it’s harder to read now, still, the essences of the source code, like the security schemes that we’re using, AES256_GCM_SPEC
or AES256_GCM_HKDF_4KB
can still be clearly spotted at the respective 31st and 37th lines which is not good enough. Now let’s look over the one with R8 enabled:
No, I’m not pasting the wrong gist. What we’re seeing is valid and I do not say the code is really getting deleted by the R8 but more like it’s obfuscated and inlined to somewhere entry point classes where the invocation of the instance happens, e.g., MainActivity
.
This new decompiled project structure should make any tracing-based process from a given APK/AAB becomes one step harder. Strictly speaking,
Note: dex2jar and JD-GUI tools are used to generate above decompiled source code.
Use Case
Last but not least, this writing also brings some daily use cases (based on experience) to the surface that we might want to put great attention on securing them.
Google Cloud API Keys
If we integrate some google cloud services, then we probably have this secret API key, and no matter what, we will want to protect the key in a VERY secure way, else the billing will just blow up.
There are numerous ways we can store the key, like storing it in a C/C++ file using NDK⁴ in the first place or hosting own service provider to keep it. The rest is optional if we welcome JetSec’s EncryptedSharedPreferences
for an easier accessing benefit with some security guarantees. We could also add an extra security layer by taking advantage of the GCP’s key restrictions feature.
Lock which platforms can make the requests, fill up the forms, and we can sit relaxed for a moment in case the key leaks.
Google Cloud Service Accounts
This credential is literally an alternative to the previous GCP API keys. As far I can state, certain GCP services, like Translation API, require this credential parameter for some advanced auth ops.
Same rule as previous, we want to store the JSON first to some trusted mediums where we favor one and having the JetSec’s EncryptedFile
which is a better candidate for securing this credential type in advance.
Firebase’s google-services.json
FAQ:
“Hold on a sec… Are you saying we could secure the JSON as well? We just follow every instruction as stated (below) and let the system do the rest for the Firebase initialization.“
Well, before I’m about to answer “Yes” o̶r̶ ̶”̶N̶o̶”̶, let me show you what could go wrong afterward by the above integration steps:
So when someone has the APK and decompiles back, we can see the resources.arsc
file exposes every embedded resource including our firebase credential information. Judging by this issue, well, in all respects, I personally wouldn’t recommend by following the “official” documented for the firebase integration.
Instead, we could do some hacks by extracting the JSON fields, variablizing them, then we can initialize the Firebase manually. Here’s how:
- Don’t remove
com.google.gms.google-services
plugin that has been set by us in app-levelbuild.gradle
. We need it to extract the fields. - Run
./gradlew :app:assembleDebug
(at least in MacOs) in terminal or viaGradle
tab >[project_name]
>app
>Tasks
>other
>assembleDebug
to extract the fields. - Now we can remove the plugin along the
classpath "com.google.gms:google-services:$whatever_version"
set in project-levelbuild.gradle
. - The extracted result is mapped to an XML file as shown below:
- Copy them and we can initialize the firebase manually.
Note: By owning these constants, now we can store somewhere and secure them with any fitted security tool we have mastered.
Closing
Thanks for reaching the potato. Few words left by this great said:
“Security is always excessive until it’s not enough.”
- Robbie Sinclair
In the security world, I believe, there’s no such single tool, silver bullet solution that automatically puts us in the green zone. Great security can only be achieved with a broad collection of complementary tools, every considered security element is taken into account, that forms a layered defense. A layered defense is the only viable defense.
That’s all about. Hope this helps, thanks.
External links:
- https://developer.android.com/training/articles/keystore
- https://developer.android.com/topic/security/data
- https://www.infoworld.com/article/2076237/signed-and-sealed-objects-deliver-secure-serialized-content.html
- https://medium.com/programming-lite/securing-api-keys-in-android-app-using-ndk-native-development-kit-7aaa6c0176be