Encrypted Storage

SQLite databases, by default, are stored on internal storage, accessible only to the app that creates them.

At least, that is the theory.

In practice, it is conceivable that others could get at an app’s SQLite database, and that those “others” may not have the user’s best interests at heart. Hence, if you are storing data in SQLite that should remain confidential despite extreme measures to steal the data, you may wish to consider encrypting the database.

Perhaps the simplest way to encrypt a SQLite database is to use SQLCipher. SQLCipher is a SQLite extension that encrypts and decrypts database pages as they are written and read. However, SQLite extensions need to be compiled into SQLite, and the stock Android SQLite does not have the SQLCipher extension.

SQLCipher for Android, therefore, comes in the form of a replacement implementation of SQLite that you add as an NDK library to your project. It also ships with replacement editions of the android.database.sqlite.* classes that use the SQLCipher library instead of the built-in SQLite. This way, your app can be largely oblivious to the actual database implementation, particularly if it is hidden behind a ContentProvider or similar abstraction layer.

SQLCipher for Android is a joint initiative of Zetetic (the creators of SQLCipher) and the Guardian Project (home of many privacy-enhancing projects for Android). SQLCipher for Android is open source, under the Apache License 2.0.

Prerequisites

Understanding this chapter requires that you have read the chapter on database access.

Scenarios for Encryption

So, why might you want to encrypt a database?

Some developers probably are thinking that this is a way of protecting the app’s content against “those pesky rooted device users”. In practice, this is unlikely to help. As with most encryption mechanisms, SQLCipher uses an encryption key. If the app has the key, such as being hard-coded into the app itself, anyone can get the key by reverse-engineering the app.

Rather, encrypted databases are to help the user defend their data against other people seeing it when they should not. The classic example is somebody leaving their phone in the back of a taxi — if that device winds up in the hands of some group with the skills to root the device, they can get at any unencrypted content they want. While some users will handle this via the whole-disk encryption available since Android 3.0, others might not.

If the database is going anywhere other than internal storage, there is all the more reason to consider encrypting it, as then it may not even require a rooted device to access the database. Scenarios here include:

  1. Databases stored on external storage
  2. Databases backed up using external storage, BackupManager, or another Internet-based solution
  3. Databases explicitly being shared among a user’s devices, or between a user’s device and a desktop (note that SQLCipher works on many operating systems, including desktops and iOS)

Obtaining SQLCipher

SQLCipher is available from Zetetic. As of July 2016, the current shipping version was 3.5.0. It is very important for you to use 3.5.0 or higher, as earlier versions of SQLCipher for Android will not work on Android 7.0 or higher versions of Android.

In Android Studio, to add SQLCipher for Android to your project, just add the official AAR dependency:


dependencies {
    implementation 'net.zetetic:android-database-sqlcipher:3.5.0@aar'
}

Using SQLCipher

If you have existing code that uses classic Android SQLite, you will need to change your import statements to pick up the SQLCipher for Android equivalents of the classes. For example, you obtain SQLiteDatabase now from net.sqlcipher.database.sqlcipher, not android.database.sqlite. Similarly, you obtain SQLException from net.sqlcipher.database instead of android.database. Unfortunately, there is no complete list of which classes need this conversion — Cursor, for example, does not. Try converting everything from android.database and android.database.sqlite, and leave alone those that do not exist in the SQLCipher for Android equivalent packages.

Before starting to use SQLCipher for Android, you need to call SQLiteDatabase.loadLibs(), supplying a suitable Context object as a parameter. This initializes the necessary libraries. If you are using a ContentProvider, just call this in onCreate() before actually using anything else with your database. If you are not using a ContentProvider, you probably will want to create a custom subclass of Application and make this call from that class’ onCreate(), and reference your custom Application class in the android:name attribute of the <application> element in your manifest. Either of these approaches will help ensure that the libraries are ready before you try doing anything with the database.

Finally, when calling getReadableDatabase() or getWritableDatabase() on SQLiteDatabase, you need to supply the encryption key to use. For the purposes of book examples, a hard-coded passphrase is sufficient. However, those can be trivially reverse-engineered, and so they offer little real-world protection. But, they keep the code simple, which is useful when examining APIs.

