Merge pull request #227 from vector-im/feature/encrypt_local_data

Encrypt Realm databases
This commit is contained in:
Benoit Marty 2019-07-02 16:49:36 +02:00 committed by GitHub
commit 504d7e95fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 28 deletions

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotredesign.core.utils package im.vector.matrix.android.api.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build

View file

@ -23,8 +23,8 @@ import dagger.Provides
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.internal.auth.db.AuthRealmModule import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.di.AuthDatabase import im.vector.matrix.android.internal.di.AuthDatabase
import im.vector.matrix.android.internal.di.MatrixScope
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import java.io.File import java.io.File
@ -42,6 +42,7 @@ internal abstract class AuthModule {
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
} }
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.configureEncryption("matrix-sdk-auth", context)
.name("matrix-sdk-auth.realm") .name("matrix-sdk-auth.realm")
.modules(AuthRealmModule()) .modules(AuthRealmModule())
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
@ -50,7 +51,6 @@ internal abstract class AuthModule {
} }
@Binds @Binds
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore

View file

@ -29,12 +29,13 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import im.vector.matrix.android.internal.crypto.store.db.hash
import im.vector.matrix.android.internal.crypto.tasks.* import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import retrofit2.Retrofit import retrofit2.Retrofit
import java.io.File import java.io.File
@ -45,14 +46,16 @@ internal abstract class CryptoModule {
@Module @Module
companion object { companion object {
// Realm configuration, named to avoid clash with main cache realm configuration
@JvmStatic @JvmStatic
@Provides @Provides
@CryptoDatabase @CryptoDatabase
@SessionScope @SessionScope
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration { fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
val userIDHash = credentials.userId.md5()
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(File(context.filesDir, credentials.userId.hash())) .directory(File(context.filesDir, userIDHash))
.configureEncryption("crypto_module_$userIDHash", context)
.name("crypto_store.realm") .name("crypto_store.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
@ -169,6 +172,4 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask
} }

View file

@ -25,27 +25,9 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.security.MessageDigest
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
/**
* Compute a Hash of a String, using md5 algorithm
*/
fun String.hash() = try {
val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray())
val bytes = digest.digest()
val sb = StringBuilder()
for (i in bytes.indices) {
sb.append(String.format("%02X", bytes[i]))
}
sb.toString().toLowerCase()
} catch (exc: Exception) {
// Should not happen, but just in case
hashCode().toString()
}
/** /**
* Get realm, invoke the action, close realm, and return the result of the action * Get realm, invoke the action, close realm, and return the result of the action
*/ */

View file

@ -0,0 +1,106 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database
import android.content.Context
import android.util.Base64
import im.vector.matrix.android.api.util.SecretStoringUtils
import io.realm.RealmConfiguration
import timber.log.Timber
import java.security.SecureRandom
/**
* On creation a random key is generated, this key is then encrypted using the system KeyStore.
* The encrypted key is stored in shared preferences.
* When the database is opened again, the encrypted key is taken from the shared pref,
* then the Keystore is used to decrypt the key. The decrypted key is passed to the RealConfiguration.
*
* On android >=M, the KeyStore generates an AES key to encrypt/decrypt the database key,
* and the encrypted key is stored with the initialization vector in base64 in the shared pref.
* On android <M, the KeyStore cannot create AES keys, so a public/private key pair is generated,
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
*/
private object RealmKeysUtils {
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
private val rng = SecureRandom()
private fun generateKeyForRealm(): ByteArray {
val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH)
rng.nextBytes(keyForRealm)
return keyForRealm
}
/**
* Check if there is already a key for this alias
*/
fun hasKeyForDatabase(alias: String, context: Context): Boolean {
val sharedPreferences = getSharedPreferences(context)
return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias")
}
/**
* Creates a new secure random key for this database.
* The random key is then encrypted by the keystore, and the encrypted key is stored
* in shared preferences.
*
* @return the generate key (can be passed to Realm Configuration)
*/
fun createAndSaveKeyForDatabase(alias: String, context: Context): ByteArray {
val key = generateKeyForRealm()
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context)
val sharedPreferences = getSharedPreferences(context)
sharedPreferences
.edit()
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
.apply()
return key
}
/**
* Retrieves the key for this database
* throws if something goes wrong
*/
fun extractKeyForDatabase(alias: String, context: Context): ByteArray {
val sharedPreferences = getSharedPreferences(context)
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context)
return Base64.decode(b64!!, Base64.NO_PADDING)
}
private fun getSharedPreferences(context: Context) =
context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
}
fun RealmConfiguration.Builder.configureEncryption(alias: String, context: Context): RealmConfiguration.Builder {
if (RealmKeysUtils.hasKeyForDatabase(alias, context)) {
Timber.i("Found key for alias:$alias")
RealmKeysUtils.extractKeyForDatabase(alias, context).also {
this.encryptionKey(it)
}
} else {
Timber.i("Create key for DB alias:$alias")
RealmKeysUtils.createAndSaveKeyForDatabase(alias, context).also {
this.encryptionKey(it)
}
}
return this
}

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.Authenticated import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
@ -74,6 +75,7 @@ internal abstract class SessionModule {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
.name("disk_store.realm") .name("disk_store.realm")
.configureEncryption("session_db_$childPath", context)
.modules(SessionRealmModule()) .modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
.build() .build()

View file

@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.util
import java.security.MessageDigest import java.security.MessageDigest
/**
* Compute a Hash of a String, using md5 algorithm
*/
fun String.md5() = try { fun String.md5() = try {
val digest = MessageDigest.getInstance("md5") val digest = MessageDigest.getInstance("md5")
digest.update(toByteArray()) digest.update(toByteArray())

View file

@ -21,10 +21,10 @@ import android.graphics.Bitmap
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.Person import androidx.core.app.Person
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.util.SecretStoringUtils
import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ActiveSessionHolder import im.vector.riotredesign.core.di.ActiveSessionHolder
import im.vector.riotredesign.core.utils.SecretStoringUtils
import im.vector.riotredesign.features.settings.PreferencesManager import im.vector.riotredesign.features.settings.PreferencesManager
import me.gujun.android.span.span import me.gujun.android.span.span
import timber.log.Timber import timber.log.Timber