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:
- Users are far too likely to choose poor passphrases, such as
12345
- Users have to enter that passphrase every time, which makes using better passphrases more annoying
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:
- A database encrypted by a generated passphrase
- That generated passphrase encrypted using a key that is inaccessible except from our app
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:
-
EncryptedFile
may have problems on some hardware. - The database is unusable, except via the app. For example, you cannot copy the database off the device and use it with another client, unless you also arrange to get a copy of the passphrase. In development, that might be a matter of logging it with Logcat… so long as you do not accidentally ship such code. In production, this will cause issues with backup solutions, if they back up the database but not the encryption key needed to actually use that database.
- Anyone who can get into the phone can get into the database via the app. “Social engineering” attacks can trick users into handing over their phones in an unlocked state. This can be improved somewhat by creating a custom
MasterKeys
that includes options likesetUserAuthenticationRequired(true)
. - The implementation here keeps things simple and does the disk I/O for the
EncryptedFile
on the current thread, which may well be the main application thread. This is not ideal, and a production-grade app ideally would do a better job.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.