Merge pull request #4115 from vector-im/feature/adm/login-key-verification-flow

Skipping passphrase screen when no passphrase is available
This commit is contained in:
Benoit Marty 2021-09-29 15:04:58 +02:00 committed by GitHub
commit 357d7ee338
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 381 additions and 9 deletions

2
changelog.d/3898.bugfix Normal file
View file

@ -0,0 +1,2 @@
Fixes the passphrase screen being incorrectly shown when pressing back on the key verification screen.
When the user doesn't have a passphrase set we don't show the passphrase screen.

View file

@ -76,7 +76,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
} }
init { init {
setState { setState {
copy(userId = session.myUserId) copy(userId = session.myUserId)
} }
@ -167,10 +166,14 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
if (state.checkingSSSSAction is Loading) return@withState // ignore if (state.checkingSSSSAction is Loading) return@withState // ignore
when (state.step) { when (state.step) {
SharedSecureStorageViewState.Step.EnterKey -> { SharedSecureStorageViewState.Step.EnterKey -> {
setState { if (state.hasPassphrase) {
copy( setState {
step = SharedSecureStorageViewState.Step.EnterPassphrase copy(
) step = SharedSecureStorageViewState.Step.EnterPassphrase
)
}
} else {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} }
} }
SharedSecureStorageViewState.Step.ResetAll -> { SharedSecureStorageViewState.Step.ResetAll -> {

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2021 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 androidx.lifecycle
/**
* Manual test override to stop BaseMvRxViewModel from interacting with the android looper/main thread
* Tests will run on their original test worker threads
*
* This has been fixed is newer versions of Mavericks via LifecycleRegistry.createUnsafe
* https://github.com/airbnb/mavericks/blob/master/mvrx-rxjava2/src/main/kotlin/com/airbnb/mvrx/BaseMvRxViewModel.kt#L61
*/
@Suppress("UNUSED")
class LifecycleRegistry(@Suppress("UNUSED_PARAMETER") lifecycleOwner: LifecycleOwner) : Lifecycle() {
private var state = State.INITIALIZED
fun setCurrentState(state: State) {
this.state = state
}
override fun addObserver(observer: LifecycleObserver) {
TODO("Not yet implemented")
}
override fun removeObserver(observer: LifecycleObserver) {
TODO("Not yet implemented")
}
override fun getCurrentState() = state
}

View file

@ -40,7 +40,7 @@ class KeysExporterTest {
private val cryptoService = FakeCryptoService() private val cryptoService = FakeCryptoService()
private val context = FakeContext() private val context = FakeContext()
private val keysExporter = KeysExporter( private val keysExporter = KeysExporter(
session = FakeSession(cryptoService = cryptoService), session = FakeSession(fakeCryptoService = cryptoService),
context = context.instance, context = context.instance,
dispatchers = CoroutineDispatchers(Dispatchers.Unconfined) dispatchers = CoroutineDispatchers(Dispatchers.Unconfined)
) )

View file

@ -0,0 +1,148 @@
/*
* Copyright (c) 2021 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.crypto.quads
import com.airbnb.mvrx.Uninitialized
import im.vector.app.test.InstantRxRule
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.FakeStringProvider
import im.vector.app.test.test
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfo
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
private const val IGNORED_PASSPHRASE_INTEGRITY = false
private val KEY_INFO_WITH_PASSPHRASE = KeyInfo(
id = "id",
content = SecretStorageKeyContent(passphrase = SsssPassphrase(null, 0, null))
)
private val KEY_INFO_WITHOUT_PASSPHRASE = KeyInfo(id = "id", content = SecretStorageKeyContent(passphrase = null))
class SharedSecureStorageViewModelTest {
@get:Rule
val instantRx = InstantRxRule()
private val stringProvider = FakeStringProvider()
private val session = FakeSession()
@Test
fun `given a key info with passphrase when initialising then step is EnterPassphrase`() {
givenKey(KEY_INFO_WITH_PASSPHRASE)
val viewModel = createViewModel()
viewModel.test().assertState(aViewState(
hasPassphrase = true,
step = SharedSecureStorageViewState.Step.EnterPassphrase
))
}
@Test
fun `given a key info without passphrase when initialising then step is EnterKey`() {
givenKey(KEY_INFO_WITHOUT_PASSPHRASE)
val viewModel = createViewModel()
viewModel.test().assertState(aViewState(
hasPassphrase = false,
step = SharedSecureStorageViewState.Step.EnterKey
))
}
@Test
fun `given on EnterKey step when going back then dismisses`() {
givenKey(KEY_INFO_WITHOUT_PASSPHRASE)
val viewModel = createViewModel()
val test = viewModel.test()
viewModel.handle(SharedSecureStorageAction.Back)
test.assertEvents(SharedSecureStorageViewEvent.Dismiss)
}
@Test
fun `given on passphrase step when using key then step is EnterKey`() {
givenKey(KEY_INFO_WITH_PASSPHRASE)
val viewModel = createViewModel()
val test = viewModel.test()
viewModel.handle(SharedSecureStorageAction.UseKey)
test.assertState(aViewState(
hasPassphrase = true,
step = SharedSecureStorageViewState.Step.EnterKey
))
}
@Test
fun `given a key info with passphrase and on EnterKey step when going back then step is EnterPassphrase`() {
givenKey(KEY_INFO_WITH_PASSPHRASE)
val viewModel = createViewModel()
val test = viewModel.test()
viewModel.handle(SharedSecureStorageAction.UseKey)
viewModel.handle(SharedSecureStorageAction.Back)
test.assertState(aViewState(
hasPassphrase = true,
step = SharedSecureStorageViewState.Step.EnterPassphrase
))
}
@Test
fun `given on passphrase step when going back then dismisses`() {
givenKey(KEY_INFO_WITH_PASSPHRASE)
val viewModel = createViewModel()
val test = viewModel.test()
viewModel.handle(SharedSecureStorageAction.Back)
test.assertEvents(SharedSecureStorageViewEvent.Dismiss)
}
private fun createViewModel() = SharedSecureStorageViewModel(
SharedSecureStorageViewState(),
SharedSecureStorageActivity.Args(keyId = null, emptyList(), "alias"),
stringProvider.instance,
session
)
private fun aViewState(hasPassphrase: Boolean, step: SharedSecureStorageViewState.Step) = SharedSecureStorageViewState(
ready = true,
hasPassphrase = hasPassphrase,
checkingSSSSAction = Uninitialized,
step = step,
activeDeviceCount = 0,
showResetAllAction = false,
userId = ""
)
private fun givenKey(keyInfo: KeyInfo) {
givenHasAccessToSecrets()
session.fakeSharedSecretStorageService._defaultKey = KeyInfoResult.Success(keyInfo)
}
private fun givenHasAccessToSecrets() {
session.fakeSharedSecretStorageService.integrityResult = IntegrityResult.Success(passphraseBased = IGNORED_PASSPHRASE_INTEGRITY)
}
}

View file

@ -16,4 +16,31 @@
package im.vector.app.test package im.vector.app.test
import com.airbnb.mvrx.MvRxState
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import io.reactivex.observers.TestObserver
import org.amshove.kluent.shouldBeEqualTo
fun String.trimIndentOneLine() = trimIndent().replace("\n", "") fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
fun <S : MvRxState, VA : VectorViewModelAction, VE : VectorViewEvents> VectorViewModel<S, VA, VE>.test(): ViewModelTest<S, VE> {
val state = { com.airbnb.mvrx.withState(this) { it } }
val viewEvents = viewEvents.observe().test()
return ViewModelTest(state, viewEvents)
}
class ViewModelTest<S, VE>(
val state: () -> S,
val viewEvents: TestObserver<VE>
) {
fun assertEvents(vararg expected: VE) {
viewEvents.assertValues(*expected)
}
fun assertState(expected: S) {
state() shouldBeEqualTo expected
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 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.test
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.schedulers.Schedulers
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class InstantRxRule : TestRule {
override fun apply(base: Statement, description: Description?): Statement {
RxJavaPlugins.setInitNewThreadSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
return base
}
}

View file

@ -16,12 +16,23 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import androidx.lifecycle.MutableLiveData
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
class FakeCryptoService : CryptoService by mockk() { class FakeCryptoService : CryptoService by mockk() {
var roomKeysExport = ByteArray(size = 1) var roomKeysExport = ByteArray(size = 1)
var cryptoDeviceInfos = mutableMapOf<String, CryptoDeviceInfo>()
override suspend fun exportRoomKeys(password: String) = roomKeysExport override suspend fun exportRoomKeys(password: String) = roomKeysExport
override fun getLiveCryptoDeviceInfo() = MutableLiveData(cryptoDeviceInfos.values.toList())
override fun getLiveCryptoDeviceInfo(userId: String) = getLiveCryptoDeviceInfo(listOf(userId))
override fun getLiveCryptoDeviceInfo(userIds: List<String>) = MutableLiveData(
cryptoDeviceInfos.filterKeys { userIds.contains(it) }.values.toList()
)
} }

View file

@ -18,10 +18,11 @@ package im.vector.app.test.fakes
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.CryptoService
class FakeSession( class FakeSession(
private val cryptoService: CryptoService = FakeCryptoService() val fakeCryptoService: FakeCryptoService = FakeCryptoService(),
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
) : Session by mockk(relaxed = true) { ) : Session by mockk(relaxed = true) {
override fun cryptoService() = cryptoService override fun cryptoService() = fakeCryptoService
override val sharedSecretStorageService = fakeSharedSecretStorageService
} }

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2021 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.test.fakes
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.KeySigner
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
class FakeSharedSecretStorageService : SharedSecretStorageService {
var integrityResult: IntegrityResult = IntegrityResult.Error(SharedSecretStorageError.OtherError(IllegalStateException()))
var _defaultKey: KeyInfoResult = KeyInfoResult.Error(SharedSecretStorageError.OtherError(IllegalStateException()))
override suspend fun generateKey(keyId: String, key: SsssKeySpec?, keyName: String, keySigner: KeySigner?): SsssKeyCreationInfo {
TODO("Not yet implemented")
}
override suspend fun generateKeyWithPassphrase(keyId: String, keyName: String, passphrase: String, keySigner: KeySigner, progressListener: ProgressListener?): SsssKeyCreationInfo {
TODO("Not yet implemented")
}
override fun getKey(keyId: String): KeyInfoResult {
TODO("Not yet implemented")
}
override fun getDefaultKey() = _defaultKey
override suspend fun setDefaultKey(keyId: String) {
TODO("Not yet implemented")
}
override fun hasKey(keyId: String): Boolean {
TODO("Not yet implemented")
}
override suspend fun storeSecret(name: String, secretBase64: String, keys: List<SharedSecretStorageService.KeyRef>) {
TODO("Not yet implemented")
}
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
TODO("Not yet implemented")
}
override suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String {
TODO("Not yet implemented")
}
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
override fun requestSecret(name: String, myOtherDeviceId: String) {
TODO("Not yet implemented")
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021 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.test.fakes
import im.vector.app.core.resources.StringProvider
import io.mockk.every
import io.mockk.mockk
class FakeStringProvider {
val instance = mockk<StringProvider>()
init {
every { instance.getString(any()) } answers {
"test-${args[0]}"
}
}
}