Merge pull request #3316 from vector-im/feature/bma/secretstoring_migration

Feature/bma/secretstoring migration
This commit is contained in:
Benoit Marty 2021-05-20 15:45:58 +02:00 committed by GitHub
commit 8d94b5548d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 335 additions and 86 deletions

View File

@ -59,6 +59,7 @@ Bugfix 🐛:
- Properly clean the back stack if the user cancel registration when waiting for email validation
- Fix read marker visibility/position when filtering some events
- Fix user invitation in case of restricted profile api (#3306)
- Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304)
SDK API changes ⚠️:
- RegistrationWizard.createAccount() parameters are now all optional, following Matrix spec (#3205)

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.MatrixModule
import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.di.NetworkModule
import org.matrix.android.sdk.internal.raw.RawModule
import org.matrix.android.sdk.internal.util.system.SystemModule
@Component(modules = [
TestModule::class,
@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.raw.RawModule
NetworkModule::class,
AuthModule::class,
RawModule::class,
SystemModule::class,
TestNetworkModule::class
])
@MatrixScope

View File

@ -0,0 +1,184 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.securestorage
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBeEqualTo
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import java.io.ByteArrayOutputStream
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class SecretStoringUtilsTest : InstrumentedTest {
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider)
companion object {
const val TEST_STR = "This is something I want to store safely!"
}
@Test
fun testStringNominalCaseApi21() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
// Decrypt
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testStringNominalCaseApi23() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Encrypt
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
// Decrypt
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testStringNominalCaseApi30() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
// Encrypt
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
// Decrypt
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testStringMigration21_23() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
// Simulate a system upgrade
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Decrypt
val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testObjectNominalCaseApi21() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
val encrypted = ByteArrayOutputStream().also { outputStream ->
outputStream.use {
secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
}
}
.toByteArray()
.toBase64NoPadding()
// Decrypt
val decrypted = encrypted.fromBase64().inputStream().use {
secretStoringUtils.loadSecureSecret<String>(it, alias)
}
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testObjectNominalCaseApi23() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Encrypt
val encrypted = ByteArrayOutputStream().also { outputStream ->
outputStream.use {
secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
}
}
.toByteArray()
.toBase64NoPadding()
// Decrypt
val decrypted = encrypted.fromBase64().inputStream().use {
secretStoringUtils.loadSecureSecret<String>(it, alias)
}
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testObjectNominalCaseApi30() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
// Encrypt
val encrypted = ByteArrayOutputStream().also { outputStream ->
outputStream.use {
secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
}
}
.toByteArray()
.toBase64NoPadding()
// Decrypt
val decrypted = encrypted.fromBase64().inputStream().use {
secretStoringUtils.loadSecureSecret<String>(it, alias)
}
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
@Test
fun testObjectMigration21_23() {
val alias = generateAlias()
buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
// Encrypt
val encrypted = ByteArrayOutputStream().also { outputStream ->
outputStream.use {
secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
}
}
.toByteArray()
.toBase64NoPadding()
// Simulate a system upgrade
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
// Decrypt
val decrypted = encrypted.fromBase64().inputStream().use {
secretStoringUtils.loadSecureSecret<String>(it, alias)
}
decrypted shouldBeEqualTo TEST_STR
secretStoringUtils.safeDeleteKey(alias)
}
private fun generateAlias() = UUID.randomUUID().toString()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.securestorage
import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
var value: Int = 0
override fun get() = value
}

View File

@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
sharedPreferences.edit {
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
}
return key
}
@ -84,7 +84,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
return Base64.decode(b64!!, Base64.NO_PADDING)
return Base64.decode(b64, Base64.NO_PADDING)
}
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {

View File

@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.TestInterceptor
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.system.SystemModule
import org.matrix.olm.OlmManager
import java.io.File
@ -44,6 +45,7 @@ import java.io.File
NetworkModule::class,
AuthModule::class,
RawModule::class,
SystemModule::class,
NoOpTestModule::class
])
@MatrixScope

View File

