π 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:
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 givenΒΉ, 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?>
Β² 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 versionsΒ³.
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> |
Job Offers
β¨ 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.
$ 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Β sqlcipher
β΄ 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 utilization 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 libraryβ΅, 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
- 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
[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
This article was originally published onΒ proandroiddev.com on June 05, 2022