diff --git a/.gitignore b/.gitignore index ff086d7723..8313fb5c63 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ /fastlane/private /fastlane/report.xml -/library/build +/**/build diff --git a/build.gradle b/build.gradle index 2cb67b7795..eabe7bb5d2 100644 --- a/build.gradle +++ b/build.gradle @@ -252,11 +252,7 @@ dependencyAnalysis { exclude("org.json:json") // Used in unit tests, overwrites the one bundled into Android } } - project(":library:ui-styles") { - onUnusedDependencies { - exclude("com.github.vector-im:PFLockScreen-Android") // False positive - } - } + project(":library:ui-styles") project(":matrix-sdk-android") { onUnusedDependencies { exclude("io.reactivex.rxjava2:rxkotlin") // Transitively required for mocking realm as monarchy doesn't expose Rx diff --git a/changelog.d/6217.feature b/changelog.d/6217.feature new file mode 100644 index 0000000000..6a8a31790f --- /dev/null +++ b/changelog.d/6217.feature @@ -0,0 +1 @@ +Improve lock screen implementation. diff --git a/coverage.gradle b/coverage.gradle index fc69ce7e90..1deda1b8d9 100644 --- a/coverage.gradle +++ b/coverage.gradle @@ -24,11 +24,13 @@ def excludes = [ def initializeReport(report, projects, classExcludes) { projects.each { project -> project.apply plugin: 'jacoco' } - report.executionData { fileTree(rootProject.rootDir.absolutePath).include( - "**/build/outputs/unit_test_code_coverage/**/*.exec", - "**/build/outputs/code_coverage/**/coverage.ec" - ) } + report.executionData { + fileTree(rootProject.rootDir.absolutePath).include( + "**/build/outputs/unit_test_code_coverage/**/*.exec", + "**/build/outputs/code_coverage/**/coverage.ec", + ) + } report.reports { xml.enabled true html.enabled true diff --git a/dependencies.gradle b/dependencies.gradle index 962f07f21f..43a774db0a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -28,13 +28,13 @@ def bigImageViewer = "1.8.1" def jjwt = "0.11.5" def vanniktechEmoji = "0.15.0" +def fragment = "1.4.1" + // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" def androidxTest = "1.4.0" def androidxOrchestrator = "1.4.1" - - ext.libs = [ gradle : [ 'gradlePlugin' : "com.android.tools.build:gradle:$gradle", @@ -50,11 +50,14 @@ ext.libs = [ androidx : [ 'annotation' : "androidx.annotation:annotation:1.3.0", 'activity' : "androidx.activity:activity:1.4.0", + 'annotations' : "androidx.annotation:annotation:1.3.0", 'appCompat' : "androidx.appcompat:appcompat:1.4.2", + 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.8.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", - 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.1", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", + 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", 'work' : "androidx.work:work-runtime-ktx:2.7.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", @@ -85,6 +88,7 @@ ext.libs = [ 'dagger' : "com.google.dagger:dagger:$dagger", 'daggerCompiler' : "com.google.dagger:dagger-compiler:$dagger", 'hilt' : "com.google.dagger:hilt-android:$dagger", + 'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger", 'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger" ], squareup : [ @@ -155,3 +159,5 @@ ext.libs = [ 'junit' : "junit:junit:4.13.2" ] ] + + diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 31cfdd24c7..eabd0f36f6 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -56,8 +56,6 @@ dependencies { implementation libs.google.material // Pref theme implementation libs.androidx.preferenceKtx - // PFLockScreen attrs - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // dialpad dimen implementation 'im.dlg:android-dialer:1.2.5' } diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_background.xml new file mode 100644 index 0000000000..5688c433f7 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_background.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml new file mode 100644 index 0000000000..87fa99063c --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_background.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml new file mode 100644 index 0000000000..abde6087e0 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_empty.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml new file mode 100644 index 0000000000..e3f1082324 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_code_fill.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml new file mode 100644 index 0000000000..3fdebfbbe0 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_circle_key_selector.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml new file mode 100644 index 0000000000..5de4957a3b --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_code_selector.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml b/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml new file mode 100644 index 0000000000..e1d70e8f41 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_delete.xml @@ -0,0 +1,7 @@ + + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml b/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml new file mode 100644 index 0000000000..7f0abe850a --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_fingerprint.xml @@ -0,0 +1,5 @@ + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml b/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml new file mode 100644 index 0000000000..b205b2d91c --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_side_button_background.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml b/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml new file mode 100644 index 0000000000..141f2ac698 --- /dev/null +++ b/library/ui-styles/src/main/res/drawable/lockscreen_touch_selector.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml b/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml new file mode 100644 index 0000000000..2ae3ca0689 --- /dev/null +++ b/library/ui-styles/src/main/res/values-land/lockscreen_default_dimen.xml @@ -0,0 +1,5 @@ + + + 60dp + 15dp + diff --git a/library/ui-styles/src/main/res/values/lockscreen_attr.xml b/library/ui-styles/src/main/res/values/lockscreen_attr.xml new file mode 100644 index 0000000000..64e77d3c4e --- /dev/null +++ b/library/ui-styles/src/main/res/values/lockscreen_attr.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml b/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml new file mode 100644 index 0000000000..eb9115d636 --- /dev/null +++ b/library/ui-styles/src/main/res/values/lockscreen_default_colors.xml @@ -0,0 +1,8 @@ + + + #ffffff + #66ffffff + #42000000 + #f4511e + #009688 + diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml b/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml new file mode 100644 index 0000000000..7d30f179a6 --- /dev/null +++ b/library/ui-styles/src/main/res/values/lockscreen_default_dimens.xml @@ -0,0 +1,7 @@ + + + 70dp + 25dp + 10dp + 5dp + diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml b/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml new file mode 100644 index 0000000000..f0d7a75851 --- /dev/null +++ b/library/ui-styles/src/main/res/values/lockscreen_default_strings.xml @@ -0,0 +1,17 @@ + + Cancel + Use pin + Sign in + Next + Forgot? + Input pin code or use biometric authentication + Fingerprint not recognized. Try again + Fingerprint recognized + + Confirm fingerprint to continue + Touch sensor + Fingerprint icon + Confirm PIN + Logo + + diff --git a/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml b/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml new file mode 100644 index 0000000000..dba92df0bb --- /dev/null +++ b/library/ui-styles/src/main/res/values/lockscreen_default_styles.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/styles_pin_code.xml b/library/ui-styles/src/main/res/values/styles_pin_code.xml index cb22863694..8459778e29 100644 --- a/library/ui-styles/src/main/res/values/styles_pin_code.xml +++ b/library/ui-styles/src/main/res/values/styles_pin_code.xml @@ -22,13 +22,13 @@ diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index 733f7e8eb5..f86a05ed66 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -111,14 +111,14 @@ @style/PreferenceThemeOverlay.v14.Material - @style/PinCodeScreenStyle - @style/PinCodeKeyButtonStyle - @style/PinCodeTitleStyle - @style/PinCodeHintStyle - @style/PinCodeDotsViewStyle - @style/PinCodeDeleteButtonStyle - @style/PinCodeFingerprintButtonStyle - @style/PinCodeNextButtonStyle + @style/PinCodeScreenStyle + @style/PinCodeKeyButtonStyle + @style/PinCodeTitleStyle + @style/PinCodeHintStyle + @style/PinCodeDotsViewStyle + @style/PinCodeDeleteButtonStyle + @style/PinCodeFingerprintButtonStyle + @style/PinCodeNextButtonStyle @color/android_status_bar_background_dark @color/android_navigation_bar_background_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 77996c8ce5..173b502dcd 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -111,14 +111,14 @@ @style/PreferenceThemeOverlay.v14.Material - @style/PinCodeScreenStyle - @style/PinCodeKeyButtonStyle - @style/PinCodeTitleStyle - @style/PinCodeHintStyle - @style/PinCodeDotsViewStyle - @style/PinCodeDeleteButtonStyle - @style/PinCodeFingerprintButtonStyle - @style/PinCodeNextButtonStyle + @style/PinCodeScreenStyle + @style/PinCodeKeyButtonStyle + @style/PinCodeTitleStyle + @style/PinCodeHintStyle + @style/PinCodeDotsViewStyle + @style/PinCodeDeleteButtonStyle + @style/PinCodeFingerprintButtonStyle + @style/PinCodeNextButtonStyle @color/android_status_bar_background_dark diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt similarity index 78% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt index b08c88fb24..d0d64491ef 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * Copyright 2022 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. @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.securestorage +package org.matrix.android.sdk -import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { var value: Int = 0 diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt similarity index 62% rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt index 6bcd12742b..14f985243c 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt @@ -14,40 +14,57 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.securestorage +package org.matrix.android.sdk.api.securestorage import android.os.Build +import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.spyk +import org.amshove.kluent.invoking import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeInstanceOf +import org.amshove.kluent.shouldNotThrow +import org.amshove.kluent.shouldThrow +import org.junit.Before 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.api.util.fromBase64 -import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.TestBuildVersionSdkIntProvider import java.io.ByteArrayOutputStream +import java.security.KeyStore +import java.security.KeyStoreException import java.util.UUID @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) -class SecretStoringUtilsTest : InstrumentedTest { +class SecretStoringUtilsTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() - private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider) + private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) } + private val secretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) companion object { const val TEST_STR = "This is something I want to store safely!" } + @Before + fun setup() { + clearAllMocks() + } + @Test fun testStringNominalCaseApi21() { val alias = generateAlias() buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP // Encrypt - val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias) // Decrypt - val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias)) decrypted shouldBeEqualTo TEST_STR secretStoringUtils.safeDeleteKey(alias) } @@ -57,9 +74,9 @@ class SecretStoringUtilsTest : InstrumentedTest { val alias = generateAlias() buildVersionSdkIntProvider.value = Build.VERSION_CODES.M // Encrypt - val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias) // Decrypt - val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias)) decrypted shouldBeEqualTo TEST_STR secretStoringUtils.safeDeleteKey(alias) } @@ -69,9 +86,9 @@ class SecretStoringUtilsTest : InstrumentedTest { val alias = generateAlias() buildVersionSdkIntProvider.value = Build.VERSION_CODES.R // Encrypt - val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias) // Decrypt - val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias)) decrypted shouldBeEqualTo TEST_STR secretStoringUtils.safeDeleteKey(alias) } @@ -81,13 +98,13 @@ class SecretStoringUtilsTest : InstrumentedTest { val alias = generateAlias() buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP // Encrypt - val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias) + val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias) // Simulate a system upgrade buildVersionSdkIntProvider.value = Build.VERSION_CODES.M // Decrypt - val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias) + val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias)) decrypted shouldBeEqualTo TEST_STR secretStoringUtils.safeDeleteKey(alias) } @@ -180,5 +197,56 @@ class SecretStoringUtilsTest : InstrumentedTest { secretStoringUtils.safeDeleteKey(alias) } + @Test + fun testEnsureKeyReturnsSymmetricKeyOnAndroidM() { + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + val alias = generateAlias() + + val key = secretStoringUtils.ensureKey(alias) + key shouldBeInstanceOf KeyStore.SecretKeyEntry::class + + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testEnsureKeyReturnsPrivateKeyOnAndroidL() { + buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP + val alias = generateAlias() + + val key = secretStoringUtils.ensureKey(alias) + key shouldBeInstanceOf KeyStore.PrivateKeyEntry::class + + secretStoringUtils.safeDeleteKey(alias) + } + + @Test + fun testSafeDeleteCanHandleKeyStoreExceptions() { + every { keyStore.deleteEntry(any()) } throws KeyStoreException() + + invoking { secretStoringUtils.safeDeleteKey(generateAlias()) } shouldNotThrow KeyStoreException::class + } + + @Test + fun testLoadSecureSecretBytesWillThrowOnInvalidStreamFormat() { + invoking { + secretStoringUtils.loadSecureSecretBytes(byteArrayOf(255.toByte()), generateAlias()) + } shouldThrow IllegalArgumentException::class + } + + @Test + fun testLoadSecureSecretWillThrowOnInvalidStreamFormat() { + invoking { + secretStoringUtils.loadSecureSecret(byteArrayOf(255.toByte()).inputStream(), generateAlias()) + } shouldThrow IllegalArgumentException::class + } + private fun generateAlias() = UUID.randomUUID().toString() } + +private fun ByteArray.toBase64NoPadding(): String { + return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) +} + +private fun String.fromBase64(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt index 52beb1b484..6cf01d4ae2 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt @@ -20,6 +20,7 @@ import android.content.Context import dagger.BindsInstance import dagger.Component import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.securestorage.SecureStorageModule import org.matrix.android.sdk.internal.auth.AuthModule import org.matrix.android.sdk.internal.debug.DebugModule import org.matrix.android.sdk.internal.di.MatrixComponent @@ -39,7 +40,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule RawModule::class, DebugModule::class, SettingsModule::class, - SystemModule::class + SystemModule::class, + SecureStorageModule::class, ] ) @MatrixScope @@ -51,7 +53,7 @@ internal interface TestMatrixComponent : MatrixComponent { interface Factory { fun create( @BindsInstance context: Context, - @BindsInstance matrixConfiguration: MatrixConfiguration + @BindsInstance matrixConfiguration: MatrixConfiguration, ): TestMatrixComponent } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 55569580a4..953ebddcbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.api import android.content.Context +import android.os.Handler +import android.os.Looper import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration import androidx.work.WorkManager @@ -30,6 +32,7 @@ import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.network.ApiInterceptorListener import org.matrix.android.sdk.api.network.ApiPath import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.securestorage.SecureStorageService import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.di.DaggerMatrixComponent @@ -64,6 +67,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { @Inject internal lateinit var apiInterceptor: ApiInterceptor @Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory @Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage + @Inject internal lateinit var secureStorageService: SecureStorageService + + private val uiHandler = Handler(Looper.getMainLooper()) init { val appContext = context.applicationContext @@ -76,7 +82,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { .build() WorkManager.initialize(appContext, configuration) } - ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + uiHandler.post { + ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) + } } /** @@ -115,6 +123,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) { */ fun legacySessionImporter() = legacySessionImporter + /** + * Returns the SecureStorageService used to encrypt and decrypt sensitive data. + */ + fun secureStorageService(): SecureStorageService = secureStorageService + /** * Get the worker factory. The returned value has to be provided to `WorkConfiguration.Builder()`. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt similarity index 82% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt index 8b35bd173e..bd2a1078b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt @@ -16,7 +16,7 @@ @file:Suppress("DEPRECATION") -package org.matrix.android.sdk.internal.session.securestorage +package org.matrix.android.sdk.api.securestorage import android.annotation.SuppressLint import android.content.Context @@ -25,7 +25,7 @@ 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 org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -80,9 +80,11 @@ 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( +class SecretStoringUtils @Inject constructor( private val context: Context, - private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider + private val keyStore: KeyStore, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, + private val keyNeedsUserAuthentication: Boolean = false, ) { companion object { @@ -94,14 +96,24 @@ internal class SecretStoringUtils @Inject constructor( private const val FORMAT_1: Byte = 1 } - private val keyStore: KeyStore by lazy { - KeyStore.getInstance(ANDROID_KEY_STORE).apply { - load(null) - } - } - private val secureRandom = SecureRandom() + /** + * Allows creation of the crypto keys associated witht he [alias] before encrypting some value with it. + * @return A [KeyStore.Entry] with the keys. + */ + @SuppressLint("NewApi") + fun ensureKey(alias: String): KeyStore.Entry { + when { + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> getOrGenerateSymmetricKeyForAliasM(alias) + else -> getOrGenerateKeyPairForAlias(alias).privateKey + } + return keyStore.getEntry(alias, null) + } + + /** + * Deletes the key associated with the [keyAlias] and logs any [KeyStoreException] that could happen. + */ fun safeDeleteKey(keyAlias: String) { try { keyStore.deleteEntry(keyAlias) @@ -121,24 +133,24 @@ internal class SecretStoringUtils @Inject constructor( */ @SuppressLint("NewApi") @Throws(Exception::class) - fun securelyStoreString(secret: String, keyAlias: String): ByteArray { + fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray { return when { - buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias) - else -> encryptString(secret, keyAlias) + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias) + else -> encryptBytes(secret, keyAlias) } } /** - * Decrypt a secret that was encrypted by #securelyStoreString(). + * Decrypt a secret that was encrypted by [securelyStoreBytes]. */ @SuppressLint("NewApi") @Throws(Exception::class) - fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String { + fun loadSecureSecretBytes(encrypted: ByteArray, keyAlias: String): ByteArray { 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) + FORMAT_API_M -> decryptBytesM(inputStream, keyAlias) + FORMAT_1 -> decryptBytes(inputStream, keyAlias) else -> throw IllegalArgumentException("Unknown format $format") } } @@ -162,6 +174,22 @@ internal class SecretStoringUtils @Inject constructor( } } + fun getEncryptCipher(alias: String): Cipher { + val key = when (val keyEntry = ensureKey(alias)) { + is KeyStore.SecretKeyEntry -> keyEntry.secretKey + is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey + else -> throw IllegalStateException("Unknown KeyEntry type.") + } + val cipherMode = when { + buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE + else -> RSA_MODE + } + val cipher = Cipher.getInstance(cipherMode) + cipher.init(Cipher.ENCRYPT_MODE, key) + return cipher + } + + @SuppressLint("NewApi") @RequiresApi(Build.VERSION_CODES.M) private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey { val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) @@ -176,6 +204,13 @@ internal class SecretStoringUtils @Inject constructor( .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(128) + .apply { + setUserAuthenticationRequired(keyNeedsUserAuthentication) + if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) { + setInvalidatedByBiometricEnrollment(true) + } + } + .setUserAuthenticationRequired(keyNeedsUserAuthentication) .build() generator.init(keyGenSpec) return generator.generateKey() @@ -216,19 +251,16 @@ internal class SecretStoringUtils @Inject constructor( } @RequiresApi(Build.VERSION_CODES.M) - private fun encryptStringM(text: String, keyAlias: String): ByteArray { - val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) - - val cipher = Cipher.getInstance(AES_MODE) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) + private fun encryptBytesM(byteArray: ByteArray, keyAlias: String): ByteArray { + val cipher = getEncryptCipher(keyAlias) val iv = cipher.iv // we happen the iv to the final result - val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + val encryptedBytes: ByteArray = cipher.doFinal(byteArray) return formatMMake(iv, encryptedBytes) } @RequiresApi(Build.VERSION_CODES.M) - private fun decryptStringM(inputStream: InputStream, keyAlias: String): String { + private fun decryptBytesM(inputStream: InputStream, keyAlias: String): ByteArray { val (iv, encryptedText) = formatMExtract(inputStream) val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) @@ -237,10 +269,10 @@ internal class SecretStoringUtils @Inject constructor( val spec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) - return String(cipher.doFinal(encryptedText), Charsets.UTF_8) + return cipher.doFinal(encryptedText) } - private fun encryptString(text: String, keyAlias: String): ByteArray { + private fun encryptBytes(byteArray: ByteArray, keyAlias: String): ByteArray { // we generate a random symmetric key val key = ByteArray(16) secureRandom.nextBytes(key) @@ -252,12 +284,12 @@ internal class SecretStoringUtils @Inject constructor( val cipher = Cipher.getInstance(AES_MODE) cipher.init(Cipher.ENCRYPT_MODE, sKey) val iv = cipher.iv - val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8)) + val encryptedBytes: ByteArray = cipher.doFinal(byteArray) return format1Make(encryptedKey, iv, encryptedBytes) } - private fun decryptString(inputStream: InputStream, keyAlias: String): String { + private fun decryptBytes(inputStream: InputStream, keyAlias: String): ByteArray { val (encryptedKey, iv, encrypted) = format1Extract(inputStream) // we need to decrypt the key @@ -266,16 +298,13 @@ internal class SecretStoringUtils @Inject constructor( val spec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec) - return String(cipher.doFinal(encrypted), Charsets.UTF_8) + return cipher.doFinal(encrypted) } @RequiresApi(Build.VERSION_CODES.M) @Throws(IOException::class) private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) { - val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) - - val cipher = Cipher.getInstance(AES_MODE) - cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/) + val cipher = getEncryptCipher(keyAlias) val iv = cipher.iv val bos1 = ByteArrayOutputStream() @@ -362,10 +391,8 @@ internal class SecretStoringUtils @Inject constructor( @Throws(Exception::class) 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) + val inputCipher = getEncryptCipher(alias) val outputStream = ByteArrayOutputStream() CipherOutputStream(outputStream, inputCipher).use { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt new file mode 100644 index 0000000000..37a40fd677 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2022 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.api.securestorage + +import android.content.Context +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider +import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider +import java.security.KeyStore + +@Module +internal abstract class SecureStorageModule { + + @Module + companion object { + @Provides + fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } + + @Provides + fun provideSecretStoringUtils( + context: Context, + keyStore: KeyStore, + buildVersionSdkIntProvider: BuildVersionSdkIntProvider, + ): SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) + } + + @Binds + abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt index 6b75c94cb2..e217611d96 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.securestorage +package org.matrix.android.sdk.api.securestorage import java.io.InputStream import java.io.OutputStream diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index a0d122635d..1b01239de5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -47,7 +47,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.search.SearchService -import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService @@ -200,11 +199,6 @@ interface Session { */ fun syncService(): SyncService - /** - * Returns the SecureStorageService associated with the session. - */ - fun secureStorageService(): SecureStorageService - /** * Returns the ProfileService associated with the session. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt similarity index 87% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt index 515656049a..b7ea187ec5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.util.system +package org.matrix.android.sdk.api.util -internal interface BuildVersionSdkIntProvider { +interface BuildVersionSdkIntProvider { /** * Return the current version of the Android SDK. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt similarity index 85% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt index 806c6e9735..7f0024cafa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.util.system +package org.matrix.android.sdk.api.util import android.os.Build import javax.inject.Inject -internal class DefaultBuildVersionSdkIntProvider @Inject constructor() : +class DefaultBuildVersionSdkIntProvider @Inject constructor() : BuildVersionSdkIntProvider { override fun get() = Build.VERSION.SDK_INT } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt index b3a039d119..86355ceaa8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt @@ -21,7 +21,7 @@ import androidx.core.content.edit import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.BuildConfig -import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils import timber.log.Timber import java.security.SecureRandom import javax.inject.Inject @@ -40,7 +40,7 @@ import javax.inject.Inject */ internal class RealmKeysUtils @Inject constructor( context: Context, - private val secretStoringUtils: SecretStoringUtils + private val secretStoringUtils: SecretStoringUtils, ) { private val rng = SecureRandom() @@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor( private fun createAndSaveKeyForDatabase(alias: String): ByteArray { val key = generateKeyForRealm() val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) - val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) + val toStore = secretStoringUtils.securelyStoreBytes(encodedKey.toByteArray(), alias) sharedPreferences.edit { putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING)) } @@ -85,7 +85,7 @@ internal class RealmKeysUtils @Inject constructor( 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) + val b64 = secretStoringUtils.loadSecureSecretBytes(encryptedKey, alias) return Base64.decode(b64, Base64.NO_PADDING) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index d668c0498f..44ec90ed40 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.securestorage.SecureStorageModule +import org.matrix.android.sdk.api.securestorage.SecureStorageService import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.AuthModule @@ -53,7 +55,8 @@ import java.io.File DebugModule::class, SettingsModule::class, SystemModule::class, - NoOpTestModule::class + NoOpTestModule::class, + SecureStorageModule::class, ] ) @MatrixScope @@ -96,6 +99,8 @@ internal interface MatrixComponent { fun sessionManager(): SessionManager + fun secureStorageService(): SecureStorageService + fun matrixWorkerFactory(): MatrixWorkerFactory fun inject(matrix: Matrix) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt index ef8133dd15..8f6605d657 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.securestorage +package org.matrix.android.sdk.internal.securestorage -import org.matrix.android.sdk.api.session.securestorage.SecureStorageService +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils +import org.matrix.android.sdk.api.securestorage.SecureStorageService import java.io.InputStream import java.io.OutputStream import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 36d3f0606b..7c50a0ff84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -55,7 +55,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.search.SearchService -import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService @@ -111,7 +110,6 @@ internal class DefaultSession @Inject constructor( private val cryptoService: Lazy, private val defaultFileService: Lazy, private val permalinkService: Lazy, - private val secureStorageService: Lazy, private val profileService: Lazy, private val syncService: Lazy, private val mediaService: Lazy, @@ -220,7 +218,6 @@ internal class DefaultSession @Inject constructor( override fun eventService(): EventService = eventService.get() override fun termsService(): TermsService = termsService.get() override fun syncService(): SyncService = syncService.get() - override fun secureStorageService(): SecureStorageService = secureStorageService.get() override fun profileService(): ProfileService = profileService.get() override fun presenceService(): PresenceService = presenceService.get() override fun accountService(): AccountService = accountService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index f01451b688..d3cae3ac2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -20,6 +20,7 @@ import dagger.BindsInstance import dagger.Component import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams +import org.matrix.android.sdk.api.securestorage.SecureStorageModule import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.internal.crypto.CryptoModule import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker @@ -98,7 +99,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule ThirdPartyModule::class, SpaceModule::class, PresenceModule::class, - RequestModule::class + RequestModule::class, + SecureStorageModule::class, ] ) @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 950cb899f8..f8a52f0b7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService import org.matrix.android.sdk.api.session.openid.OpenIdService import org.matrix.android.sdk.api.session.permalinks.PermalinkService -import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.util.md5 @@ -93,7 +92,6 @@ import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcesso import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor -import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter @@ -367,9 +365,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver - @Binds - abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService - @Binds abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt index 396d12f369..8c7d7704ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.util.system import dagger.Binds import dagger.Module +import org.matrix.android.sdk.api.securestorage.SecureStorageService +import org.matrix.android.sdk.internal.securestorage.DefaultSecureStorageService import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.DefaultClock @@ -25,7 +27,7 @@ import org.matrix.android.sdk.internal.util.time.DefaultClock internal abstract class SystemModule { @Binds - abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider + abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService @Binds abstract fun bindClock(clock: DefaultClock): Clock diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt new file mode 100644 index 0000000000..c118cf07a1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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.util + +import android.os.Build +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider + +class DefaultBuildVersionSdkIntProviderTests { + + @Test + fun getReturnsCurrentVersionFromBuild_Version_SDK_INT() { + val provider = DefaultBuildVersionSdkIntProvider() + provider.get() shouldBeEqualTo Build.VERSION.SDK_INT + } +} diff --git a/vector/build.gradle b/vector/build.gradle index 8d704141e5..95e4b29007 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -361,6 +361,7 @@ dependencies { implementation libs.androidx.core implementation "androidx.media:media:1.6.0" implementation "androidx.transition:transition:1.4.1" + implementation libs.androidx.biometric implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" @@ -421,7 +422,6 @@ dependencies { implementation 'com.google.android.flexbox:flexbox:3.0.0' implementation libs.androidx.autoFill implementation 'jp.wasabeef:glide-transformations:4.3.0' - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' implementation 'com.github.hyuwah:DraggableView:1.0.0' // Custom Tab @@ -561,4 +561,5 @@ dependencies { } androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator + debugImplementation libs.androidx.fragmentTesting } diff --git a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt index 1399d1d6a9..7920e8e0d8 100644 --- a/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt +++ b/vector/src/androidTest/java/im/vector/app/RegistrationTest.kt @@ -29,6 +29,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import im.vector.app.features.MainActivity +import im.vector.app.features.analytics.ui.consent.AnalyticsOptInActivity import im.vector.app.features.home.HomeActivity import org.hamcrest.CoreMatchers.not import org.junit.Ignore @@ -106,6 +107,12 @@ class RegistrationTest { .check(matches(isEnabled())) .perform(closeSoftKeyboard(), click()) + withIdlingResource(activityIdlingResource(AnalyticsOptInActivity::class.java)) { + onView(withId(R.id.later)) + .check(matches(isDisplayed())) + .perform(click()) + } + withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { onView(withId(R.id.roomListContainer)) .check(matches(isDisplayed())) diff --git a/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt new file mode 100644 index 0000000000..ddf89b5e46 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/TestBuildVersionSdkIntProvider.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022 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.app + +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider + +class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider { + var value: Int = 0 + + override fun get() = value +} diff --git a/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt index 428efdea86..0c8aa95ee4 100644 --- a/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/html/SpanUtilsTest.kt @@ -25,9 +25,11 @@ import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.UnderlineSpan import androidx.emoji2.text.EmojiCompat +import androidx.test.platform.app.InstrumentationRegistry import im.vector.app.InstrumentedTest import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeTrue +import org.junit.BeforeClass import org.junit.FixMethodOrder import org.junit.Ignore import org.junit.Test @@ -42,6 +44,14 @@ import java.util.concurrent.TimeUnit @Ignore class SpanUtilsTest : InstrumentedTest { + companion object { + @BeforeClass + @JvmStatic + fun setupClass() { + EmojiCompat.init(InstrumentationRegistry.getInstrumentation().targetContext) + } + } + private val spanUtils = SpanUtils { val emojiCompat = EmojiCompat.get() emojiCompat.waitForInit() diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt new file mode 100644 index 0000000000..21e15e1585 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/LockScreenTestConstants.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen + +object LockScreenTestConstants { + const val ALIAS = "some_alias" +} diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt new file mode 100644 index 0000000000..b519d2f623 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelperTests.kt @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen.biometrics + +import android.content.Intent +import android.os.Build +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED +import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS +import androidx.lifecycle.lifecycleScope +import androidx.test.core.app.ActivityScenario +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.TestBuildVersionSdkIntProvider +import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration +import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider +import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode +import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository +import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity +import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment +import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.unmockkObject +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class BiometricHelperTests { + + private val biometricManager = mockk(relaxed = true) + private val lockScreenKeyRepository = mockk(relaxed = true) + private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() + + @Before + fun setup() { + clearAllMocks() + } + + @Test + fun canUseWeakBiometricAuthReturnsTrueIfIsFaceUnlockEnabledAndCanAuthenticate() { + every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_SUCCESS + val configuration = createDefaultConfiguration(isFaceUnlockEnabled = true) + val biometricUtils = createBiometricHelper(configuration) + + biometricUtils.canUseWeakBiometricAuth.shouldBeTrue() + + val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isFaceUnlockEnabled = false)) + biometricUtilsWithDisabledAuth.canUseWeakBiometricAuth.shouldBeFalse() + + every { biometricManager.canAuthenticate(BIOMETRIC_WEAK) } returns BIOMETRIC_ERROR_NONE_ENROLLED + biometricUtils.canUseWeakBiometricAuth.shouldBeFalse() + } + + @Test + fun canUseStrongBiometricAuthReturnsTrueIfIsBiometricsEnabledAndCanAuthenticate() { + every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_SUCCESS + val configuration = createDefaultConfiguration(isBiometricsEnabled = true) + val biometricUtils = createBiometricHelper(configuration) + + biometricUtils.canUseStrongBiometricAuth.shouldBeTrue() + + val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = false)) + biometricUtilsWithDisabledAuth.canUseStrongBiometricAuth.shouldBeFalse() + + every { biometricManager.canAuthenticate(BIOMETRIC_STRONG) } returns BIOMETRIC_ERROR_NONE_ENROLLED + biometricUtils.canUseStrongBiometricAuth.shouldBeFalse() + } + + @Test + fun canUseDeviceCredentialAuthReturnsTrueIfIsDeviceCredentialsUnlockEnabledAndCanAuthenticate() { + every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_SUCCESS + val configuration = createDefaultConfiguration(isDeviceCredentialUnlockEnabled = true) + val biometricUtils = createBiometricHelper(configuration) + + biometricUtils.canUseDeviceCredentialsAuth.shouldBeTrue() + + val biometricUtilsWithDisabledAuth = createBiometricHelper(createDefaultConfiguration(isDeviceCredentialUnlockEnabled = false)) + biometricUtilsWithDisabledAuth.canUseDeviceCredentialsAuth.shouldBeFalse() + + every { biometricManager.canAuthenticate(DEVICE_CREDENTIAL) } returns BIOMETRIC_ERROR_NONE_ENROLLED + biometricUtils.canUseDeviceCredentialsAuth.shouldBeFalse() + } + + @Test + fun isSystemAuthEnabledReturnsTrueIfAnyAuthenticationMethodIsAvailableAndEnabledAndSystemKeyExists() { + val biometricHelper = mockk(relaxed = true) { + every { hasSystemKey } returns true + every { isSystemKeyValid } returns true + every { canUseAnySystemAuth } answers { callOriginal() } + every { isSystemAuthEnabledAndValid } answers { callOriginal() } + } + biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse() + + every { biometricHelper.canUseWeakBiometricAuth } returns true + biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue() + + every { biometricHelper.canUseWeakBiometricAuth } returns false + every { biometricHelper.canUseStrongBiometricAuth } returns true + biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue() + + every { biometricHelper.canUseStrongBiometricAuth } returns false + every { biometricHelper.canUseDeviceCredentialsAuth } returns true + biometricHelper.isSystemAuthEnabledAndValid.shouldBeTrue() + + every { biometricHelper.isSystemKeyValid } returns false + biometricHelper.isSystemAuthEnabledAndValid.shouldBeFalse() + } + + @Test + fun hasSystemKeyReturnsKeyHelperHasSystemKey() { + val biometricUtils = createBiometricHelper(createDefaultConfiguration()) + every { lockScreenKeyRepository.hasSystemKey() } returns true + biometricUtils.hasSystemKey.shouldBeTrue() + + every { lockScreenKeyRepository.hasSystemKey() } returns false + biometricUtils.hasSystemKey.shouldBeFalse() + } + + @Test + fun isSystemKeyValidReturnsKeyHelperIsSystemKeyValid() { + val biometricUtils = createBiometricHelper(createDefaultConfiguration()) + every { lockScreenKeyRepository.isSystemKeyValid() } returns true + biometricUtils.isSystemKeyValid.shouldBeTrue() + + every { lockScreenKeyRepository.isSystemKeyValid() } returns false + biometricUtils.isSystemKeyValid.shouldBeFalse() + } + + @Test + fun disableAuthenticationDeletesSystemKeyAndCancelsPrompt() { + val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration())) + biometricUtils.disableAuthentication() + + verify { lockScreenKeyRepository.deleteSystemKey() } + verify { biometricUtils.cancelPrompt() } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Ignore("This won't work in CI as the emulator won't have biometric auth enabled.") + @Test + fun authenticateShowsPrompt() = runTest { + val biometricUtils = createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true)) + every { lockScreenKeyRepository.isSystemKeyValid() } returns true + val latch = CountDownLatch(1) + with(ActivityScenario.launch(LockScreenTestActivity::class.java)) { + onActivity { activity -> + biometricUtils.authenticate(activity) + activity.supportFragmentManager.fragments.isNotEmpty().shouldBeTrue() + close() + latch.countDown() + } + } + latch.await(1, TimeUnit.SECONDS) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest { + mockkStatic("kotlinx.coroutines.flow.FlowKt") + val mockAuthChannel: Channel = mockk(relaxed = true) { + // Empty flow to keep the dialog open + every { receiveAsFlow() } returns flowOf() + } + val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) { + every { createAuthChannel() } returns mockAuthChannel + } + mockkObject(DevicePromptCheck) + every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true + every { lockScreenKeyRepository.isSystemKeyValid() } returns true + val latch = CountDownLatch(1) + val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java) + with(ActivityScenario.launch(intent)) { + onActivity { activity -> + biometricUtils.authenticate(activity) + launch { + activity.supportFragmentManager.fragments.any { it is FallbackBiometricDialogFragment }.shouldBeTrue() + close() + latch.countDown() + } + } + } + latch.await(1, TimeUnit.SECONDS) + unmockkObject(DevicePromptCheck) + unmockkStatic("kotlinx.coroutines.flow.FlowKt") + } + + @Test + fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest { + buildVersionSdkIntProvider.value = Build.VERSION_CODES.M + every { lockScreenKeyRepository.isSystemKeyValid() } returns true + val mockAuthChannel = Channel(capacity = 1) + val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) { + every { createAuthChannel() } returns mockAuthChannel + every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk() + } + + val latch = CountDownLatch(1) + val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java) + ActivityScenario.launch(intent).onActivity { activity -> + activity.lifecycleScope.launch { + launch { + mockAuthChannel.send(true) + mockAuthChannel.close() + } + biometricUtils.authenticate(activity).collect() + latch.countDown() + } + } + + latch.await(1, TimeUnit.SECONDS) + verify { lockScreenKeyRepository.ensureSystemKey() } + } + + private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val configProvider = LockScreenConfiguratorProvider(configuration) + return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider) + } + + private fun createDefaultConfiguration( + mode: LockScreenMode = LockScreenMode.VERIFY, + pinCodeLength: Int = 4, + isBiometricsEnabled: Boolean = false, + isFaceUnlockEnabled: Boolean = false, + isDeviceCredentialUnlockEnabled: Boolean = false, + needsNewCodeValidation: Boolean = false, + otherChanges: LockScreenConfiguration.() -> LockScreenConfiguration = { this }, + ): LockScreenConfiguration = LockScreenConfiguration( + mode, + pinCodeLength, + isBiometricsEnabled, + isFaceUnlockEnabled, + isDeviceCredentialUnlockEnabled, + needsNewCodeValidation + ).let(otherChanges) +} diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt new file mode 100644 index 0000000000..68e1244791 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/KeyStoreCryptoTests.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen.crypto + +import android.os.Build +import android.security.keystore.KeyPermanentlyInvalidatedException +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.TestBuildVersionSdkIntProvider +import io.mockk.every +import io.mockk.spyk +import io.mockk.verify +import org.amshove.kluent.invoking +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldThrow +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils +import java.security.KeyStore + +class KeyStoreCryptoTests { + + private val alias = "some_alias" + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } + private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M } + private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider)) + private val keyStoreCrypto = spyk( + KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils) + ) + + @After + fun setup() { + keyStore.deleteEntry(alias) + } + + @Test + fun ensureKeyChecksValidityOfKeyAndThrows() { + keyStore.containsAlias(alias) shouldBe false + + val exception = KeyPermanentlyInvalidatedException() + every { secretStoringUtils.getEncryptCipher(any()) } throws exception + + invoking { keyStoreCrypto.ensureKey() } shouldThrow exception + keyStoreCrypto.hasValidKey() shouldBe false + } + + @Test + fun hasValidKeyChecksValidityOfKey() { + runCatching { keyStoreCrypto.ensureKey() } + keyStoreCrypto.hasValidKey() shouldBe true + + val exception = KeyPermanentlyInvalidatedException() + every { secretStoringUtils.getEncryptCipher(any()) } throws exception + + runCatching { keyStoreCrypto.ensureKey() } + keyStoreCrypto.hasValidKey() shouldBe false + } + + @Test + fun hasKeyChecksIfKeyExists() { + keyStoreCrypto.hasKey() shouldBe false + + keyStoreCrypto.ensureKey() + keyStoreCrypto.hasKey() shouldBe true + keyStore.containsAlias(keyStoreCrypto.alias) + + keyStoreCrypto.deleteKey() + keyStoreCrypto.hasKey() shouldBe false + } + + @Test + fun deleteKeyRemovesTheKey() { + keyStore.containsAlias(alias) shouldBe false + + keyStoreCrypto.ensureKey() + keyStore.containsAlias(alias) shouldBe true + + keyStoreCrypto.deleteKey() + keyStore.containsAlias(alias) shouldBe false + } + + @Test + fun checkEncryptionAndDecryptionOfStringsWorkAsExpected() { + val original = "some plain text" + val encryptedString = keyStoreCrypto.encryptToString(original) + val encryptedBytes = keyStoreCrypto.encrypt(original) + val result = keyStoreCrypto.decryptToString(encryptedString) + val resultFromBytes = keyStoreCrypto.decryptToString(encryptedBytes) + result shouldBeEqualTo original + resultFromBytes shouldBeEqualTo original + } + + @Test + fun checkEncryptionAndDecryptionWorkAsExpected() { + val original = "some plain text".toByteArray() + val encryptedBytes = keyStoreCrypto.encrypt(original) + val encryptedString = keyStoreCrypto.encryptToString(original) + val result = keyStoreCrypto.decrypt(encryptedBytes) + val resultFromString = keyStoreCrypto.decrypt(encryptedString) + result shouldBeEqualTo original + resultFromString shouldBeEqualTo original + } + + @Test + fun hasValidKeyReturnsFalseWhenKeyPermanentlyInvalidatedExceptionIsThrown() { + every { keyStoreCrypto.hasKey() } returns true + every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException() + + keyStoreCrypto.hasValidKey().shouldBeFalse() + } + + @Test + fun hasValidKeyReturnsFalseWhenKeyDoesNotExist() { + every { keyStoreCrypto.hasKey() } returns false + keyStoreCrypto.hasValidKey().shouldBeFalse() + } + + @Test + fun hasValidKeyReturnsIfKeyExistsOnAndroidL() { + versionProvider.value = Build.VERSION_CODES.LOLLIPOP + + every { keyStoreCrypto.hasKey() } returns true + keyStoreCrypto.hasValidKey().shouldBeTrue() + + every { keyStoreCrypto.hasKey() } returns false + keyStoreCrypto.hasValidKey().shouldBeFalse() + } + + @Test + fun getCryptoObjectUsesCipherFromSecretStoringUtils() { + keyStoreCrypto.getCryptoObject() + verify { secretStoringUtils.getEncryptCipher(any()) } + + every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException() + invoking { keyStoreCrypto.getCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt new file mode 100644 index 0000000000..23eefe6577 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepositoryTests.kt @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen.crypto + +import android.security.keystore.KeyPermanentlyInvalidatedException +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.features.settings.VectorPreferences +import io.mockk.clearAllMocks +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.coInvoking +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldNotThrow +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider +import java.security.KeyStore + +class LockScreenKeyRepositoryTests { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val buildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider() + + private val keyStoreCryptoFactory: KeyStoreCrypto.Factory = mockk { + every { provide(any(), any()) } answers { + KeyStoreCrypto(arg(0), false, context, buildVersionSdkIntProvider, keyStore) + } + } + + private lateinit var lockScreenKeyRepository: LockScreenKeyRepository + private val pinCodeMigrator: PinCodeMigrator = mockk(relaxed = true) + private val vectorPreferences: VectorPreferences = mockk(relaxed = true) + + private val keyStore: KeyStore by lazy { + KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) } + } + + @Before + fun setup() { + lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory)) + } + + @After + fun tearDown() { + clearAllMocks() + keyStore.deleteEntry("base.pin_code") + keyStore.deleteEntry("base.system") + } + + @Test + fun ensureSystemKeyCreatesSystemKeyIfNeeded() { + lockScreenKeyRepository.ensureSystemKey() + lockScreenKeyRepository.hasSystemKey().shouldBeTrue() + } + + @Test + fun encryptPinCodeCreatesPinCodeKey() { + lockScreenKeyRepository.encryptPinCode("1234") + lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue() + } + + @Test + fun decryptPinCodeDecryptsEncodedPinCode() { + val decodedPinCode = "1234" + val pinCodeKeyCryptoMock = mockk(relaxed = true) { + every { decryptToString(any()) } returns decodedPinCode + } + every { keyStoreCryptoFactory.provide(any(), any()) } returns pinCodeKeyCryptoMock + lockScreenKeyRepository.decryptPinCode("SOME_VALUE") shouldBeEqualTo decodedPinCode + } + + @Test + fun isSystemKeyValidReturnsWhatKeyStoreCryptoHasValidKeyReplies() { + val systemKeyCryptoMock = mockk(relaxed = true) { + every { hasKey() } returns true + } + every { keyStoreCryptoFactory.provide(any(), any()) } returns systemKeyCryptoMock + + every { systemKeyCryptoMock.hasValidKey() } returns false + lockScreenKeyRepository.isSystemKeyValid().shouldBeFalse() + + every { systemKeyCryptoMock.hasValidKey() } returns true + lockScreenKeyRepository.isSystemKeyValid().shouldBeTrue() + } + + @Test + fun hasSystemKeyReturnsTrueAfterSystemKeyIsCreated() { + lockScreenKeyRepository.hasSystemKey().shouldBeFalse() + + lockScreenKeyRepository.ensureSystemKey() + + lockScreenKeyRepository.hasSystemKey().shouldBeTrue() + } + + @Test + fun hasPinCodeKeyReturnsTrueAfterPinCodeKeyIsCreated() { + lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse() + + lockScreenKeyRepository.encryptPinCode("1234") + + lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue() + } + + @Test + fun deleteSystemKeyRemovesTheKeyFromKeyStore() { + lockScreenKeyRepository.ensureSystemKey() + lockScreenKeyRepository.hasSystemKey().shouldBeTrue() + + lockScreenKeyRepository.deleteSystemKey() + + lockScreenKeyRepository.hasSystemKey().shouldBeFalse() + } + + @Test + fun deletePinCodeKeyRemovesTheKeyFromKeyStore() { + lockScreenKeyRepository.encryptPinCode("1234") + lockScreenKeyRepository.hasPinCodeKey().shouldBeTrue() + + lockScreenKeyRepository.deletePinCodeKey() + + lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse() + } + + @Test + fun migrateKeysIfNeededReturnsEarlyIfNotNeeded() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns false + + lockScreenKeyRepository.migrateKeysIfNeeded() + + coVerify(exactly = 0) { pinCodeMigrator.migrate(any()) } + } + + @Test + fun migrateKeysIfNeededWillMigratePinCodeAndKeys() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns true + + lockScreenKeyRepository.migrateKeysIfNeeded() + + coVerify { pinCodeMigrator.migrate(any()) } + } + + @Test + fun migrateKeysIfNeededWillCreateSystemKeyIfNeeded() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns true + every { vectorPreferences.useBiometricsToUnlock() } returns true + every { lockScreenKeyRepository.ensureSystemKey() } returns mockk() + + lockScreenKeyRepository.migrateKeysIfNeeded() + + verify { lockScreenKeyRepository.ensureSystemKey() } + } + + @Test + fun migrateKeysIfNeededWillHandleKeyPermanentlyInvalidatedException() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns true + every { vectorPreferences.useBiometricsToUnlock() } returns true + every { lockScreenKeyRepository.ensureSystemKey() } throws KeyPermanentlyInvalidatedException() + + coInvoking { lockScreenKeyRepository.migrateKeysIfNeeded() } shouldNotThrow KeyPermanentlyInvalidatedException::class + + verify { lockScreenKeyRepository.ensureSystemKey() } + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt new file mode 100644 index 0000000000..297793c7a4 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/crypto/PinCodeMigratorTests.kt @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2022 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. + */ + +@file:Suppress("DEPRECATION") + +package im.vector.app.features.pin.lockscreen.crypto + +import android.os.Build +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.preference.PreferenceManager +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.features.pin.PinCodeStore +import im.vector.app.features.pin.SharedPrefPinCodeStore +import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.ANDROID_KEY_STORE +import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.securestorage.SecretStoringUtils +import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider +import java.math.BigInteger +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.spec.MGF1ParameterSpec +import java.security.spec.X509EncodedKeySpec +import java.util.Calendar +import java.util.UUID +import javax.crypto.Cipher +import javax.crypto.spec.OAEPParameterSpec +import javax.crypto.spec.PSource +import javax.security.auth.x500.X500Principal +import kotlin.math.abs + +class PinCodeMigratorTests { + + private val alias = UUID.randomUUID().toString() + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val pinCodeStore: PinCodeStore = spyk( + SharedPrefPinCodeStore(PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().context)) + ) + private val keyStore: KeyStore = spyk(KeyStore.getInstance(ANDROID_KEY_STORE)).also { it.load(null) } + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider = mockk { + every { get() } returns Build.VERSION_CODES.M + } + private val secretStoringUtils: SecretStoringUtils = spyk( + SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) + ) + private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)) + + @After + fun tearDown() { + if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) { + keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) + } + if (keyStore.containsAlias(alias)) { + keyStore.deleteEntry(alias) + } + runBlocking { pinCodeStore.deletePinCode() } + } + + @Test + fun isMigrationNeededReturnsTrueIfLegacyKeyExists() { + pinCodeMigrator.isMigrationNeeded() shouldBe false + + generateLegacyKey() + + pinCodeMigrator.isMigrationNeeded() shouldBe true + } + + @Test + fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns false + coEvery { pinCodeStore.getPinCode() } returns null + + pinCodeMigrator.migrate(alias) + + coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } + verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } + coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } + verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } + } + + @Test + fun migrateWillReturnEarlyIfIsNotNeeded() = runTest { + every { pinCodeMigrator.isMigrationNeeded() } returns false + coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234" + every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0) + + pinCodeMigrator.migrate(alias) + + coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } + verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } + coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } + verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } + } + + @Test + fun migratePinCodeM() = runTest { + val pinCode = "1234" + saveLegacyPinCode(pinCode) + + pinCodeMigrator.migrate(alias) + + coVerify { pinCodeMigrator.getDecryptedPinCode() } + verify { secretStoringUtils.securelyStoreBytes(any(), any()) } + coVerify { pinCodeStore.savePinCode(any()) } + verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } + + val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias)) + decodedPinCode shouldBeEqualTo pinCode + keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false + keyStore.containsAlias(alias) shouldBe true + } + + @Test + fun migratePinCodeL() = runTest { + val pinCode = "1234" + every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP + saveLegacyPinCode(pinCode) + + pinCodeMigrator.migrate(alias) + + coVerify { pinCodeMigrator.getDecryptedPinCode() } + verify { secretStoringUtils.securelyStoreBytes(any(), any()) } + coVerify { pinCodeStore.savePinCode(any()) } + verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } + + val decodedPinCode = String(secretStoringUtils.loadSecureSecretBytes(Base64.decode(pinCodeStore.getPinCode().orEmpty(), Base64.NO_WRAP), alias)) + decodedPinCode shouldBeEqualTo pinCode + keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS) shouldBe false + keyStore.containsAlias(alias) shouldBe true + } + + private fun generateLegacyKey() { + if (keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return + + if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { + generateLegacyKeyM() + } else { + generateLegacyKeyL() + } + } + + private fun generateLegacyKeyL() { + val start = Calendar.getInstance() + val end = Calendar.getInstance().also { it.add(Calendar.YEAR, 25) } + + val keyGen = KeyPairGenerator + .getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE) + + val spec = KeyPairGeneratorSpec.Builder(context) + .setAlias(LEGACY_PIN_CODE_KEY_ALIAS) + .setSubject(X500Principal("CN=$LEGACY_PIN_CODE_KEY_ALIAS")) + .setSerialNumber(BigInteger.valueOf(abs(LEGACY_PIN_CODE_KEY_ALIAS.hashCode()).toLong())) + .setEndDate(end.time) + .setStartDate(start.time) + .setSerialNumber(BigInteger.ONE) + .setSubject(X500Principal("CN = Secured Preference Store, O = Devliving Online")) + .build() + + keyGen.initialize(spec) + keyGen.generateKeyPair() + } + + private fun generateLegacyKeyM() { + val keyGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE) + keyGenerator.initialize( + KeyGenParameterSpec.Builder(LEGACY_PIN_CODE_KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) + .build() + ) + keyGenerator.generateKeyPair() + } + + private suspend fun saveLegacyPinCode(value: String) { + generateLegacyKey() + val publicKey = keyStore.getCertificate(LEGACY_PIN_CODE_KEY_ALIAS).publicKey + val cipher = getLegacyCipher() + if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) { + val unrestrictedKey = KeyFactory.getInstance(publicKey.algorithm).generatePublic(X509EncodedKeySpec(publicKey.encoded)) + val spec = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT) + cipher.init(Cipher.ENCRYPT_MODE, unrestrictedKey, spec) + } else { + cipher.init(Cipher.ENCRYPT_MODE, publicKey) + } + val bytes = cipher.doFinal(value.toByteArray()) + val encryptedPinCode = Base64.encodeToString(bytes, Base64.NO_WRAP) + pinCodeStore.savePinCode(encryptedPinCode) + } + + private fun getLegacyCipher(): Cipher { + return when (buildVersionSdkIntProvider.get()) { + Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.LOLLIPOP_MR1 -> getCipherL() + else -> getCipherM() + } + } + + private fun getCipherL(): Cipher { + val provider = if (buildVersionSdkIntProvider.get() < Build.VERSION_CODES.M) "AndroidOpenSSL" else "AndroidKeyStoreBCWorkaround" + val transformation = "RSA/ECB/PKCS1Padding" + return Cipher.getInstance(transformation, provider) + } + + private fun getCipherM(): Cipher { + val transformation = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" + return Cipher.getInstance(transformation) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt new file mode 100644 index 0000000000..1545e140a0 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/tests/LockScreenTestActivity.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen.tests + +import androidx.fragment.app.FragmentActivity + +class LockScreenTestActivity : FragmentActivity() diff --git a/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt new file mode 100644 index 0000000000..3781535f72 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/features/pin/lockscreen/ui/fallbackprompt/FallbackBiometricDialogFragmentTests.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2022 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.app.features.pin.lockscreen.ui.fallbackprompt + +import android.view.View +import android.widget.Button +import android.widget.TextView +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.testing.launchFragment +import androidx.lifecycle.Lifecycle +import androidx.test.platform.app.InstrumentationRegistry +import com.airbnb.mvrx.Mavericks +import im.vector.app.R +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.receiveAsFlow +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import java.util.concurrent.CountDownLatch + +class FallbackBiometricDialogFragmentTests { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + + @Test + fun dismissTriggersOnDismissCallback() { + val latch = CountDownLatch(1) + val fragmentScenario = launchFragment(noArgsBundle()) + fragmentScenario.onFragment { fragment -> + fragment.onDismiss = { latch.countDown() } + fragment.dismiss() + } + latch.await() + } + + @Test + fun argsModifyUI() { + val latch = CountDownLatch(1) + val args = FallbackBiometricDialogFragment.Args( + title = "Title", + description = "Description", + cancelActionText = "Cancel text", + ) + val fragmentScenario = launchFragment(bundleOf(Mavericks.KEY_ARG to args)) + fragmentScenario.onFragment { fragment -> + val view = fragment.requireView() + view.findViewById