diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 9d7495ef23..31a70a22e0 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -19,7 +19,6 @@
android:supportsRtl="true"
android:theme="@style/AppTheme.Light"
tools:replace="android:allowBackup">
-
@@ -57,7 +56,6 @@
-
@@ -73,7 +71,7 @@
android:value=".features.home.HomeActivity" />
-
+
@@ -97,6 +95,7 @@
+
@@ -104,15 +103,14 @@
+
-
-
-
@@ -137,9 +134,9 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".features.home.HomeActivity" />
-
+
@@ -149,25 +146,17 @@
android:theme="@style/AppTheme.AttachmentsPreview" />
-
-
-
-
-
+ android:exported="false" />
-
-
-
+ android:exported="false" />
,
+ val resultKeyStoreAlias: String
+ ) : Parcelable
+
+ private val uiDisposables = CompositeDisposable()
+
+ private fun Disposable.disposeOnDestroyView(): Disposable {
+ uiDisposables.add(this)
+ return this
+ }
+
+ private val viewModel: SharedSecureStorageViewModel by viewModel()
+ @Inject lateinit var viewModelFactory: SharedSecureStorageViewModel.Factory
+ @Inject lateinit var errorFormatter: ErrorFormatter
+
+ override fun injectWith(injector: ScreenComponent) {
+ super.injectWith(injector)
+ injector.inject(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ toolbar.visibility = View.GONE
+ if (isFirstCreation()) {
+ addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java)
+ }
+
+
+ viewModel.viewEvents
+ .observe()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ observeViewEvents(it)
+ }
+ .disposeOnDestroyView()
+
+ viewModel.subscribe(this) {
+// renderState(it)
+ }
+ }
+
+ private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
+ when (it) {
+ is SharedSecureStorageViewEvent.Dismiss -> {
+ setResult(Activity.RESULT_CANCELED)
+ finish()
+ }
+ is SharedSecureStorageViewEvent.ShowModalLoading -> {
+ showWaitingView()
+ }
+ is SharedSecureStorageViewEvent.HideModalLoading -> {
+ hideWaitingView()
+ }
+ is SharedSecureStorageViewEvent.UpdateLoadingState -> {
+ updateWaitingView(it.waitingData)
+ }
+ }
+ }
+
+// fun renderState(state: SharedSecureStorageViewState) {
+// }
+
+ companion object {
+
+ const val RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
+ fun newIntent(context: Context, keyId: String? = null, requestedSecrets: List, resultKeyStoreAlias: String = RESULT_KEYSTORE_ALIAS): Intent {
+ require(requestedSecrets.isNotEmpty())
+ return Intent(context, SharedSecureStorageActivity::class.java).also {
+ it.putExtra(MvRx.KEY_ARG, Args(
+ keyId,
+ requestedSecrets,
+ resultKeyStoreAlias
+ ))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt
new file mode 100644
index 0000000000..44ba52a8d3
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2020 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.riotx.features.crypto.quads
+
+import com.airbnb.mvrx.MvRx
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.listeners.ProgressListener
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
+import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
+import im.vector.riotx.R
+import im.vector.riotx.core.platform.VectorViewModel
+import im.vector.riotx.core.platform.WaitingViewData
+import im.vector.riotx.core.resources.StringProvider
+
+data class SharedSecureStorageViewState(
+ val requestedSecrets: List = emptyList(),
+ val passphraseVisible: Boolean = false
+) : MvRxState
+
+class SharedSecureStorageViewModel @AssistedInject constructor(
+ @Assisted initialState: SharedSecureStorageViewState,
+ private val stringProvider: StringProvider,
+ private val session: Session)
+ : VectorViewModel(initialState) {
+
+ @AssistedInject.Factory
+ interface Factory {
+ fun create(initialState: SharedSecureStorageViewState): SharedSecureStorageViewModel
+ }
+
+ override fun handle(action: SharedSecureStorageAction) = withState { state ->
+ when (action) {
+ is SharedSecureStorageAction.TogglePasswordVisibility -> {
+ setState {
+ copy(
+ passphraseVisible = !passphraseVisible
+ )
+ }
+ }
+ is SharedSecureStorageAction.Cancel -> {
+ _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
+ }
+ is SharedSecureStorageAction.SubmitPassphrase -> {
+ _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
+ val passphrase = action.passphrase
+ val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
+ if (!keyInfoResult.isSuccess()) {
+ _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
+ _viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
+ return@withState
+ }
+ val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
+
+ // TODO
+// val decryptedSecretMap = HashMap()
+// val errors = ArrayList()
+ _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
+ WaitingViewData(
+ message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
+ isIndeterminate = true
+ )
+ ))
+ state.requestedSecrets.forEach {
+ val keySpec = Curve25519AesSha2KeySpec.fromPassphrase(
+ passphrase,
+ keyInfo.content.passphrase?.salt ?: "",
+ keyInfo.content.passphrase?.iterations ?: 0,
+ // TODO
+ object : ProgressListener {
+ override fun onProgress(progress: Int, total: Int) {
+ _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
+ WaitingViewData(
+ message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
+ isIndeterminate = false,
+ progress = progress,
+ progressTotal = total
+ )
+ ))
+ }
+ }
+ )
+ session.sharedSecretStorageService.getSecret(
+ name = it,
+ keyId = keyInfo.id,
+ secretKey = keySpec,
+ callback = object : MatrixCallback {
+ override fun onSuccess(data: String) {
+ _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
+ }
+
+ override fun onFailure(failure: Throwable) {
+ _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
+ _viewEvents.post(SharedSecureStorageViewEvent.InlineError(failure.localizedMessage))
+ }
+ })
+ }
+ }
+ }
+ }
+
+ companion object : MvRxViewModelFactory {
+
+ @JvmStatic
+ override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? {
+ val activity: SharedSecureStorageActivity = viewModelContext.activity()
+ val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
+ return activity.viewModelFactory.create(
+ SharedSecureStorageViewState(
+ requestedSecrets = args.requestedSecrets
+ )
+ )
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
new file mode 100644
index 0000000000..58c24cf2e9
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2020 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.riotx.features.crypto.quads
+
+import android.os.Bundle
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.Button
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.TextView
+import butterknife.BindView
+import butterknife.OnClick
+import com.airbnb.mvrx.activityViewModel
+import com.airbnb.mvrx.withState
+import com.google.android.material.textfield.TextInputLayout
+import com.jakewharton.rxbinding3.widget.editorActionEvents
+import com.jakewharton.rxbinding3.widget.textChanges
+import im.vector.riotx.R
+import im.vector.riotx.core.extensions.showPassword
+import im.vector.riotx.core.platform.VectorBaseFragment
+import im.vector.riotx.core.utils.DebouncedClickListener
+import me.gujun.android.span.span
+import java.util.concurrent.TimeUnit
+
+class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
+
+ override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase
+
+ val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
+
+ @BindView(R.id.ssss_restore_with_passphrase_warning_text)
+ lateinit var warningText: TextView
+
+ @BindView(R.id.ssss_restore_with_passphrase_warning_reason)
+ lateinit var reasonText: TextView
+
+ @BindView(R.id.ssss_passphrase_enter_til)
+ lateinit var mPassphraseInputLayout: TextInputLayout
+
+ @BindView(R.id.ssss_passphrase_enter_edittext)
+ lateinit var mPassphraseTextEdit: EditText
+
+ @BindView(R.id.ssss_view_show_password)
+ lateinit var mPassphraseReveal: ImageView
+
+ @BindView(R.id.ssss_passphrase_submit)
+ lateinit var submitButton: Button
+
+ @BindView(R.id.ssss_passphrase_cancel)
+ lateinit var cancelButton: Button
+
+ @OnClick(R.id.ssss_view_show_password)
+ fun toggleVisibilityMode() {
+ sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ warningText.text = span {
+ span(getString(R.string.enter_secret_storage_passphrase_warning)) {
+ textStyle = "bold"
+ }
+ +" "
+ +getString(R.string.enter_secret_storage_passphrase_warning_text)
+ }
+
+ reasonText.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
+
+ mPassphraseTextEdit.editorActionEvents()
+ .throttleFirst(300, TimeUnit.MILLISECONDS)
+ .subscribe {
+ if (it.actionId == EditorInfo.IME_ACTION_DONE) {
+ submit()
+ }
+ }
+ .disposeOnDestroyView()
+
+ mPassphraseTextEdit.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
+ submit()
+ return@setOnEditorActionListener true
+ }
+ return@setOnEditorActionListener false
+ }
+
+ mPassphraseTextEdit.textChanges()
+ .subscribe {
+ mPassphraseInputLayout.error = null
+ submitButton.isEnabled = it.isNotBlank()
+ }
+ .disposeOnDestroyView()
+
+ sharedViewModel.observeViewEvents {
+ when (it) {
+ is SharedSecureStorageViewEvent.InlineError -> {
+ mPassphraseInputLayout.error = it.message
+ }
+ }
+ }
+
+ submitButton.setOnClickListener(DebouncedClickListener(
+ View.OnClickListener {
+ submit()
+ }
+ ))
+
+ cancelButton.setOnClickListener(DebouncedClickListener(
+ View.OnClickListener {
+ sharedViewModel.handle(SharedSecureStorageAction.Cancel)
+ }
+ ))
+ }
+
+ fun submit() {
+ val text = mPassphraseTextEdit.text.toString()
+ if (text.isBlank()) return // Should not reach this point as button disabled
+ submitButton.isEnabled = false
+ sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text))
+ }
+
+ override fun invalidate() = withState(sharedViewModel) { state ->
+ val shouldBeVisible = state.passphraseVisible
+ mPassphraseTextEdit.showPassword(shouldBeVisible)
+ mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
index 111328b0b1..15eba2d138 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt
@@ -29,4 +29,5 @@ sealed class VerificationAction : VectorViewModelAction {
data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
object GotItConclusion : VerificationAction()
object SkipVerification : VerificationAction()
+ object VerifyFromPassphrase : VerificationAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
index 554344b06f..0efa2a8480 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt
@@ -36,6 +36,7 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment
@@ -87,6 +88,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
viewModel.observeViewEvents {
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
+ is VerificationBottomSheetViewEvents.AccessSecretStore -> {
+ startActivity(SharedSecureStorageActivity.newIntent(requireContext(),null, listOf("m.cross_signing.user_signing")))
+ }
}.exhaustive
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
index 5509ecbe16..3a3bf54883 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt
@@ -23,4 +23,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
*/
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents()
+ object AccessSecretStore : VerificationBottomSheetViewEvents()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
index 7ae3cf29eb..981387abd7 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt
@@ -253,6 +253,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
is VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
+ is VerificationAction.VerifyFromPassphrase -> {
+ _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
+ }
}.exhaustive
}
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
index 0cc7a2beab..64000d07a1 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt
@@ -63,6 +63,7 @@ class VerificationRequestFragment @Inject constructor(
}
override fun onClickRecoverFromPassphrase() {
+ viewModel.handle(VerificationAction.VerifyFromPassphrase)
}
override fun onClickDismiss() {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
index 55e217670e..95cea89702 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt
@@ -148,9 +148,22 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
// We need to ask
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
- navigator.waitSessionVerification(this)
- } else {
- // TODO upgrade security -> bootstrap cross signing
+ PopupAlertManager.postVectorAlert(
+ PopupAlertManager.VectorAlert(
+ uid = "completeSecurity",
+ title = getString(R.string.new_signin),
+ description = getString(R.string.complete_security),
+ iconId = R.drawable.ic_shield_warning
+ ).apply {
+ colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_destructive_accent)
+ contentAction = Runnable {
+ (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
+ it.navigator.waitSessionVerification(it)
+ }
+ }
+ dismissedAction = Runnable {}
+ }
+ )
}
}
diff --git a/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml
new file mode 100644
index 0000000000..db50cfd34d
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/values/dimens.xml b/vector/src/main/res/values/dimens.xml
index 665d1819f7..411945c8ed 100644
--- a/vector/src/main/res/values/dimens.xml
+++ b/vector/src/main/res/values/dimens.xml
@@ -32,5 +32,6 @@
280dp
+ 16dp
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 6c2caedd01..31cd015051 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2151,4 +2151,5 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
No
Connectivity to the server has been lost
+ ShareSecureStorageActivity
diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml
index 082ce235bb..e6a2701e33 100644
--- a/vector/src/main/res/values/strings_riotX.xml
+++ b/vector/src/main/res/values/strings_riotX.xml
@@ -21,6 +21,15 @@
Can‘t access an existing session?
Use your recovery key or passphrase
+
+
+ New Sign In
+
+ Enter secret storage passphrase
+ Warning:
+ You should only access secret storage from a trusted device
+ Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase
+
diff --git a/vector/src/main/res/values/styles.xml b/vector/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..0bb90ea4d0
--- /dev/null
+++ b/vector/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+