The Database/ConstantsSecure-AndroidStudio sample app is yet another variation of the ConstantsBrowser sample that we have been using for most of the database examples. From the standpoint of the ConstantsBrowser activity and ConstantsFragment UI, nothing is different. However, DatabaseHelper uses SQLCipher, rather than SQLite.

In the DatabaseHelper constructor, we call loadLibs() on the SQLiteDatabase class, which is a required initialization step to get the native libraries set up:

  public DatabaseHelper(Context context) {
    super(context, DATABASE_NAME, null, SCHEMA);

    SQLiteDatabase.loadLibs(context);
  }
(from Database/ConstantsSecure-AndroidStudio/app/src/main/java/com/commonsware/android/sqlcipher/DatabaseHelper.java)

It also offers zero-argument getReadableDatabase() and getWritableDatabase() methods, akin to those offered by the regular SQLiteOpenHelper. However, the DatabaseHelper editions turn around and invoke the one-argument equivalents on the SQLCipher edition of SQLiteOpenHelper:

  SQLiteDatabase getReadableDatabase() {
    return(super.getReadableDatabase(PASSPHRASE));
  }

  SQLiteDatabase getWritableDatabase() {
    return(super.getWritableDatabase(PASSPHRASE));
  }
(from Database/ConstantsSecure-AndroidStudio/app/src/main/java/com/commonsware/android/sqlcipher/DatabaseHelper.java)

Here, the PASSPHRASE is just a hard-coded string:

  private static final String PASSPHRASE=
      "hard-coding passphrases is only for sample code;"+
      "nobody does this in production";
(from Database/ConstantsSecure-AndroidStudio/app/src/main/java/com/commonsware/android/sqlcipher/DatabaseHelper.java)

That is all the changes that are needed to use SQLCipher.

SQLCipher Limitations

Alas, SQLCipher for Android is not perfect.

It will add a few MB to the size of your APK file per CPU architecture. For most modern Android devices, this extra size will not be a huge issue, though it will be an impediment for older devices with less internal storage, or for apps that are getting close to the size limits imposed by the Play Store or other distribution mechanisms. The chapter on the NDK contains a section about a technology called libhoudini that can help reduce this bloat, albeit with a significant performance penalty.

However, the size is mostly from code, and that may cause a problem for Eclipse users. Eclipse may crash with its own OutOfMemoryError during the final build process. To address that, find your eclipse.ini file (location varies by OS and installation method) and increase the -Xmx value shown on one of the lines (e.g., change it to -Xmx512m).

Other code that expects to be using native SQLite databases will require alteration to work with SQLCipher for Android databases. For example, the SQLiteAssetHelper described elsewhere in this book would need to be ported to use the SQLCipher for Android implementations of SQLiteOpenHelper, SQLiteDatabase, etc. This is not too difficult for an open source component like SQLiteAssetHelper.

Passwords and Sessions

Given an encrypted database, there are several ways that an attacker can try to access the data, including:

  1. Use a brute-force attack via the app itself
  2. Use a brute-force attack on the database directly, by copying it to some other machine
  3. Obtain the password by the strategic deployment of a $5 wrench

The classic way to prevent the first approach is by having business logic that prevents lots of failed login attempts in a short period of time. This can be built into your login dialog (or the equivalent), tracking the number and times of failed logins and introducing delays, forced app exits, or something to add time and hassle for trying lots of passwords.

Since manually trying passwords is nasty, brutish, and long, many attackers would automate the process by copying the SQLCipher database to another machine (e.g., desktop) and running a brute-force attack on it directly. SQLCipher for Android has many built-in protections to help defend against this. So long as you are using a sufficiently long and complex encryption key, you should be fairly well-protected against such attacks.

Defending against wrenches is decidedly more difficult and is beyond the scope of this book.

About Those Passphrases…

Having a solid encryption algorithm, like the AES-256 used by default with SQLCipher for Android, is only half the battle. The other half is in using a high-quality passphrase, one that is unlikely to be guessed by anyone looking to break the encryption.

Upgrading to Encryption

