This post was made prior to the OWASP Top 10 2024 update - please see my blog post for more details on the 2024 Top 10
👋 Hi and welcome to the second post in this series where we deep-dive into Android Security. This series focuses on the Top 10 Mobile security threats as determined by The Open Web Application Security Project (OWASP) Foundation, the leading application security community in our field.
Before checking this post, please consider checking out the previous one ‘Improper Platform Usage’ which is available on my site, and on ProAndroidDev.
⚠️ Please note that this series is for educational purposes only. Remember to only test on apps where you have permission to do so and most of all, don’t be evil.
Finally, if you enjoy this series or have any feedback, please drop me a message. Thanks!
Introduction
In this second helping of my series on Android Security, we shall take a look into the #2 threat to mobile application security as determined by OWASP, “Insecure Data Storage”.
When it comes to data storage and Android, there are a number of solutions that may immediately jump to mind.
In this blog, we will look at the three most common approaches you’ll already likely be using in your apps:
SharedPreferences
- Room databases
- Jetpack’s
DataStore
As app developers, the need for storing data is an extremely common scenario that we face. However, it is also important to understand the security concerns that this introduces. As we shall see, it is often trivial for malicious actors to access stored data on the device and compromise it.
It is also worth noting before we jump in, that it is a best practice to avoid storing any form of sensitive data on a device. Sensitive data may include a user’s personally identifiable information (PII), your API keys or any other type of data that may be ‘dangerous’ if it fell into the wrong hands. I would always recommend that, whenever possible, sensitive data should be stored remotely and only accessed by your app when it is required.
Let’s kick things off by looking at our ever-faithful companion SharedPreferences
Shared Preferences
The SharedPreferences
API has been a staple of Android Development since the very beginning, making its debut on the platform in API 1. It’s a tried and tested quick solution for developers to store data in a key-value pair (KVP), however, it is not without its flaws.
Let’s look at a very simple example:
Behind the scenes, as a specific preference file name was given1, an XML file with this name is created (if required) within the device’s /data/data/{package name}/shared_prefs
folder.
Next, the preferences to be stored are aggregated and then accessed via an internal reference to a Map<String, Any?>
2 which then has its contents written to the XML file through the use of a TypedXmlSerializer
, thus saving the KVPs for read/write requests in future.
Through the use of adb
let’s look and see what this output does on a device with a debuggable build:
As you can see, the “mySecretKey” preference is stored in plaintext, free for all to view. However, attempting this on a no-debuggable release build will result in an error package not debuggable
. So, that’s secure right? Wrong.
⚠️ Android Backup Exploitation
It is still possible to access the shared preferences of a production app on a device through the ‘misuse’ of the adb backup
command. This command’s intended purpose is to allow for an archive of a device or specific app to be created and then later restored to a device through the use of adb restore
, However, the backup archive is itself a tar
file that can be extracted to read the contents of the private app data, including any shared preference XML files.
The process of completing this is fairly straightforward and through the use of an external tool, known as the Android Backup Extractor, this process can yield the hidden contents of an app extremely easily.
Voila! We have read the contents of a non-debuggable app’s preferences.
It should be noted that this method is only available to exploit apps that do not explicitly set android:allowBackup="false"
within their manifest file. If your app does not need to handle any form of backup during a device upgrade, it is highly recommended you set this option within your manifest to avoid this potential hazard.
Additionally, Google has recently acknowledged this vulnerability and, since Android 12, has implemented the following restrictions:
To help protect private app data, Android 12 changes the default behavior of the
adb backup
command. For apps that target Android 12 (API level 31) or higher, when a user runs theadb backup
command, app data is excluded from any other system data that is exported from the device.
This is very good news. Simply through the targeting of an SDK, your non-debuggable apps should be well protected from just about anyone snooping on your preferences. However, can we take this even further?
Encrypting Shared Preferences
I am hopeful that by now you can see the dangers of storing PII or other sensitive data through SharedPreferences
. However, should your app require a backup strategy, not target API level 31+ or you are (rightly) concerned about your mobile app’s security, you may wish to encrypt any data you save within your shared preferences to ensure data cannot be easily read.
This is not something that SharedPreferences
does out the box, but thankfully Jetpack Security (known as JetSec) can provide this functionality through it’s androidx.security:security-crypto
library, giving developers access to an EncryptedSharedPreferences
class that wraps the existing SharedPreferences
API.
Note: While the stable 1.0.0 release of androidx.security:security-crypto
supports API 23+, a back-port to API 21 is part of the upcoming 1.1.0 release and is currently available in early alpha versions3.
A basic example of using JetSec to secure shared prefs can be seen here
I highly recommend checking out the Google blog post that goes into further detail on how the library can be used and configured for your own app’s security needs. In particular, it is worth making note of the methods highlighted by the JetSec team that enable extra layers of security, including forcing cryptographic operations to be performed on a dedicated hardware security module or requiring user authentication before preferences can be accessed.
Important options:
userAuthenticationRequired()
anduserAuthenticationValiditySeconds()
can be used to create a time-bound key. Time-bound keys require authorization usingBiometricPrompt
for both encryption and decryption of symmetric keys.unlockedDeviceRequired()
sets a flag that helps ensure key access cannot happen if the device is not unlocked. This flag is available on Android Pie and higher.- Use
setIsStrongBoxBacked()
, to run crypto operations on a stronger separate chip. This has a slight performance impact, but is more secure. It’s available on some devices that run Android Pie or higher.
But how is this actually working behind the scenes? The cryptography used by JetSec is handled by Google’s tink library, an open-source project that provides industry-leading crypto algorithms and helps developers follow best practices when performing cryptography. As EncryptedSharedPreferences
is just an implementation of SharedPreferences
, it can utilise Tink internally and provide Tink’s functionality when handling preferences.
Let’s take a look at the XML output from using JetSec
✨ Much better!
Room Databases
Next let’s look at Room databases, a popular choice in the Android ecosystem for storing large amounts of structured data. Room is an abstraction layer over the database engine SQLite, allowing for developers to easily create tables, write queries and auto-magically handle the standard CRUD operations for databases.
Using the Room documentation’s basic setup example, let’s assume a production application created a very simplistic database named insecure-database
that contains user data, such as their user id, first and last name. Certainly, something you might not want to be sharing inadvertently.
As we have already discovered with shared preferences, the Android backup process can once again be used to extract private app data. Following the same process we used previously, an app’s Room SQLite databases can be found within the {package name}/db
.
Using sqlite3
on the command line, we can inspect the database, list the tables and show table data including the contents.
Should an app be storing any sensitive data in a database without proper encryption, it is (just like shared preferences) available to inspect in plaintext. Boo 👎
Encrypting Room
So, how can we fix this? Thankfully the well-known (and trusted) SQLCipher project provides an Android Library to support the transparent 256-bit AES encryption of database files.
This can be added as a dependency via net.zetetic:android-database-sqlcipher:{latest version}
. However, to use SQLCipher when creating your Room instance, supply the builder with SQLCipher’s implementation of SupportSQLiteOpenHelper.Factory
, aptly named SupportFactory
.
Behind the scenes, on initialisation SQLCipher uses the supplied ‘passphrase’ to generate a unique key for the AES encryption/decryption of the database file. This is explained in further detail within the SQLCipher docs and paraphrased below.
When initialized with a passphrase SQLCipher derives the key data using PBKDF2-HMAC-SHA512. Each database is initialized with a unique random salt in the first 16 bytes of the file. This salt is used for key derivation and it ensures that even if two databases are created using the same password, they will not have the same encryption key. The default configuration uses 256,000 iterations for key derivation.
To verify this is working as expected, we can install sqlcipher
4 and open the secure database. Using PRAGMA key
with the same passcode as provided in your code, it is possible to decrypt the database and read the contents in plaintext once again.
Voila. Once again, we find ourselves having stored our data securely with minimal development effort!
DataStore
Finally, let’s look at DataStore the relative newcomer of data storage within the Android ecosystem. Its Kotlin-first approach and utilisation of coroutines to provide asynchronous reading or writing of preferences make it an attractive option for modern Android development. However, once again, it is potentially vulnerable to being read via the adb backup
method 😬
Let’s take another simple example
Using the backup method, once extracted we find our data within datastore/insecure-data-store.preferences_pb
. This file may look slightly odd at first glance, but by using Google’s protobuf library5, we can inspect the contents of the file and read the saved preferences.
😅 I mean, what else did we expect at this point!
Encrypting DataStore
As of the time of writing, we don’t have any official support for encryption from the DataStore or JetSec library, however, we may not have long to wait 👀
We're following up because we didn't get this question live. Yes, and we have plans for more comprehensive Jetpack support for the AndroidKeystore and DataStore. Stay Tuned!
— Jon Markoff (@Jonmarkoff) May 13, 2022
In the meantime, an open-source solution exists in encrypted-datastore, a library that wraps DataStore and utilises Google’s tink library in a similar fashion to that of JetSec’s EncryptedSharedPreferences
. However, please only use this with extreme caution in your projects as this library is maintained by an individual and not a well-known trusted entity.
Let’s hope we see an official solution very soon!
Next up 🚀
In the upcoming posts within this series, we shall explore more of the OWASP Top 10 for Mobile. Next up is #3 Insecure Communication.
Thanks 🌟
Thanks as always for reading! I hope you found this post interesting, please feel free to tweet me with any feedback at @Sp4ghettiCode and don’t forget to clap, like, tweet, share, star etc
Further Reading
- owasp-top-five Companion App
- M2: Insecure Data Storage : OWASP Foundation
- Retrieve Data From Android Devices Without Rooting - Lam Pham
- Get Your Hand Dirty With Jetpack Datastore - Lam Pham
Footnotes
-
The
getPreferences(int mode)
method withinActivity
will use the activity’s name as the file name by default ↩ -
The implementation of
SharedPreferences
is in Java, soMap<String, Object>
is the true typing ↩ -
It is worth noting that it seems like
androidx.security:security-crypto-ktx:1.1.0-alpha03
does not correctly support a minimum version of API 21 and remains at API 23+ - I raised a bug report for this here ↩ -
Using
brew install sqlcipher
on macOS - check online for other OS install methods ↩ -
Using
brew install protobuf
on macOS - check online for other OS install methods ↩