Data Storage Security on Android
The protection of sensitive data, such as credentials, authentication tokens or private information(user information), is a key focus in mobile security. Public data should be available and accessible for everybody, but sensitive/private data needs to be protected or even better not get stored on the device in the first place.
Testing for Sensitive Data in Local Storage
Conventional wisdom suggests saving as little sensitive data as possible on permanent local storage. However, in most practical scenarios, at least some types of user-related data need to be stored. For example, asking the user to enter a highly complex password every time the app is started isn't a great idea from a usability perspective. As a result, most apps must locally cache some kind of authentication token. Other types of sensitive data, such as personally identifiable information (PII), might also be saved if the particular scenario calls for it.
A vulnerability occurs when sensitive data is not properly protected by an app when persistently storing it. The app might be able to store it in different places, for example locally on the device or on an external SD card. When trying to exploit these kinds of issues, consider that there might be a lot of information processed and stored in different locations. It is important to identify at the beginning what kind of information is processed by the mobile application and keyed in by the user and what might be interesting and valuable for an attacker.
Consequences for disclosing sensitive information can be various, like disclosure of encryption keys that can be used by an attacker to decrypt information.
Storing data is essential for many mobile apps, for example in order to keep track of user settings or data a user has keyed in that needs to be stored locally or offline. Data can be stored persistently in various ways. The following list shows those mechanisms that are widely used on the Android platform:
Shared Preferences
SQLite Databases
Realm Databases
Internal Storage
External Storage
Shared Preferences
Shared Preferences is a common approach to store Key/Value pairs persistently in the filesystem by using an XML structure. Within an activity the following code might be used to store sensitive information like a username and a password:
SharedPreferences sharedPref = getSharedPreferences("key", MODE_WORLD_READABLE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("username", "administrator");
editor.putString("password", "supersecret");
editor.commit();
Once the activity is called, the file key.xml is created with the provided data. This code is violating several best practices.
SQLite Database (Unencrypted)
SQLite is a SQL database that stores data to a .db file. The Android SDK comes with built in support for SQLite databases. The main package to manage the databases is android.database.sqlite. Within an activity the following code might be used to store sensitive information like a username and a password:
SQLiteDatabase appName = openOrCreateDatabase("appName",MODE_PRIVATE,null);
notSoSecure.execSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR, Password VARCHAR);");
notSoSecure.execSQL("INSERT INTO Accounts VALUES('admin','password');");
notSoSecure.close();
Once the activity is called, the database file appName is created with the provided data and is stored in clear text in /data/data/<package-name>/databases/appName.
There might be several files available in the databases directory, besides the SQLite database.
Journal Files: These are temporary files used to implement atomic commit and rollback capabilities in SQLite.
Lock Files: The lock files are part of the locking and journaling mechanism designed to improve concurrency in SQLite and to reduce the writer starvation problem.
Unencrypted SQLite databases should not be used to store sensitive information.
SQLite Databases (Encrypted)
By using the library SqlCipher SQLite databases can be encrypted, by providing a password.
SQLiteDatabase secureDB = SQLiteDatabase.openOrCreateDatabase(database, "dbpassword", null);
secureDB.execSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR,Password VARCHAR);");
secureDB.execSQL("INSERT INTO Accounts VALUES('admin','password');");
secureDB.close();
If encrypted SQLite databases are used, check if the password is hardcoded in the source, stored in shared preferences or hidden somewhere else in the code or file system. A secure approach to retrieve the key, instead of storing it locally could be to either:
Ask the user every time for a PIN or password to decrypt the database, once the app is opened (weak password or PIN is prone to brute force attacks), or
Store the key on the server and make it accessible via a web service (then the app can only be used when the device is online).
Internal Storage
Files can be saved directly on the internal storage of the device. By default, files saved to the internal storage are private to your app and other apps cannot access them. When the user uninstalls your app, these files are removed. Within an activity the following code might be used to store sensitive information in the variable test persistently to the internal storage:
FileOutputStream fos = null;
try {
fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(test.getBytes());
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
The file mode needs to be checked to make sure that only the app itself has access to the file. It should also be checked what files are opened and read within the app by searching for the class FileInputStream. Part of the internal storage mechanisms is also the cache storage. To cache data temporarily, functions like getCacheDir() can be used.
External Storage
Every Android device supports a shared external storage that you can be used to save files. Files saved to the external storage are world-readable and can be modified by the user when they enable USB mass storage to transfer files on a computer. following code might be used to store sensitive information in the file credntials.txt persistently to the external storage:
File file = new File (Environment.getExternalFilesDir(), " credntials.txt");
String password = "Password";
FileOutputStream fos;
fos = new FileOutputStream(file);
fos.write(password.getBytes());
fos.close();
Once the activity is called, the file is created with the provided data and the data is stored in clear text in the external storage.It’s also worth to know that files stored outside the application folder (data/data/<package-name>/) will not be deleted when the user uninstalls the application.
Local Storage
As already pointed out, there are several ways to store information within Android. check the source code to identify the storage mechanisms used within the Android app and whether sensitive data is processed insecurely.
Check AndroidManifest.xml for permissions to read from or write to external storage, like uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
Check the source code for keywords and API calls that are used for storing data:File permissions like:MODE_WORLD_READABLE or MODE_WORLD_WRITABLE: Usage of MODE_WORLD_WRITEABLE or MODE_WORLD_READABLE should generally be avoided for files, as any app would be able to read or write the file even though it may be stored in the app private data directory. If data needs to be shared with other applications, a content provider should be considered. A content provider offers read and write permissions to other apps and can make dynamic permission grants on a case-by-case basis.
Classes and functions like: SharedPreferences Class (Storage of key-value pairs)
FileOutPutStream Class (Using Internal or External Storage)
getExternal* functions (Using External Storage)
getWritableDatabase function (return a SQLiteDatabase for writing)
getReadableDatabase function (return a SQLiteDatabase for reading)
getCacheDir and getExternalCacheDirs function (Using cached files)
Encryption operations should rely on solid and tested functions provided by the SDK. The following describes different “bad practices” that should be checked in the source code:
Check if simple bit operations are used, like XOR or Bit flipping to “encrypt” sensitive information that is stored locally. This should be avoided as the data can easily be recovered.
Check if keys are created or used without taking advantage of the Android onboard features like the Android KeyStore.
Check if keys are disclosed by hardcoding them in the source code.
Typical Misuse: Hardcoded Cryptographic Keys
The use of a hardcoded or world-readable cryptographic key significantly increases the possibility that encrypted data may be recovered. Once it is obtained by an attacker, the task to decrypt the sensitive data becomes trivial, and the initial idea to protect confidentiality fails. When using symmetric cryptography, the key needs to be stored within the device and it is just a matter of time and effort from the attacker to identify it. Consider the following scenario: An application is reading and writing to an encrypted database but the decryption is done based on a hardcoded key:
this.db = localUserSecretStore.getWritableDatabase("SuperPassword123");
Since the key is the same for all app installations it is trivial to obtain it. The advantages of having sensitive data encrypted are gone, and there is effectively no benefit in using encryption in this way. Similarly, look for hardcoded API keys/private keys and other valuable pieces. Encoded/encrypted keys is just another attempt to make it harder but not impossible to get the crown jewels.
Let's consider this piece of code:
//A more complicated effort to store the XOR'ed halves of a key (instead of the key itself)
private static final String[] myCompositeKey = new String[]{
"oNQavjbaNNSgEqoCkT9Em4imeQQ=","3o8eFOX4ri/F8fgHgiy/BS47"
};
Algorithm to decode the original key in this case might look like this:
public void useXorStringHiding(String myHiddenMessage) {
byte[] xorParts0 = Base64.decode(myCompositeKey[0],0);
byte[] xorParts1 = Base64.decode(myCompositeKey[1],0);
byte[] xorKey = new byte[xorParts0.length];
for(int i = 0; i < xorParts1.length; i++){
xorKey[i] = (byte) (xorParts0[i] ^ xorParts1[i]);
}
HidingUtil.doHiding(myHiddenMessage.getBytes(), xorKey, false);
}
Verify common places where secrets are usually hidden:
resources (typically at res/values/strings.xml)
Example:
build configs, such as in local.properties or gradle.properties
Example:
buildTypes {
debug {
minifyEnabled true
buildConfigField "String", "hiddenPassword", ""${hiddenPassword}""
}
}
KeyStore
The androidKeyStore provides a means of (more or less) secure credential storage. As of Android 4.3, it provides public APIs for storing and using app-private keys. An app can create a new private/public key pair to encrypt application secrets by using the public key and decrypt the same by using the private key.
The keys stored in the Android KeyStore can be protected such that the user needs to authenticate to access them. The user's lock screen credentials (pattern, PIN, password or fingerprint) are used for authentication.
Stored keys can be configured to operate in one of the two modes:
User authentication authorizes the use of keys for a duration of time. All keys in this mode are authorized for use as soon as the user unlocks the device. The duration for which the authorization remains valid can be customized for each key. This option can only be used if the secure lock screen is enabled. If the user disables the secure lock screen, any stored keys become permanently invalidated.
User authentication authorizes a specific cryptographic operation associated with one key. In this mode, each operation involving such a key must be individually authorized by the user. Currently, the only means of such authorization is fingerprint authentication.
The level of security afforded by the Android KeyStore depends on its implementation, which differs between devices. Most modern devices offer a hardware-backed KeyStore implementation. In that case, keys are generated and used in a Trusted Execution Environment (TEE) or a Secure Element (SE) and are not directly accessible for the operating system. This means that the encryption keys themselves cannot be easily retrieved, even from a rooted device. You can check whether the keys are inside the secure hardware, based on the return value of the isInsideSecureHardware() method which is part of the class KeyInfo. Please note that private keys are often indeed stored correctly within the secure hardware, but secret keys, HMAC keys are, are not stored securely according to the KeyInfo on quite some devices.
Older KeyStore Implementations
Older Android versions do not have a KeyStore, but do have the KeyStore interface from JCA (Java Cryptography Architecture). One can use various KeyStores that implement this interface and ensure secrecy and integrity to the keys stored in the KeyStore implementation. All implementations rely on the fact that a file is stored on the filesystem, which then protects its content by a password. For this, it is recommended to use the BouncyCastle KeyStore (BKS). You can create one by using the KeyStore.getInstance("BKS", "BC");, where "BKS" is the KeyStore name (BouncyCastle Keystore) and "BC" is the provider (BouncyCastle). Alternatively you can use SpongyCastle as a wrapper and initialize the KeyStore: KeyStore.getInstance("BKS", "SC");.
Please be aware that not all KeyStores offer proper protection to the keys stored in the KeyStore files.
KeyChain
KeyChain class is used to store and retrieve system-wide private keys and their corresponding certificate (chain). The user will be prompted to set a lock screen pin or password to protect the credential storage if it hasn’t been set and if something gets imported into the KeyChain the first time.
Check that the app is using the Android KeyStore and Cipher mechanisms to securely store encrypted information on the device. Look for the pattern import java.security.KeyStore, import javax.crypto.Cipher, import java.security.SecureRandom and corresponding usages.
The store(OutputStream stream, char[] password) function can be used to store the KeyStore to disk with a specified password. Check that the password provided is not hardcoded and is defined by user input as this should only be known to the user.
Thanks for sharing!
UPVOTED!
By the way, I started following you. I would appreciate your follow back too. Thanks!