Skip to content

WAL-57 Fix crash on testnet identity duplication #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@

- An issue where signing a text message through WalletConnect did not work.
- An issue where a dApp could request to get a transaction signed by a different account than the one chosen for the WalletConnect session.
- Crashing when received unexpected error from an identity provider
- Exiting the wallet after accepting an identity verification error

[Unreleased]: https://github.com/Concordium/cryptox-android/compare/0.6.1-qa.5...HEAD
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,16 @@ class FailedActivity : BaseActivity(
private fun finishFlow() {
when (viewModel.source) {
FailedViewModel.Source.Identity -> {
finishAffinity()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
finishAffinity()
}
FailedViewModel.Source.Account -> {
finishAffinity()
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
finishAffinity()
}
FailedViewModel.Source.Transfer -> {
val intent = Intent(this, AccountDetailsActivity::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.concordium.wallet.ui.identity.identityproviderwebview

import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
Expand Down Expand Up @@ -32,8 +33,11 @@ import com.concordium.wallet.data.room.Identity
import com.concordium.wallet.data.room.Recipient
import com.concordium.wallet.data.room.WalletDatabase
import com.concordium.wallet.ui.common.BackendErrorHandler
import com.concordium.wallet.ui.passphrase.recoverprocess.retrofit.IdentityProviderApiInstance
import com.concordium.wallet.util.Log
import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.math.BigInteger

Expand All @@ -50,6 +54,10 @@ class IdentityProviderWebViewViewModel(application: Application) : AndroidViewMo
val errorLiveData: LiveData<Event<Int>>
get() = _errorLiveData

private val _handleIdentityVerificationUri = MutableLiveData<Event<Uri>>()
val handleIdentityVerificationUri: LiveData<Event<Uri>>
get() = _handleIdentityVerificationUri

private val _identityCreationError = MutableLiveData<Event<String>>()
val identityCreationError: LiveData<Event<String>>
get() = _identityCreationError
Expand Down Expand Up @@ -110,16 +118,31 @@ class IdentityProviderWebViewViewModel(application: Application) : AndroidViewMo
}
}

fun getIdentityProviderUrl(): String {
/**
* Upon finish, either [handleIdentityVerificationUri] or [identityCreationError]
* gets triggered.
*/
fun beginVerification() = viewModelScope.launch(Dispatchers.IO) {
_waitingLiveData.postValue(true)

val idObjectRequest = gson.toJson(IdentityRequest(identityCreationData.idObjectRequest))
val baseUrl = identityCreationData.identityProvider.metadata.issuanceStart
val delimiter = if (baseUrl.contains('?')) "&" else "?"
return if (BuildConfig.DEBUG && BuildConfig.ENV_NAME == "prod_testnet" && baseUrl.lowercase()
.contains("notabene")
)
"${baseUrl}${delimiter}response_type=code&redirect_uri=$CALLBACK_URL&scope=identity&state=$idObjectRequest&test_flow=1"
else
"${baseUrl}${delimiter}response_type=code&redirect_uri=$CALLBACK_URL&scope=identity&state=$idObjectRequest"
try {
val verificationRedirectUri = IdentityProviderApiInstance.getVerificationRedirectUri(
verificationStartUrl = baseUrl +
"${delimiter}response_type=code" +
"&redirect_uri=$CALLBACK_URL" +
"&scope=identity" +
"&state=$idObjectRequest",
redirectUriScheme = BuildConfig.SCHEME,
)
_handleIdentityVerificationUri.postValue(Event(Uri.parse(verificationRedirectUri)))
} catch (error: Exception) {
_identityCreationError.postValue(Event(error.message ?: error.toString()))
} finally {
_waitingLiveData.postValue(false)
}
}

private fun saveNewIdentity(identityObject: IdentityObject) {
Expand Down Expand Up @@ -204,8 +227,14 @@ class IdentityProviderWebViewViewModel(application: Application) : AndroidViewMo
}

fun parseIdentityError(errorContent: String) {
val error: Map<String, Any> =
val error: Map<String, Any> = try {
gson.fromJson(errorContent, object : TypeToken<Map<String, Any>>() {}.type)
} catch (jsonException: JsonParseException) {
Log.e("Unexpected identity error content", jsonException)
_identityCreationError.value = Event(errorContent)
return
}

val map = error["error"] as Map<*, *>
val event = Event(map["detail"].toString())
if (map["code"]!! == "USER_CANCEL") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ class IdentityProviderWebviewActivity : BaseActivity(
}

// Have to keep this intent data in case the Activity is force killed while on the IdentityProvider website
private inner class IdentityDataPreferences(context: Context, preferenceName: String, preferenceMode: Int) :
private inner class IdentityDataPreferences(
context: Context,
preferenceName: String,
preferenceMode: Int
) :
Preferences(context, preferenceName, preferenceMode) {
private inner class StoredIdentityCreationData(
val version: Int,
Expand Down Expand Up @@ -162,7 +166,7 @@ class IdentityProviderWebviewActivity : BaseActivity(
viewModel.initialize(tempData)
initViews()
if (!viewModel.useTemporaryBackend) {
showChromeCustomTab(viewModel.getIdentityProviderUrl())
viewModel.beginVerification()
}
}
}
Expand Down Expand Up @@ -202,6 +206,15 @@ class IdentityProviderWebviewActivity : BaseActivity(
showError(value)
}
})
viewModel.handleIdentityVerificationUri.observe(this, object : EventObserver<Uri>() {
override fun onUnhandledEvent(value: Uri) {
if (hasValidCallbackUri(value)) {
handleNewIntentData(value, null)
} else {
showChromeCustomTab(value.toString())
}
}
})
viewModel.identityCreationError.observe(this, object : EventObserver<String>() {
override fun onUnhandledEvent(value: String) {
val error = BackendError(0, value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,122 @@
package com.concordium.wallet.ui.passphrase.recoverprocess.retrofit

import android.net.Uri
import com.concordium.wallet.BuildConfig
import com.concordium.wallet.data.model.RecoverErrorResponse
import com.concordium.wallet.data.model.RecoverResponse
import com.concordium.wallet.util.Log
import com.google.gson.Gson
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create

class IdentityProviderApiInstance {
companion object {
private val retrofit by lazy {
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
.addInterceptor(logging)
.build()
Retrofit.Builder()
.baseUrl("https://some.api.url/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
object IdentityProviderApiInstance {
private val httpClient: OkHttpClient by lazy {
val logging = HttpLoggingInterceptor()
logging.setLevel(
if (BuildConfig.DEBUG)
HttpLoggingInterceptor.Level.BODY
else
HttpLoggingInterceptor.Level.NONE
)
OkHttpClient.Builder()
.addInterceptor(logging)
.build()
}

private val api: IdentityProviderApi by lazy {
retrofit.create(IdentityProviderApi::class.java)
}
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://some.api.url/")
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient)
.build()
}

suspend fun safeRecoverCall(url: String?): Pair<Boolean, RecoverResponse?> {
try {
val response = api.recover(url)
if (response.isSuccessful) {
response.body()?.let {
if (it.value != null)
return Pair(true, it)
else
return Pair(true, null)
}
private val api: IdentityProviderApi by lazy(retrofit::create)

suspend fun safeRecoverCall(url: String?): Pair<Boolean, RecoverResponse?> {
try {
val response = api.recover(url)
if (response.isSuccessful) {
response.body()?.let {
if (it.value != null)
return Pair(true, it)
else
return Pair(true, null)
}
} else {
return if (response.errorBody() != null && response.code() == 404) {
val errorResponse: RecoverErrorResponse = Gson().fromJson(
response.errorBody()!!.charStream(),
RecoverErrorResponse::class.java
)
Log.d("${errorResponse.code} ${errorResponse.message} on $url")
Pair(true, null)
} else {
return if (response.errorBody() != null && response.code() == 404) {
val errorResponse: RecoverErrorResponse = Gson().fromJson(
response.errorBody()!!.charStream(),
RecoverErrorResponse::class.java
)
Log.d("${errorResponse.code} ${errorResponse.message} on $url")
Pair(true, null)
} else {
Pair(false, null)
}
Pair(false, null)
}
} catch (t: Throwable) {
Log.d(Log.toString(t))
}
return Pair(false, null)
} catch (t: Throwable) {
Log.d(Log.toString(t))
}
return Pair(false, null)
}

/**
* When the ID verification start URL is built, it must be requested
* internally before passing to a browser. The final URI will be obtained
* after following all the redirects. It will help to detect errors
* such as idCredPub duplication faster.
*
* @param verificationStartUrl a URL crafted to start the verification process
* @param redirectUriScheme a scheme which the app expects for result redirect
*
* @return a URL to proceed with the verification or a URI with [redirectUriScheme]
* if the result is available instantly.
*/
fun getVerificationRedirectUri(
verificationStartUrl: String,
redirectUriScheme: String,
): String =
httpClient
.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build()
.newCall(
Request.Builder()
.url(verificationStartUrl)
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
)
.execute()
.use { response ->
check(response.isRedirect) {
"The identity provider did not redirect as expected"
}

val redirectLocation = response.header("Location")
?: error("Can't find the location in the identity provider response")

return@use if (Uri.parse(redirectLocation).scheme in setOf(
"http",
"https",
redirectUriScheme,
)
) {
// Return the location if it is a URI with an allowed scheme:
// HTTP or HTTPS to proceed with the verification on a web page,
// or the app redirect URI scheme to get the instant result.
redirectLocation
} else {
// Otherwise try to resolve the location against the base URL
// in case it is a relative path.
response.request.url.resolve(redirectLocation)?.toString()
?: error("Can't resolve the redirect: $redirectLocation")
}
}
}