Suppose you have an app already out on the market, and you decide that you want to add the option for encryption. It is fairly likely that the user will be miffed if they lose all their data in the process of switching to an encrypted database. Therefore, you will want to try to retain their data.

SQLCipher for Android does not support in-place encryption of database. However, it does support working with unencrypted databases and encrypted databases simultaneously, giving you the option of migration.

The approach boils down to:

Since both database files will exist at one time, you will find it simplest to use separate names for them (e.g., stuff.db and stuff-encrypted.db).

To see how this works, take a look at the Database/SQLCipherPassphrase-AndroidStudio, which is a variation of the original, non-ContentProvider “constants” sample app, this time using SQLCipher for Android and supporting an upgrade from a non-encrypted database to an encrypted one.

The bulk of the logic for handling the encryption upgrade is in a static encrypt() method on our DatabaseHelper:

  static void encrypt(Context ctxt) {
    SQLiteDatabase.loadLibs(ctxt);

    File dbFile=ctxt.getDatabasePath(DATABASE_NAME);
    File legacyFile=ctxt.getDatabasePath(LEGACY_DATABASE_NAME);

    if (!dbFile.exists() && legacyFile.exists()) {
      SQLiteDatabase db=
          SQLiteDatabase.openOrCreateDatabase(legacyFile, "", null);

      db.rawExecSQL(String.format("ATTACH DATABASE '%s' AS encrypted KEY '%s';",
                                  dbFile.getAbsolutePath(), PASSPHRASE));
      db.rawExecSQL("SELECT sqlcipher_export('encrypted')");
      db.rawExecSQL("DETACH DATABASE encrypted;");

      int version=db.getVersion();

      db.close();

      db=SQLiteDatabase.openOrCreateDatabase(dbFile, PASSPHRASE, null);
      db.setVersion(version);
      db.close();

      legacyFile.delete();
    }
  }
(from Database/SQLCipherPassphrase-AndroidStudio/app/src/main/java/com/commonsware/android/constants/DatabaseHelper.java)

First, we initialize SQLCipher for Android by calling loadLibs() on the SQLCipher version of SQLiteDatabase. We could do this someplace else, but for this sample, this is as good a spot as any.

We then create File objects pointing at the locations of the old, unencrypted database (with a name represented by a LEGACY_DATABASE_NAME static data member) and the new encrypted database (DATABASE_NAME). To get the File locations of those databases, we use getDatabasePath(), a method on Context, which returns the correct location for a database file given its name.

If the encrypted database exists, there is nothing that we need to do. Similarly, if it does not exist but the unencrypted database also does not exist, there is nothing that we can do. In either of those cases, we skip over the rest of the logic. In the first case, we already did the conversion (presumably); in the latter case, this is a new installation, and our SQLiteOpenHelper onCreate() logic will handle that. But, in the case where we do not have the encrypted database but do have the unencrypted one, we can create the encrypted database from the unencrypted data, which is what the bulk of the encrypt() method does.

To that, we:

The combination of doing all of that migrates our data from an unencrypted database to an encrypted one.

Then, we simply need to call encrypt() before we try loading our constants, from doInBackground() of our LoadCursorTask:

  private class LoadCursorTask extends BaseTask<Void> {
    private final Context ctxt;

    LoadCursorTask() {
      this.ctxt=getActivity().getApplicationContext();
    }

    @Override
    protected Cursor doInBackground(Void... params) {
      DatabaseHelper.encrypt(ctxt);
      return(doQuery());
    }
  }
(from Database/SQLCipherPassphrase-AndroidStudio/app/src/main/java/com/commonsware/android/constants/ConstantsFragment.java)

To test this upgrade logic, you will need to:

You will see your added constant appear along with all of the standard ones, yet if you examine /data/data/com.commonsware.android.constants/databases on your ARM emulator via DDMS, you will see that your database is now named constants-crypt.db instead of constants.db, as we have replaced the unencrypted database with an encrypted one.

Changing Encryption Passphrases

Another thing the user might wish to do is change their passphrase. Perhaps they fear that their existing passphrase has been compromised (e.g., a narrow escape from a $5 wrench). Perhaps they rotate their passphrases as a matter of course. Perhaps they simply keep typing in their current one incorrectly and want to switch to one they think they can enter more accurately.

