From 363f52b10c46421ea5d792ee5663d92e5205417f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 26 Jun 2019 14:59:58 +0200 Subject: [PATCH 1/3] Encrypt Realm databases --- .../android/api/util}/SecretStoringUtils.kt | 4 +- .../android/internal/auth/AuthModule.kt | 4 +- .../android/internal/crypto/CryptoModule.kt | 9 +- .../internal/database/RealmKeysUtils.kt | 96 +++++++++++++++++++ .../android/internal/session/SessionModule.kt | 2 + .../NotificationDrawerManager.kt | 2 +- 6 files changed, 108 insertions(+), 9 deletions(-) rename {vector/src/main/java/im/vector/riotredesign/core/utils => matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util}/SecretStoringUtils.kt (99%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt similarity index 99% rename from vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt index 1cbb28aeab..6dc0d789cc 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotredesign.core.utils +package im.vector.matrix.android.api.util import android.content.Context import android.os.Build diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 472d653dee..2459c3546a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -23,8 +23,8 @@ import dagger.Provides 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.RealmSessionParamsStore +import im.vector.matrix.android.internal.database.configureEncryption import im.vector.matrix.android.internal.di.AuthDatabase -import im.vector.matrix.android.internal.di.MatrixScope import io.realm.RealmConfiguration import java.io.File @@ -42,6 +42,7 @@ internal abstract class AuthModule { old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm")) } return RealmConfiguration.Builder() + .configureEncryption("matrix-sdk-auth", context) .name("matrix-sdk-auth.realm") .modules(AuthRealmModule()) .deleteRealmIfMigrationNeeded() @@ -50,7 +51,6 @@ internal abstract class AuthModule { } - @Binds abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 89baf1c6b1..7b1bc00b47 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigrati 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.database.configureEncryption import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.cache.ClearCacheTask @@ -45,14 +46,16 @@ internal abstract class CryptoModule { @Module companion object { - // Realm configuration, named to avoid clash with main cache realm configuration @JvmStatic @Provides @CryptoDatabase @SessionScope fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration { + val userIDHash = credentials.userId.hash() + return RealmConfiguration.Builder() - .directory(File(context.filesDir, credentials.userId.hash())) + .directory(File(context.filesDir, userIDHash)) + .configureEncryption("crypto_module_$userIDHash", context) .name("crypto_store.realm") .modules(RealmCryptoStoreModule()) .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) @@ -169,6 +172,4 @@ internal abstract class CryptoModule { @Binds abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask - - } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt new file mode 100644 index 0000000000..bdfa442916 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -0,0 +1,96 @@ +/* + * 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 + + +object RealmKeysUtils { + + + private val ENCRYPTED_KEY_PREFIX = "REALM_ENCR_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 encrytpedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) + val encryptedKey = Base64.decode(encrytpedB64, 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.riotx-sdk", 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 +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 10d4abc971..1a3abc409c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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.session.Session 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.di.Authenticated import im.vector.matrix.android.internal.di.SessionDatabase @@ -74,6 +75,7 @@ internal abstract class SessionModule { return RealmConfiguration.Builder() .directory(directory) .name("disk_store.realm") + .configureEncryption("session_db_$childPath", context) .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() .build() diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt index 8c81570b7e..a66d6395cf 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationDrawerManager.kt @@ -21,10 +21,10 @@ import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person 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.R import im.vector.riotredesign.core.di.ActiveSessionHolder -import im.vector.riotredesign.core.utils.SecretStoringUtils import im.vector.riotredesign.features.settings.PreferencesManager import me.gujun.android.span.span import timber.log.Timber From bc1edcf33dde763b67e32bfcbc2669215fe71f9d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Jun 2019 11:14:18 +0200 Subject: [PATCH 2/3] Code review --- .../internal/database/RealmKeysUtils.kt | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index bdfa442916..ee8ee41821 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -22,11 +22,21 @@ 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 Date: Thu, 27 Jun 2019 11:16:14 +0200 Subject: [PATCH 3/3] Remove duplicated method --- .../android/internal/crypto/CryptoModule.kt | 4 ++-- .../android/internal/crypto/store/db/Helper.kt | 18 ------------------ .../matrix/android/internal/util/Hash.kt | 3 +++ 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 7b1bc00b47..32a5b3d271 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -29,13 +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.RealmCryptoStoreMigration 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.database.configureEncryption import im.vector.matrix.android.internal.di.CryptoDatabase 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.RealmClearCacheTask +import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import retrofit2.Retrofit import java.io.File @@ -51,7 +51,7 @@ internal abstract class CryptoModule { @CryptoDatabase @SessionScope fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration { - val userIDHash = credentials.userId.hash() + val userIDHash = credentials.userId.md5() return RealmConfiguration.Builder() .directory(File(context.filesDir, userIDHash)) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt index cbf941f1c1..4ba484ae1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -25,27 +25,9 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream -import java.security.MessageDigest 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 */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt index b97de018f8..e57c289388 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Hash.kt @@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.util import java.security.MessageDigest +/** + * Compute a Hash of a String, using md5 algorithm + */ fun String.md5() = try { val digest = MessageDigest.getInstance("md5") digest.update(toByteArray())