SQLCipher and Passphrases

The fun with SQLCipher for Android — particularly when used with Room and dependency inversion frameworks — is in getting the passphrase to use for the database.

In the preceding chapter, we hard-coded the passphrase. This is simple but insecure. Every user has the same passphrase, and that passphrase is fairly easy to find out via reverse-engineering the app’s APK. At that point, SQLCipher for Android adds no real security over regular SQLite, so you have all the costs (e.g., APK size, runtime performance) with no benefits.

Instead, we need to have a passphrase that is unique to the app installation. Ideally, attackers would have no way to find out the passphrase for any user. But, even if they could get the passphrase for one user, that would only affect that user — it does not immediately compromise all other users.

So, in this chapter, we will explore alternative ways of setting up passphrases.

Generating a Passphrase

The classic solution for this problem is to have the user provide their own passphrase. We will explore that option later in the chapter.

However, typically, that solution has issues:

The nice thing about the hard-coded passphrase is that the user does not have to worry about it. They just use the app normally.

We could generate a per-installation passphrase and use that to encrypt the database. We then wind up with a “chicken and egg” problem: where do we store that generated passphrase, such that it cannot be accessed by attackers? We could store it in an ordinary file, but then any attacker that can get to the database file can also get to the passphrase file, and our security is blown.

We could store the generated passphrase in an encrypted file. This gives us another form of the “chicken and egg” problem: how are we going to encrypt it? After all, most encryption systems, like SQLCipher, require a passphrase, and so if encrypting a passphrase requires another passphrase, we seem to have gotten nowhere.

However, for plain files, Google has better options for encryption. In particular, we can use the Security library from the Jetpack. This encrypts data using encryption keys that (on most hardware) is stored in a hardware-backed “keystore”, one that is designed to be tamper-resistant. So, we wind up with:

We still have a risk of an attacker accessing our generated passphrase, as we will see later in the chapter, but it really starts to ramp up the difficulty, and with some careful work, we can reduce the risk even further.

So, with all of that in mind, let us look at the ToDoGen module of the book’s primary sample project. This is a clone of ToDoCrypt that we saw in the previous chapter, except that we use the Security library and a generated passphrase, rather than a hard-coded one.

Creating the Passphrase

The passphrase that we generate will never be entered by a user — it is purely for internal use. Hence, we do not need to worry about how easy it is to type in. However, Zetetic requires binary keys to not have zero byte values.

So, for the purposes of this sample, we will go with a 32-byte passphrase, rejecting any that contain zero as a value:

  private fun generatePassphrase(): ByteArray {
    val random = SecureRandom.getInstanceStrong()
    val result = ByteArray(PASSPHRASE_LENGTH)

    random.nextBytes(result)

    // filter out zero byte values, as SQLCipher does not like them
    while (result.contains(0)) {
      random.nextBytes(result)
    }

    return result
  }

Safely Storing the Passphrase

That passphrase is generated by a PassphraseRepository, backed by a EncryptedFile instance. If we do not have a passphrase file, we generate a passphrase and save it in an encrypted form. If we do have a passphrase file, we decrypt it to get the passphrase to use:

package com.commonsware.todo.repo

import android.content.Context
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKeys
import java.io.File
import java.security.SecureRandom

private const val PASSPHRASE_LENGTH = 32

class PassphraseRepository(private val context: Context) {
  fun getPassphrase(): ByteArray {
    val file = File(context.filesDir, "passphrase.bin")
    val encryptedFile = EncryptedFile.Builder(
      file,
      context,
      MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
      EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()

    return if (file.exists()) {
      encryptedFile.openFileInput().use { it.readBytes() }
    } else {
      generatePassphrase().also { passphrase ->
        encryptedFile.openFileOutput().use { it.write(passphrase) }
      }
    }
  }

  private fun generatePassphrase(): ByteArray {
    val random = SecureRandom.getInstanceStrong()
    val result = ByteArray(PASSPHRASE_LENGTH)

    random.nextBytes(result)

    // filter out zero byte values, as SQLCipher does not like them
    while (result.contains(0)) {
      random.nextBytes(result)
    }

    return result
  }
}

The specific recipe used here, in terms of the MasterKeys and various Scheme objects, comes from Google and appears to be a reasonable set of defaults.

That EncryptedFile class comes from the androidx.security:security-crypto added to the module’s build.gradle file:

    implementation "androidx.security:security-crypto:1.0.0"

The PassphraseRepository itself is created by Koin in ToDoApp, as part of a module:

    single { PassphraseRepository(androidContext()) }

The net effect is that our PrefsRepository stores that generated passphrase in the encrypted SharedPreferences, and that repository is available for other objects to use via Koin.

Using the Generated Passphrase

Our ToDoDatabase factory function needs that generated passphrase. So, we add it to the function signature and remove the hard-coded PASSPHRASE that ToDoCrypt used:

package com.commonsware.todo.repo

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import net.sqlcipher.database.SupportFactory

private const val DB_NAME = "stuff.db"

@Database(entities = [ToDoEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class ToDoDatabase : RoomDatabase() {
  abstract fun todoStore(): ToDoEntity.Store

  companion object {
    fun newInstance(context: Context, passphrase: ByteArray) =
      Room.databaseBuilder(context, ToDoDatabase::class.java, DB_NAME)
        .openHelperFactory(SupportFactory(passphrase))
        .build()
  }
}

When Koin creates our ToDoDatabase, we pull the passphrase from PassphraseRepository, causing it to be generated if needed:

    single {
      val passRepo: PassphraseRepository = get()

      ToDoDatabase.newInstance(androidContext(), passRepo.getPassphrase())
    }

The result is no change from the user’s standpoint, but we have replaced the hard-coded passphrase with a generated passphrase, backed by Jetpack-managed device encryption.

Pros and Cons

This required relatively little in the way of code changes. It does not change the user experience. And, it is a lot better from a security standpoint than having a hard-coded passphrase.

However, it is not perfect:


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.