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 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
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-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:
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:
- 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