Encrypt Data in Android Room Data Base
Most applications use a local database to store information that is required for data cache or to store credentials and sometimes this information is sensitive because include user data, authentication credentials and are not secured properly. In android to attend this problem we can use the library SQLCipher that help us to encrypt a normal SQL database and is compatible with room android.
Encryption
One of the most popular method to secure sensitive data is the encryption that consist in the process to encoding information of converting human-readable plaintext into incomprehensible text. Encryption need use a cryptography key that is a string of characters used for altering data so that it appears random.
SQLCipher
SQLCipher is an open source library that provides transparent, secure 256-bit AES encryption of SQLite database files with an easy integration with Java and Kotlin applications.
Implementation
Dependency
Add the cipher dependency in your app build.gradle or the module where you will implement your data layer.
implementation "net.zetetic:android-database-sqlcipher:4.5.0"
implementation "androidx.security:security-crypto:1.1.0-alpha03"
Replace the 4.5.0 version to the latest
Room integration
The older room builder may look like this
Room.databaseBuilder(
context,
DataBase::class.java,
DATABASE_NAME
).build()
Now with cipher
Room.databaseBuilder(
context,
DataBase::class.java,
DATABASE_NAME
).openHelperFactory(SupportFactory(securePassphrase)).build()
SecurePassphrase is basically your key in ByteArray format. You can obtain this Passphrase in different ways.
From a simple String:
"secureKey".toByteArray()
From a base64 decoded string:
Base64.decode("secureKey", Base64.DEFAULT)
From a secure random number:
private fun generatePassphrase(): ByteArray {
// Generate a 256-bit key
val outputKeyLength = keyLength
val secureRandom = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
SecureRandom.getInstanceStrong()
} else {
// Do *not* seed secureRandom! Automatically seeded from system entropy.
SecureRandom()
}
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(algorithm)
keyGenerator.init(outputKeyLength, secureRandom)
var tempArray = keyGenerator.generateKey().encoded
// filter out zero byte values, as SQLCipher does not like them
while (tempArray.contains(0)) {
tempArray = keyGenerator.generateKey().encoded
}
return tempArray
}
In this process we use SecureRandom that produce non-deterministic output and therefore any seed material passed to a SecureRandom object must be unpredictable this approach would be a good practice by reason of every user will have a different key. In Android 26 and higher can use getInstanceStrong to get stronger values.
All this Passphrases need to be stored because you need to use the same key every time to decrypt the data and a perfect option to store is secure files of the android security library.
fun getPassphraseFromSecureFile(): ByteArray {
val file = File(context.filesDir, fileName)
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val encryptedFile = EncryptedFile.Builder(
context,
file,
masterKey,
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) }
}
}
}
At this point the encryption is ready but in some cases yo need to migrate an non secure existing database to this new approach.
Migration
To make an easy migration, we can use the CipherHelper of this CWAC-SafeRoom library and use for encrypting a non secure database.
import android.content.Context
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import net.sqlcipher.database.SQLiteDatabase
object SQLCipherUtils {
/**
* Replaces this database with a version encrypted with the supplied
* passphrase, deleting the original. Do not call this while the database
* is open, which includes during any Room migrations.
*
* The passphrase is untouched in this call. If you are going to turn around
* and use it with SafeHelperFactory.fromUser(), fromUser() will clear the
* passphrase. If not, please set all bytes of the passphrase to 0 or something
* to clear out the passphrase.
*
* @param ctxt a Context
* @param originalFile a File pointing to the database
* @param passphrase the passphrase from the user
* @throws IOException
*/
@Throws(IOException::class)
fun encrypt(ctxt: Context, originalFile: File, passphrase: ByteArray?) {
SQLiteDatabase.loadLibs(ctxt)
if (originalFile.exists()) {
val newFile = File.createTempFile(
"sqlcipherutils", "tmp",
ctxt.cacheDir
)
var db = SQLiteDatabase.openDatabase(
originalFile.absolutePath,
"", null, SQLiteDatabase.OPEN_READWRITE
)
val version = db.version
db.close()
db = SQLiteDatabase.openDatabase(
newFile.absolutePath, passphrase,
null, SQLiteDatabase.OPEN_READWRITE, null, null
)
val st = db.compileStatement("ATTACH DATABASE ? AS plaintext KEY ''")
st.bindString(1, originalFile.absolutePath)
st.execute()
db.rawExecSQL("SELECT sqlcipher_export('main', 'plaintext')")
db.rawExecSQL("DETACH DATABASE plaintext")
db.version = version
st.close()
db.close()
originalFile.delete()
newFile.renameTo(originalFile)
} else {
throw FileNotFoundException(originalFile.absolutePath + " not found")
}
}
}
Then implement a migration method in the Room builder, this is an example and depends on your Room implementation that could be in a singleton or dependency injection module, etc…
val state = SQLCipherUtils.getDatabaseState(
context,
DATABASE_NAME
)
val key = getPassphraseFromSecureFile()
if (state == SQLCipherUtils.State.UNENCRYPTED) {
SQLCipherUtils.encrypt(
context,
DATABASE_NAME,
key
)
}return Room.databaseBuilder(context, DataBase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.openHelperFactory(KeyManagerRepository(context).getCypherFactory())
.build()
And that’s all for secure your Room DataBase.