Securing the data stored within an app is just as important as it is often ignored.
Because data is commonly not dealt with properly, it ends up being exploited and exposed to unintended leakage a lot more than it should.
Data storage security among top concerns
According to this 2016 research by The Open Web Application Security Project, insecure data storage is among top security concerns in mobile apps, second only to improper platform usage.
Luckily, Android provides us with various ways to keep data safe.
The first post from our Pentesting Series covered some key concepts of a TLS connection and showed how to achieve a secure network layer in an app which would pass a pentest.
This edition will focus on securing user data in the app storage.
Shared preferences – a security risk
Shared preferences are probably the most used Android API for storing small amounts of data. It is based on a key-value pair model, and there's a very simple process evolving in the background.
It saves the key-value pairs into an .xml file, which is located in the app data directory in a directory called
shared_prefs. Since the data is saved in a plain .xml file, the sensitive data we save in there is a security risk – especially if our
AndroidManifest.xml configuration states that app backups are allowed:
<application android:allowBackup="true" />
allowBackup attribute is set to true by default when a new project is created, as confirmed in Android Studio 3.5.2. Taking that into account, let's see how easy it is to extract shared preferences data from an app.
To exploit the
allowBackup flag we need
adb connection to the device that is running our app. Afterwards, it is enough to run the following command:
$ adb backup com.example.app
This command will result in the entire application backup file,
backup.ab. This file contains all the information needed to restore the application in case we want to migrate our system settings and app settings to another device.
To see the contents of the
.ab file, use any file-unpacking tool. This example uses Android Backup Extractor. This tool allows us to create a .tar file with the following command:
$ abe unpack backup.ab backup.tar
After acquiring the
.tar file and unpacking it, all the contents from the original
backup.ab file become available. One of those files is the shared preferences .xml file. By opening the file, we can see all the key-value pairs used in the app.
For example, let’s say the extracted .xml file looked like this:
<?xml version="1.0" encoding="utf-8"? standalone=”yes” ?> <map> <string name=”AUTH_TOKEN”>89239d28328dj2398do1320942</string> <string name=”USER_ID”>qwr3131c</string> </map>
Even when using the shared preferences in private mode, it is simple to extract and read sensitive data from the app, as these are in plain text.
What can we do to improve it?
Preparation phase - What you'll need to know
The first thing that comes to mind is to simply set the
allowBackup to false. This is a good first step, but we need to go one step further to really complicate things for attackers.
The above exploit is very simple and can be done by any attacker, so we should expect that extracting this file is always possible to a more experienced attacker, even with backup disabled.
As we don't have control over the entire Android framework, we have to rely on Google to make it difficult for attackers to access the file. What we can and should do is to focus on the issue of plain text data inside that file.
One way of solving this issue is by using an encryption algorithm. Assuming you already know the basics of cryptography, we will not delve into specifics.
This example will show using a symmetric-key algorithm called AES. This essentially means that there will be one secret key for encryption and decryption.
The general idea is to use that secret key to encrypt the values we store in shared preferences and end up with an .xml file that has only encrypted data.
A cleaner approach is having a separate shared preference file just for sensitive data we want to encrypt and not mixing everything with other data.
Since we use a secret key, we have to secure it doesn't get in the hands of the attacker – otherwise our efforts would be wasted.
There are several ways to tackle this problem. The first approach is to create a secret key every time we need it. The second is to create it only once and store it somewhere safe.
Recreate a key when needed
Recreating a secret key every time could be based on something only the user knows, like a password. After we acquire the password, we can use a key derivation function, like PBKDF2 to create the secret key.
Keeping in mind that user passwords are usually of low entropy, we should make sure that we have a large number of iterations when deriving the key in order to avoid easy brute-force attacks.
Iteration count is just one of the parameters for PBKDF2. To see other parameters for a more secure key, check out this amazing article.
One of the main cons in this approach is from the app's UX side.
It is often very hard to incorporate this in the app without annoying the user, especially if we talk about saving and retrieving data from shared preferences, which occurs very often. Also, it could have a negative impact on the app's performance.
These cons are definitely fixable problems, but the correct solution depends on the particular use cases of the app which we will not go into.
Creating a permanent key
The second approach is creating the key once and securely saving it somewhere for later use. This approach is is easier to integrate seamlessly in our application flow.
But where to store the secret key? The preferred way is using the Android keystore system, available since Android 4.3, API level 18.
Unfortunately, due to some issues, it is advised to use it only from Android 6, API level 23 and up, meaning succeeding gets a bit complicated when supporting lower API levels.
One option is to have the key saved on a server and fetch it when needed.
An alternative is to save the created secret key in shared preferences. In that case, it's important to use some sort of obfuscation when storing the secret key.
When developing for API level 23+ and not wanting to create custom secure shared preferences, it is highly recommended to use the Security dependency from Jetpack for a complete secure shared preferences solution, called EncryptedSharedPreferences.
Under the hood, it uses the Android keystore system in combination with tink. The Android security team is working together with the tink team to provide support for API level 19 and above, which would be awesome!
That's a lot of information to take in, so let’s recap it in a table to help you get a better feel of which solution is more suitable for the app in regards of implementation effort and maintenance, security and API level.
File storage – two main locations to save files
Before we go into the specifics of file storage, it's important to know what kind of encryption method is used on the device level in the context of user data.
Ever since Android 7, users have been given the benefit of file-based encryption. This means that encryption is run on file level instead of the block level. It isolates and protects files for individual users on their device.
For older OS versions, specifically versions Android 4 through 9, a full-disk encryption is used. Recent changes in the OS makes file-based encryption mandatory for devices that run Android 10. To read more about this classification, check out the official documentation.
Regarding file storage on the application level, Android offers two main locations to save files – the external and internal storages. In case of sensitive data in files, avoid using external storage because every app can gain access to it with the appropriate permission.
In addition, external storage can be removed. For example, when using a removable SD card it is highly advised to encrypt the files. Also, when working with files from external storage, make sure to perform input validation to be sure you are working with the expected file.
Internal storage, on the other hand, is a private sandbox area accessible only to your app. This makes it a more suitable place to save files that contain sensitive data and app's cache data.
Furthermore, when it comes to internal storage, all data related to the app will be deleted when a user deletes the app, and no permissions will be needed to read/write files in the app's dedicated directory.
Preparation phase - What you'll need to know
When saving a file containing sensitive data in the external storage, the file should definitely be encrypted. Depending on the API level supported in the app, there will be different options.
In the case when the minSdk is API level 23, it is highly recommended to use the already mentioned security dependency. From there, you will be able to find a class named
EncryptedFile which will allow easy creation and reading of encrypted files.
The creation of an encrypted file is very simple:
val encryptedFile = EncryptedFile.Builder( File(*directory*, "my_file.txt"), context, MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB ).build()
At the time of writing this post, the above masterKeyAlias parameter is the configuration recommended by the Android security team. Under the hood, the file encryption uses tink to encrypt the contents of the file as segments.
Before API level 23, there is really no out-of-the-box solution provided by the Android framework. One alternative is to use the
CipherOutputStream available in javax.crypto.
One of the parameters to create an instance of
Cipher, which provides the functionality of a cryptographic cipher for encryption and decryption. The configuration of the cipher depends on the algorithms that are available on the API level your app supports and the app's use cases.
Both methods for external storage described above can also be applied to saving files in the internal storage. To avoid double encryption of the files, keep in mind that all files in the internal storage are encrypted by default starting from Android 10.
If you want to delete the files, note that the method
delete from the
File class only removes the file's reference from the file system table, whereas the file will still exist on the disk until other data overwrites it, leaving it vulnerable to recovery.
In most cases, we won't need to handle this, but it's advised to overwrite the data with other random data before deleting the file but as an extra security measure.
Databases – stored in a private directory
Time to focus on SQLite databases on Android.
The SQLite database uses
.db files to store data. The
.db are stored in the private directory of your application, but the file can easily be extracted using the adb backup command.
Make sure to disable backups, in case there's sensitive information stored in your SQLite database, similar to the case described in the shared preferences section above.
In addition, note that these databases are not encrypted and that it's generally not advised to store sensitive data inside an unencrypted SQLlite database.
When doing operations on SQLite databases which contain sensitive information, it's important to have proper input validation. Otherwise, SQL injection is a viable attack vector.
In other words, an attacker can manipulate an SQL query that will be interpreted as a part of the command or query, and as a result might retrieve arbitrary database records or manipulate database content.
Those kinds of attacks are much more common in the server-side web services, but exploitable instances also exist within mobile apps.
Preparation phase - What you'll need to know
As mentioned earlier, SQLite does not provide an out-of-the-box solution for encrypting the database. Therefore, it is not recommended to save sensitive information, especially if that data is not encrypted beforehand.
One way to solve this problem is by using SQLCipher, a popular SQLite extension that offers a password-encrypted database.
Other than a small performance hit, the most noticeable negative side of using SQLCipher is its relatively big footprint on the APK size of around 7MB, unless you are using app bundles.
To use SQLCipher with Room, a persistence library that provides an abstraction layer over SQLite, follow the official instructions on the Github page. Another alternative is to use cwac-saferoom and reap the benefits from SQLCipher.
Regarding SQL injection attacks, it is advised to use parameterized query methods (query, delete, and update). To understand the exploit, check this very common example of a custom SQL query prone to an SQL injection attack:
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password +"'"; Cursor c = db.rawQuery( sql, null );
Now imagine that the user enters this in the input fields:
username = 1' or '1' = '1 password = 1' or '1' = '1
This would result in the following query:
SELECT * FROM users WHERE username='1' OR '1' = '1' AND Password='1' OR '1' = '1'
The above query will return all records in the
users table due to the condition '1' = '1' being evaluated as true. This can have some negative impact on the apps logic but could be easily avoided by using input validation.
No excuse to keep data unprotected
Dealing with sensitive information can be tricky.
Luckily, Android offers many data protection tools. In most cases, a standard encryption algorithm will be enough to secure your data but researching alternative options is advisable.
Finally, remember to always check the proven and stable methods when working with encryption algorithms. And unless you are an expert, don't try to invent an encryption algorithm (at home).
The optical illusion on the cover illustration was created by designer Marijana Šimag.