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