SQLCipher for Android supports a rekey PRAGMA that can accomplish this. Given an open encrypted database db — opened using the old passphrase – you can change the password to a newPassword string variable via:


db.execSQL(String.format("PRAGMA rekey = '%s'", newPassword));

Note that this may take some time, as SQLCipher for Android needs to re-encrypt the entire database.

Dealing with the Version 3.0.x Upgrade

If you are starting with SQLCipher for Android with the 3.0.x release, all is good.

If you have been using SQLCipher for Android from previous releases, but you are still in development mode, all is still good, so long as you can wipe out your old databases.

If you have apps in production using SQLCipher for Android from previous releases, you will have a small headache: the database structure has changed. SQLCipher for Android provides us with a PRAGMA cipher_migrate that we can run to upgrade the database in place to the new structure, once we have opened the database with our passphrase. However:

  1. There is no great built-in place to put the code for calling this pragma
  2. You do not want to blindly call this pragma every time you open the database, as it results in extra processing time

SQLCipher for Android, in an attempt to help with this, offers a modified version of methods like openOrCreateDatabase() on SQLiteDatabase, ones that take a SQLiteDatabaseHook implementation as the last parameter. This interface requires two methods:

  1. preKey(), called after the database is opened but before the passphrase is applied
  2. postKey(), called after the database is opened and after the passphrase is applied, but before anything else is done (e.g., standard SQLiteOpenHelper schema version checking)

Both methods are passed the SQLiteDatabase as a parameter, for you to do with as needed. So, for example, you could have a postKey() implementation that does the postKey() call only if needed:


public class SQLCipherV3Hook implements SQLiteDatabaseHook {
  private static final String PREFS=
      "net.sqlcipher.database.SQLCipherV3Helper";

  public static void resetMigrationFlag(Context ctxt, String dbPath) {
    SharedPreferences prefs=
        ctxt.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
    prefs.edit().putBoolean(dbPath, false).commit();
  }

  @Override
  public void preKey(SQLiteDatabase database) {
    // no-op
  }

  @Override
  public void postKey(SQLiteDatabase database) {
    SharedPreferences prefs=
        getContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
    boolean isMigrated=prefs.getBoolean(database.getPath(), false);

    if (!isMigrated) {
      database.rawExecSQL("PRAGMA cipher_migrate;");
      prefs.edit().putBoolean(database.getPath(), true).commit();
    }
  }
}

You can also pass a SQLiteDatabaseHook implementation into the SQLiteOpenHelper constructor as the fifth parameter, which will be used when SQLiteOpenHelper works with the underlying SQLiteDatabase.

Multi-Factor Authentication

Another way to effectively boost the strength of your security is to implement your own multi-factor authentication. In this case, the passphrase is not obtained solely through the user typing in the whole thing, but instead is synthesized from two or more sources. So, in addition to some EditText widget for entering in a portion of the passphrase, the rest could come from things like:

You, in code, would concatenate the pieces together, possibly using delimiters that cannot be typed in (e.g., ASCII characters below 32) to denote the sources of each segment of the passphrase. The result would be the actual passphrase you would use with SQLCipher for Android.

The objective is to make it easier for users to have more complex passphrases, while not having to type in something complex every time. Tapping an NFC tag is much faster than tapping out a passphrase on a typical phone keyboard, for example. Also, the “something you know and something you have” benefit of multi-factor authentication can help with defending against $5 wrench attacks: if the NFC tag was destroyed, and the user never knew the portion of the passphrase stored on it, the user cannot divulge it.

Of course, this adds risks, such as the NFC tag being destroyed accidentally (e.g., “my dog ate it”). This can be mitigated in some cases by some “admin” being able to reset the password or supply a new NFC tag. In that case, getting the credentials requires two kidnappings and two $5 wrenches (or the serial application of a single $5 wrench, if budgets preclude buying two such wrenches), adding to the degree of difficulty for breaking the encryption by that means.

Detecting Failed Logins

If you try to decrypt a database using the incorrect passphrase — whether an attempt by outsiders to use the app, or the user “fat-fingering” the passphrase and making a typo — you will get an exception:

