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:

  1. SharedPreferences
  2. Room databases
  3. 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:

val prefs = context?.getSharedPreferences("mySharedPrefFile", Context.MODE_PRIVATE) ?: return
// Store some credentials we might not want others to read
prefs.edit().putString("mySecretKey", "mySecretValue").apply()

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:

$ adb shell

# Run as your package name
$ run-as dev.spght.example

# Show your app's local folders
$ ls
cache  code_cache  shared_prefs

# Show what is in the shared_prefs folder
$ ls shared_prefs
mySharedPrefFile.xml

# Print out the contents of the file
$ cat shared_prefs/mySharedPrefFile.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="mySecretKey">mySecretValue</string>
</map>

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.

# Make a backup file for app 
$ adb backup -noapk -f backup.ab dev.spght.example.prod
WARNING: adb backup is deprecated and may be removed in a future release
Now unlock your device and confirm the backup operation...

# Use ABE to unpack the backup file (12345 is the password as set through the device)
$ java -jar abe.jar unpack backup.ab output.tar 12345
Calculated MK checksum (use UTF-8: true): 739E00581D0B3EB5A17F3E1A43D21F561BA8E8C1CA35C48E636495E9C57EF0A1
31% 63% 67%
4096 bytes written to output.tar.

# Extract the unpacked backup
$ tar xvf output.tar
x apps/dev.spght.example.prod/_manifest
x apps/dev.spght.example.prod/sp/example.xml

# View the extracted shared preference file
$ cat apps/dev.spght.example.prod/sp/example.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="mySecretKey">mySecretValue</string>
</map>

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 the adb 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

val masterKey = MasterKey.Builder(this)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

EncryptedSharedPreferences.create(this, "myEncryptedPrefsFile", masterKey, PrefKeyEncryptionScheme.AES256_SIV, PrefValueEncryptionScheme.AES256_GCM).edit {
    putString("mySecretKey", "mySecretValue")
}

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() and userAuthenticationValiditySeconds() can be used to create a time-bound key. Time-bound keys require authorization using BiometricPrompt 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

<map>
    <string name="ARTYCGdkOdwAqjLCjWdsepYfbO+lJzJFFrHIta8JSE0=">ASTonpk6n1buL/VN6mB/S95HNcyHzvFp5qbcpkJMjSQbqRkzO3HWe5KKcP6eTwtzIFamU3Ag</string>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a90155259183605a12481ccf406838afc98126862109cc5e083185fe7259a052ccb4d1f859ac62ee1ab624f4b35df36c53a23c24547ee322aacc4526a654fd99e9997c0e6bf389b3bf2706a2e29b63d99a1e74535d68457fda16f04e706f127ca09c01622e26db72339720e814af1d6533efc8705eb8a073fa4e5afb2dbfb9446eaa27801942e0c8a8462ff6aed86738f500cdb77655294549810c2cfbe011b4c3900e3504eb3947502c7c1a4408e790e0a601123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118e790e0a6012001</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801700a3e737db7b3f8385be4aea6f74fbb844b86532055fa99032c67df4c5a5543e799dd9a621013ba716749b3decef994896914cea1d8dbf25fc13e6a7c6f19a0488dbd4a339642f4bfc4ad82fe1b27b4d4ca0a79c55e57354389f7c2e115af8c0d6f8fff0093299c2481a6cf25b5444beb614c227a03c64e1262ecc0a137a1273df34ae24d78ddd51a440899bda2a702123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b657910011899bda2a7022001</string>
</map>

✨ 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.

Database Location

Using sqlite3 on the command line, we can inspect the database, list the tables and show table data including the contents.

$ sqlite3 insecure-database

sqlite> .tables
User    android_metadata    room_master_table

sqlite> .schema User
CREATE TABLE `User` (`uid` INTEGER NOT NULL, `first_name` TEXT, `last_name` TEXT, PRIMARY KEY(`uid`));

sqlite> SELECT * FROM User LIMIT 5;
1653992407422|First|Last
1653992407614|First|Last
1653992407658|First|Last
1653992407685|First|Last
1653992407686|First|Last

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.

// Use a user-entered passphrase to encrypt/decrypt
val passPhrase: ByteArray = "password".encodeToByteArray()
val sqlCipherSupportFactory: SupportSQLiteOpenHelper.Factory = SupportFactory(passPhrase)

val database = Room.databaseBuilder(
    applicationContext,
    YourRoomDatabase::class.java,
    "secure-database")
 .openHelperFactory(sqlCipherSupportFactory)
 .build()

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 sqlcipher4 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.

$ sqlcipher secure-database

sqlite> .tables
Error: file is not a database

sqlite> PRAGMA key = 'password';
ok

sqlite> .tables
User    room_master_table

sqlite> SELECT * FROM User LIMIT 5;
1653992407422|First|Last
1653992407614|First|Last
1653992407658|First|Last
1653992407685|First|Last
1653992407686|First|Last

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

val Context.dataStore by preferencesDataStore(name = "insecure-data-store")

val pref1 = stringPreferencesKey("example_pref")
val pref2 = stringPreferencesKey("example_pref_2")

dataStore.edit { settings ->
    settings[pref1] = "My 1st Pref"
    settings[pref2] = "My 2nd Pref"
}

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.

$ hexdump -C insecure-data-store.preferences_pb
00000000  0a 1d 0a 0c 65 78 61 6d  70 6c 65 5f 70 72 65 66  |....example_pref|
00000010  12 0d 2a 0b 4d 79 20 31  73 74 20 50 72 65 66 0a  |..*.My 1st Pref.|
00000020  1f 0a 0e 65 78 61 6d 70  6c 65 5f 70 72 65 66 5f  |...example_pref_|
00000030  32 12 0d 2a 0b 4d 79 20  32 6e 64 20 50 72 65 66  |2..*.My 2nd Pref|
00000040

$ protoc --decode_raw < insecure-data-store.preferences_pb
1 {
  1: "example_pref"
  2 {
    5: "My 1st Pref"
  }
}
1 {
  1: "example_pref_2"
  2 {
    5: "My 2nd Pref"
  }
}

😅 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 👀

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

Footnotes

  1. The getPreferences(int mode) method within Activity will use the activity’s name as the file name by default 

  2. The implementation of SharedPreferences is in Java, so Map<String, Object> is the true typing 

  3. 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 

  4. Using brew install sqlcipher on macOS - check online for other OS install methods 

  5. Using brew install protobuf on macOS - check online for other OS install methods