Bases de datos seguras en Android

Publicado por Luis Salvador Roa Rodriguez el

AndroidRoom DatabaseSQLCipherMobile

En muchas ocasiones es importante proteger la información local de nuestras Apps. Al ser información sensible nuestro objetivo debe ser que, aunque un tercero pueda acceder a ella, no sea legible.

Una de las formas de conseguirlo es cifrando la base de datos. A continuación vamos a guiarte en un paso a paso para cumplir ese objetivo.

Qué necesitamos

  • Room Database Es la la base de datos oficial de Android, una capa de abstracción de SQLite.
  • SQLCipher Es una extensión de SQLite que posibilita cifrar la información.
  • Android Keystore Almacén de claves criptográficas protegido de Android.

Empecemos

Lo primero que tenemos que añadir son las dependencias de Gradle:

// Room
implementation "androidx.room:room-runtime:$room_version"  
kapt "androidx.room:room-compiler:$room_version"  
implementation "androidx.room:room-rxjava2:$room_version"

// SQLCipher
implementation "net.zetetic:android-database-sqlcipher:$sqlcipher_version"  
implementation "androidx.sqlite:sqlite-ktx:$sqlite_version"  

Ahora vamos con la implementación. En el inyector de dependencias (Koin en este caso) se especifica a Room que debe usar SQLCipher:

single {  
    Room.databaseBuilder(
        androidContext(),
        Database::class.java,
        BuildConfig.DB_NAME
    )
    .openHelperFactory(get<SupportFactory>())
    .build()
}

single {  
    // importado desde net.sqlcipher.database.SupportFactory
    SupportFactory(get<Passphrase>().getPassphrase())
}

single {  
    Passphrase(
        androidContext()
            .getSharedPreferences(
                "sharedPreferencesRoomSqlCypher",
                Context.MODE_PRIVATE
            )
    )
}

Passphrase es la clase que proporciona la clave para cifrar la base de datos.

Para conseguir una clave segura se utiliza un generador pseudoaleatorio y una KeyStore. Los pasos para la primera ejecución son:

  • Generar la clave pseudoaleatoria.
  • Proporcionar la clave a SQLCypher.
  • Crear la clave criptográfica de la KeyStore.
  • Cifrar la clave de la base de datos con la clave criptográfica de la KeyStore.
  • Guardar la clave cifrada, por ejemplo, en SharedPreferences.

En las siguientes ejecuciones lo que haremos será:

  • Recuperar la clave de la base de datos, que está almacenada cifrada.
  • Descifrar la clave usando la clave criptográfica de la KeyStore.
  • Proporcionar la clave a SQLCipher.

El contenido de Passphrase es el siguiente:

class Passphrase constructor(  
    private val sharedPreferences: SharedPreferences,
) {

    companion object {
        private const val ALIAS = "aliaskeystore"
        private const val TRANSFORMATION = "AES/CBC/NoPadding"
        private const val ANDROID_KEY_STORE = "AndroidKeyStore"
        private val CHARSET = Charsets.ISO_8859_1
        private const val IV_KEY = "ivkey"
        private const val DB_KEY = "dbkey"
    }

    private var passphrase: ByteArray? = null

    fun getPassphrase(): ByteArray {

        this.passphrase?.let {
            return it
        } ?: run {
            return if (sharedPreferences.contains(DB_KEY) && sharedPreferences.contains(IV_KEY)) {
                val encryptedData = sharedPreferences.getString(DB_KEY, null)
                val iv = Base64.decode(sharedPreferences.getString(IV_KEY, null), Base64.DEFAULT)
                decryptData(encryptedData!!.toByteArray(CHARSET), iv)
            } else {
                this.passphrase = Random.nextBytes(128)
                val encryptedData = encrypt(this.passphrase!!)
                sharedPreferences.edit().putString(DB_KEY, encryptedData.toString(CHARSET)).apply()
                return this.passphrase!!
            }
        }
    }

Las funciones de cifrado y descrifrado de la clave son:

    @Throws(
        UnrecoverableEntryException::class,
        NoSuchAlgorithmException::class,
        KeyStoreException::class,
        NoSuchProviderException::class,
        NoSuchPaddingException::class,
        InvalidKeyException::class,
        IOException::class,
        InvalidAlgorithmParameterException::class,
        SignatureException::class,
        BadPaddingException::class,
        IllegalBlockSizeException::class
    )
    private fun encrypt(byteArray: ByteArray): ByteArray {

        val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
        sharedPreferences.edit().remove(IV_KEY).apply()
        sharedPreferences.edit().putString(IV_KEY, Base64.encodeToString(cipher.iv, Base64.DEFAULT)).apply()
        return cipher.doFinal(byteArray)
    }

    @Throws(
        UnrecoverableEntryException::class,
        NoSuchAlgorithmException::class,
        KeyStoreException::class,
        NoSuchProviderException::class,
        NoSuchPaddingException::class,
        InvalidKeyException::class,
        IOException::class,
        BadPaddingException::class,
        IllegalBlockSizeException::class,
        InvalidAlgorithmParameterException::class
    )

    fun decryptData(encryptedData: ByteArray, encryptionIv: ByteArray): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        val spec = IvParameterSpec(encryptionIv)
        cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
        return cipher.doFinal(encryptedData)
    }

Para obtener la clave de la KeyStore:

    @NonNull
    @Throws(
        NoSuchAlgorithmException::class,
        NoSuchProviderException::class,
        InvalidAlgorithmParameterException::class
    )
    private fun getSecretKey(): SecretKey {

        val ks: KeyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply {
            load(null)
        }

        return if (ks.containsAlias(ALIAS)) {
            val secretKey = ks.getEntry(ALIAS, null) as KeyStore.SecretKeyEntry
            secretKey.secretKey
        } else {
            val keyGenerator: KeyGenerator = KeyGenerator
                .getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)

            keyGenerator.init(
                KeyGenParameterSpec.Builder(
                    ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                )
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build()
            )
            return keyGenerator.generateKey()
        }
    }
}

Conclusiones

Aplicando esta guía en nuestros desarrollos, tendremos la seguridad de que toda la información local de nuestra App estará cifrada y fuera del alcance de terceros.

Si te ha gustado este post, ¡síguenos en Twitter para estar al día de próximas entregas!