@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModul
import org.matrix.android.sdk.internal.session.widgets.WidgetModule
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.system.SystemModule
@Component(dependencies = [MatrixComponent::class],
modules = [
@ -80,6 +81,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
CacheModule::class,
MediaModule::class,
CryptoModule::class,
SystemModule::class,
PushersModule::class,
OpenIdModule::class,
WidgetModule::class,

View File

@ -18,12 +18,14 @@
package org.matrix.android.sdk.internal.session.securestorage
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@ -32,6 +34,7 @@ import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.OutputStream
import java.lang.IllegalArgumentException
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
@ -58,23 +61,19 @@ import javax.security.auth.x500.X500Principal
* is not available.
*
* <b>Android [K-M[</b>
* For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
* For android >=L and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
* random secret key in generated to perform encryption.
* This secret key is encrypted with the public RSA key and stored with the encrypted secret.
* In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
*
* <b>Older androids</b>
* For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
* The salt and iv are stored with encrypted data.
*
* Sample usage:
* <code>
* val secret = "The answer is 42"
* val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
* val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias")
* //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
*
* //to get back the secret, just call
* val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
* val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted, "myAlias")
* </code>
*
* You can also just use this utility to store a secret key, and use any encryption algorithm that you want.
@ -82,7 +81,10 @@ 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.
*/
internal class SecretStoringUtils @Inject constructor(private val context: Context) {
internal class SecretStoringUtils @Inject constructor(
private val context: Context,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider
) {
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
@ -91,7 +93,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
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 {
@ -113,43 +114,52 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
/**
* Encrypt the given secret using the android Keystore.
* On android >= M, will directly use the keystore to generate a symmetric key
* On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated
* On android >= Lollipop and <M, as symmetric key gen is not available, will use an symmetric key generated
* in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned
* in the bytearray (in can be stored anywhere, it is encrypted)
* On older version a key in generated from alias with random salt.
*
* The secret is encrypted using the following method: AES/GCM/NoPadding
*/
@SuppressLint("NewApi")
@Throws(Exception::class)
fun securelyStoreString(secret: String, keyAlias: String): ByteArray? {
fun securelyStoreString(secret: String, keyAlias: String): ByteArray {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
else -> encryptString(secret, keyAlias)
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
else -> encryptString(secret, keyAlias)
}
}
/**
* Decrypt a secret that was encrypted by #securelyStoreString()
*/
@SuppressLint("NewApi")
@Throws(Exception::class)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias)
else -> decryptString(encrypted, keyAlias)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String {
encrypted.inputStream().use { inputStream ->
// First get the format
return when (val format = inputStream.read().toByte()) {
FORMAT_API_M -> decryptStringM(inputStream, keyAlias)
FORMAT_1 -> decryptString(inputStream, keyAlias)
else -> throw IllegalArgumentException("Unknown format $format")
}
}
}
@SuppressLint("NewApi")
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
else -> saveSecureObject(keyAlias, output, any)
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
else -> saveSecureObject(keyAlias, output, any)
}
}
@SuppressLint("NewApi")
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream)
else -> loadSecureObject(keyAlias, inputStream)
// First get the format
return when (val format = inputStream.read().toByte()) {
FORMAT_API_M -> loadSecureObjectM(keyAlias, inputStream)
FORMAT_1 -> loadSecureObject(keyAlias, inputStream)
else -> throw IllegalArgumentException("Unknown format $format")
}
}
@ -180,7 +190,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
- Store the encrypted AES
Generate a key pair for encryption
*/
fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
private fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
if (privateKeyEntry != null) return privateKeyEntry
@ -193,7 +203,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
.setAlias(alias)
.setSubject(X500Principal("CN=$alias"))
.setSerialNumber(BigInteger.TEN)
// .setEncryptionRequired() requires that the phone as a pin/schema
// .setEncryptionRequired() requires that the phone has a pin/schema
.setStartDate(start.time)
.setEndDate(end.time)
.build()
@ -205,7 +215,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
}
@RequiresApi(Build.VERSION_CODES.M)
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
private fun encryptStringM(text: String, keyAlias: String): ByteArray {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
@ -217,8 +227,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
}
@RequiresApi(Build.VERSION_CODES.M)
private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(encryptedChunk.inputStream())
private fun decryptStringM(inputStream: InputStream, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(inputStream)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
@ -229,7 +239,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}
private fun encryptString(text: String, keyAlias: String): ByteArray? {
private fun encryptString(text: String, keyAlias: String): ByteArray {
// we generate a random symmetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
@ -246,8 +256,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
return format1Make(encryptedKey, iv, encryptedBytes)
}
private fun decryptString(data: ByteArray, keyAlias: String): String? {
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
private fun decryptString(inputStream: InputStream, keyAlias: String): String {
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
// we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
@ -307,30 +317,11 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
output.write(bos1.toByteArray())
}
// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
// FileOutputStream(file).use {
// saveSecureObjectM(keyAlias, it, writeObject)
// }
// }
//
// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? {
// FileInputStream(file).use {
// return loadSecureObjectM<T>(keyAlias, it)
// }
// }
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val format = inputStream.read()
assert(format.toByte() == FORMAT_API_M)
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
inputStream.read(iv, 0, ivSize)
@ -393,9 +384,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
}
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
val format = bis.read().toByte()
assert(format == FORMAT_API_M)
val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize)
@ -414,9 +402,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
}
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_1)
val keySizeBig = bis.read()
val keySizeLow = bis.read()
val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
@ -443,32 +428,4 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
return bos.toByteArray()
}
private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
bos.write(FORMAT_2.toInt())
bos.write(salt.size)
bos.write(salt)
bos.write(iv.size)
bos.write(iv)
bos.write(encryptedBytes)
return bos.toByteArray()
}
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_2)
val saltSize = bis.read()
val salt = ByteArray(saltSize)
bis.read(salt)
val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv)
val encrypted = bis.readBytes()
return Triple(salt, iv, encrypted)
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util.system
internal interface BuildVersionSdkIntProvider {
/**
* Return the current version of the Android SDK
*/
fun get(): Int
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util.system
import android.os.Build
import javax.inject.Inject
internal class DefaultBuildVersionSdkIntProvider @Inject constructor()
: BuildVersionSdkIntProvider {
override fun get() = Build.VERSION.SDK_INT
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.util.system
import dagger.Binds
import dagger.Module
@Module
internal abstract class SystemModule {
@Binds
abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
}