diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 2dde175bed..53dc8e77a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -50,7 +51,8 @@ interface Session : FileService, PushRuleService, PushersService, - InitialSyncProgressService { + InitialSyncProgressService, + SecureStorageService { /** * The params associated to the session @@ -87,7 +89,7 @@ interface Session : /** * This method start the sync thread. */ - fun startSync(fromForeground : Boolean) + fun startSync(fromForeground: Boolean) /** * This method stop the sync thread. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt new file mode 100644 index 0000000000..d56b6150ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt @@ -0,0 +1,28 @@ +/* + * 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.api.session.securestorage + +import java.io.InputStream +import java.io.OutputStream + +interface SecureStorageService { + + fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) + + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt index c411760f38..5d45160b03 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt @@ -35,6 +35,7 @@ import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject import javax.security.auth.x500.X500Principal @@ -72,15 +73,17 @@ import javax.security.auth.x500.X500Principal * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you * add a pin or change the schema); So you might and with a useless pile of bytes. */ -object SecretStoringUtils { +internal class SecretStoringUtils @Inject constructor(private val context: Context) { - private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val AES_MODE = "AES/GCM/NoPadding"; - private const val RSA_MODE = "RSA/ECB/PKCS1Padding" + companion object { + private const val ANDROID_KEY_STORE = "AndroidKeyStore" + private const val AES_MODE = "AES/GCM/NoPadding" + private const val RSA_MODE = "RSA/ECB/PKCS1Padding" - private const val FORMAT_API_M: Byte = 0 - private const val FORMAT_1: Byte = 1 - private const val FORMAT_2: Byte = 2 + private const val FORMAT_API_M: Byte = 0 + private const val FORMAT_1: Byte = 1 + private const val FORMAT_2: Byte = 2 + } private val keyStore: KeyStore by lazy { KeyStore.getInstance(ANDROID_KEY_STORE).apply { @@ -109,13 +112,11 @@ object SecretStoringUtils { * The secret is encrypted using the following method: AES/GCM/NoPadding */ @Throws(Exception::class) - fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return encryptStringM(secret, keyAlias) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return encryptStringK(secret, keyAlias, context) - } else { - return encryptForOldDevicesNotGood(secret, keyAlias) + fun securelyStoreString(secret: String, keyAlias: String): ByteArray? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias) + else -> encryptForOldDevicesNotGood(secret, keyAlias) } } @@ -123,33 +124,27 @@ object SecretStoringUtils { * Decrypt a secret that was encrypted by #securelyStoreString() */ @Throws(Exception::class) - fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return decryptStringM(encrypted, keyAlias) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return decryptStringK(encrypted, keyAlias, context) - } else { - return decryptForOldDevicesNotGood(encrypted, keyAlias) + fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias) + else -> decryptForOldDevicesNotGood(encrypted, keyAlias) } } - fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - saveSecureObjectM(keyAlias, output, any) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return saveSecureObjectK(keyAlias, output, any, context) - } else { - return saveSecureObjectOldNotGood(keyAlias, output, any) + fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any) + else -> saveSecureObjectOldNotGood(keyAlias, output, any) } } - fun loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return loadSecureObjectM(keyAlias, inputStream) - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return loadSecureObjectK(keyAlias, inputStream, context) - } else { - return loadSecureObjectOldNotGood(keyAlias, inputStream) + fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream) + else -> loadSecureObjectOldNotGood(keyAlias, inputStream) } } @@ -182,7 +177,7 @@ object SecretStoringUtils { Generate a key pair for encryption */ @RequiresApi(Build.VERSION_CODES.KITKAT) - fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry { + fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry { val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry) if (privateKeyEntry != null) return privateKeyEntry @@ -234,14 +229,14 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun encryptStringK(text: String, keyAlias: String, context: Context): ByteArray? { + private fun encryptStringK(text: String, keyAlias: String): ByteArray? { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) val sKey = SecretKeySpec(key, "AES") //we encrypt this key thanks to the key store - val encryptedKey = rsaEncrypt(keyAlias, key, context) + val encryptedKey = rsaEncrypt(keyAlias, key) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, sKey) @@ -286,12 +281,12 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun decryptStringK(data: ByteArray, keyAlias: String, context: Context): String? { + private fun decryptStringK(data: ByteArray, keyAlias: String): String? { val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data)) //we need to decrypt the key - val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) val cipher = Cipher.getInstance(AES_MODE) val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) @@ -321,14 +316,14 @@ object SecretStoringUtils { } @RequiresApi(Build.VERSION_CODES.KITKAT) - private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) { + private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) { //we generate a random symetric key val key = ByteArray(16) secureRandom.nextBytes(key) val sKey = SecretKeySpec(key, "AES") //we encrypt this key thanks to the key store - val encryptedKey = rsaEncrypt(keyAlias, key, context) + val encryptedKey = rsaEncrypt(keyAlias, key) val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, sKey) @@ -418,12 +413,12 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(IOException::class) - private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { + private fun loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? { val (encryptedKey, iv, encrypted) = format1Extract(inputStream) //we need to decrypt the key - val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context) + val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey)) val cipher = Cipher.getInstance(AES_MODE) val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) @@ -464,8 +459,8 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) - private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray { - val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) // Encrypt the text val inputCipher = Cipher.getInstance(RSA_MODE) inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey) @@ -480,8 +475,8 @@ object SecretStoringUtils { @RequiresApi(Build.VERSION_CODES.KITKAT) @Throws(Exception::class) - private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray { - val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context) + private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray { + val privateKeyEntry = getOrGenerateKeyPairForAlias(alias) val output = Cipher.getInstance(RSA_MODE) output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey) 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 60d5e8a3ae..c7b4e67883 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 @@ -36,7 +36,8 @@ import javax.inject.Inject * 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 */ -internal class RealmKeysUtils @Inject constructor(private val context: Context) { +internal class RealmKeysUtils @Inject constructor(context: Context, + private val secretStoringUtils: SecretStoringUtils) { private val rng = SecureRandom() @@ -65,7 +66,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) private fun createAndSaveKeyForDatabase(alias: String): ByteArray { val key = generateKeyForRealm() val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) - val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context) + val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) sharedPreferences .edit() .putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) @@ -80,7 +81,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) private fun extractKeyForDatabase(alias: String): ByteArray { val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null) val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING) - val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context) + val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias) return Base64.decode(b64!!, Base64.NO_PADDING) } @@ -104,7 +105,7 @@ internal class RealmKeysUtils @Inject constructor(private val context: Context) // Delete elements related to the alias fun clear(alias: String) { if (hasKeyForDatabase(alias)) { - SecretStoringUtils.safeDeleteKey(alias) + secretStoringUtils.safeDeleteKey(alias) sharedPreferences .edit() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 8ee73084e7..02addaceab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.SyncState @@ -63,6 +64,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se private val pushersService: Lazy, private val cryptoService: Lazy, private val fileService: Lazy, + private val secureStorageService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val contentUploadProgressTracker: ContentUploadStateTracker, @@ -78,7 +80,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se PushRuleService by pushRuleService.get(), PushersService by pushersService.get(), FileService by fileService.get(), - InitialSyncProgressService by initialSyncProgressService.get() { + InitialSyncProgressService by initialSyncProgressService.get(), + SecureStorageService by secureStorageService.get() { private var isOpen = false 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 f8f94f0321..180cdb6ea2 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.InitialSyncProgressService import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.model.SessionRealmModule @@ -39,6 +40,7 @@ import im.vector.matrix.android.internal.session.room.EventRelationsAggregationU import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver +import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService import im.vector.matrix.android.internal.util.md5 import io.realm.RealmConfiguration import okhttp3.OkHttpClient @@ -166,4 +168,7 @@ internal abstract class SessionModule { @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService + @Binds + abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt new file mode 100644 index 0000000000..bd052f962c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt @@ -0,0 +1,36 @@ +/* + * 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.session.securestorage + +import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.api.util.SecretStoringUtils +import im.vector.matrix.android.internal.di.UserMd5 +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +internal class DefaultSecureStorageService @Inject constructor(@UserMd5 private val userMd5: String, + private val secretStoringUtils: SecretStoringUtils) : SecureStorageService { + + override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) { + secretStoringUtils.securelyStoreObject(any, "${userMd5}_$keyAlias", outputStream) + } + + override fun loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return secretStoringUtils.loadSecureSecret(inputStream, "${userMd5}_$keyAlias") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 6695c10955..01fa3210bd 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -24,7 +24,6 @@ import androidx.annotation.WorkerThread 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.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder @@ -448,7 +447,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (!file.exists()) file.createNewFile() FileOutputStream(file).use { - SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context) + activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it) } } catch (e: Throwable) { Timber.e(e, "## Failed to save cached notification info") @@ -461,7 +460,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME) if (file.exists()) { FileInputStream(file).use { - val events: ArrayList? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context) + val events: ArrayList? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE) if (events != null) { return ArrayList(events.mapNotNull { it as? NotifiableEvent }) } @@ -486,5 +485,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private const val ROOM_EVENT_NOTIFICATION_ID = 2 private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache" + + private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr" } }