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).”

Stored in [internal storage]/data/data/[package_name]/[preference_file_name]

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:

EncryptedFile - “Hello World” in ciphertext
EncryptedFile - Generated keyset
EncryptedSharedPreferences - JWT “user_token” in ciphertext
EncryptedSharedPreferences - Generated keysets

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 and EncryptedSharedPreferences 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 the SealedObject contract states. We then finally store the encapsulated Serializable 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 and GuardedObject 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:

Original source code (Kotlin)
Decompiled source code with no R8

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:

Decompiled source code with R8 (actually there’s no FileEncryptor.class file)

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,

Boromir from LOTR

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

Fake API key

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.

Google Cloud Platform (GCP) key restrictions

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

Fake service account

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

Fake 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.“

Firebase integration steps

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:

Exposed firebase project information

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-level build.gradle. We need it to extract the fields.
  • Run ./gradlew :app:assembleDebug (at least in MacOs) in terminal or via Gradle 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-level build.gradle.
  • The extracted result is mapped to an XML file as shown below:
Located in: ../app/build/generated/res/google-services/debug/values/values.xml
  • 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:

  1. https://developer.android.com/training/articles/keystore
  2. https://developer.android.com/topic/security/data
  3. https://www.infoworld.com/article/2076237/signed-and-sealed-objects-deliver-secure-serialized-content.html
  4. https://medium.com/programming-lite/securing-api-keys-in-android-app-using-ndk-native-development-kit-7aaa6c0176be

--

--