11-19 09:17:22.700: E/SQLiteOpenHelper(1634): net.sqlcipher.database.SQLiteException: file is encrypted or is not a database

Alas, this is not a specific exception, making it a bit difficult to detect failed passphrases specifically. Your options are:

SQLCipher for Android and Performance

Some developers worry about the overhead that encryption will place on the database I/O, and therefore worry that SQLCipher for Android will make their app unacceptably slow.

The impact of SQLCipher is not that bad, particularly for hardware with faster CPUs. Encryption is CPU-intensive, so faster CPUs reduce the overhead of the encryption. Also, since the disk I/O is comparable between SQLite and SQLCipher, the fact that flash memory is slow will mean that disk I/O, not decryption speed, will be the primary determinant of the speed of your queries. Similarly, disk I/O will count for more than CPU speed for the encryption needed for INSERT/UPDATE/DELETE operations.

For example, porting one relatively crude benchmark to use SQLCipher for Android showed no statistically significant performance difference from the SQLite edition on a Nexus 5 running Android 4.4.2.

To the extent that encryption adds overhead, it will tend to magnify existing problems. For example, anything that involves a “table scan” (i.e., a non-indexed lookup of database contents) will need more pages to be decrypted and, therefore, more decryption time. If your database I/O is well-tuned for SQLite, such as adding appropriate indexes, then your SQLCipher for Android overhead should be nominal.

Of course, the worse the CPU, the worse the story, and so older/cheaper devices may fare worse with SQLCipher for Android by comparison.

Encrypted Preferences

There are effectively three forms of data storage in Android:

You can encrypt SQLite via SQLCipher for Android, as seen in this chapter. You can encrypt arbitrary files as part of your data format, such as via javax.crypto.

What is not supported, out of the box, is a way to encrypt SharedPreferences.

There are two approaches for encrypting the contents of SharedPreferences:

  1. Encrypt the container in which the SharedPreferences are stored
  2. Encrypt each preference value as you store it in the SharedPreferences, and decrypt it when you read the value back out

Encryption via Custom SharedPreferences

SharedPreferences is an interface. Hence, you can create other implementations of that interface that store their data in something other than unencrypted XML files.

CWSharedPreferences is one such implementation. You can find it in the cwac-prefs project on GitHub.

CWSharedPreferences handles the SharedPreferences and SharedPreferences.Editor interfaces, along with the in-memory representations of the preferences. It then delegates the work of storing the preferences to a strategy object, implementing a strategy interface (CWSharedPreferences.StorageStrategy). Two such strategy implementations are supplied in the project: one using ordinary SQLite, and one using SQLCipher for Android.

The basic recipe for using CWSharedPreferences is:


new SQLCipherStrategy(getContext(), NAME, "atestpassword", LoadPolicy.SYNC)

(here, NAME is the name of the set of preferences, "atestpassword" is your passphrase, and LoadPolicy.SYNC indicates that the preferences should be loaded from disk immediately, not on a background thread)


new CWSharedPreferences(yourStrategyObjectGoesHere);

Encryption via Custom Preference UI and Accessors

The big drawback to the custom SharedPreferences is the fact that you cannot get the PreferenceScreen system to work with it. The preference UI is hard-wired to use the stock implementation of SharedPreferences and does not appear to support any way to substitute in some other implementation.

Hence, another approach is to keep things in standard SharedPreferences’ XML files, but encrypt text values on a preference-by-preference basis. Since the data type needs to remain the same, most likely you would restrict this to encrypting strings (e.g., EditTextPreference, ListPreference) rather than numbers, booleans, etc.

To do this, you would need to:

The downsides to this approach include:

IOCipher

SQLCipher for Android is also used as the backing store for IOCipher. IOCipher is a virtual file system (VFS) for Android, allowing you to write code that looks and works like it uses normal file I/O, yet all of the files are actually saved as BLOBs in a SQLCipher for Android database. The result is a fully-encrypted VFS, inheriting all of SQLCipher’s security features, such as default AES-256 encryption. This may be easier for you to use than encrypting and decrypting files individually via javax.crypto, for example.

IOCipher is considered to be in pre-alpha state as of November 2012.