diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt index 7f64d6db..b297c048 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt @@ -98,6 +98,7 @@ fun ExchangeListScreen( var addInProgress by remember { mutableStateOf(false) } var addErrorMessage by remember { mutableStateOf(null) } var exchangeToDelete by remember { mutableStateOf(null) } + var exchangeKeyChange by remember { mutableStateOf(null) } val addErrorText = stringResource(R.string.exchange_add_error) @@ -168,6 +169,8 @@ fun ExchangeListScreen( onAction = { action -> if (action == ExchangeAction.Delete) { exchangeToDelete = exchange + } else if (action == ExchangeAction.ReviewKeyChange) { + exchangeKeyChange = exchange } else { handleExchangeAction( scope = scope, @@ -202,6 +205,19 @@ fun ExchangeListScreen( ) } + exchangeKeyChange?.let { exchange -> + val keyChange = exchange.unconfirmedKeyChange + if (keyChange != null) ExchangeKeyChangeDialog( + exchange = exchange, + keyChange = keyChange, + onDismiss = { exchangeKeyChange = null }, + onConfirm = { + exchangeManager.confirmKeyChange(exchange.exchangeBaseUrl, keyChange.currentMasterPub) + exchangeKeyChange = null + }, + ) + } + if (exchangeToDelete != null) { DeleteExchangeDialog( onDismiss = { @@ -235,6 +251,13 @@ fun ExchangeItemComposable( stringResource(R.string.exchange_list_currency, it) } ?: stringResource(R.string.exchange_not_contacted) Text(text = currencyText, style = MaterialTheme.typography.bodyMedium) + if (exchange.unconfirmedKeyChange != null) { + Text( + text = stringResource(R.string.exchange_key_change_warning), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } } if (exchange.currency != null) { Box { @@ -268,6 +291,13 @@ fun ExchangeDropDownMenu( onAction: (ExchangeAction) -> Unit, ) { DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { + if (exchange.unconfirmedKeyChange != null) { + DropdownMenuItem( + text = { Text(stringResource(R.string.exchange_key_change_review)) }, + onClick = { onAction(ExchangeAction.ReviewKeyChange) } + ) + } + DropdownMenuItem( text = { Text(stringResource(R.string.exchange_reload)) }, onClick = { onAction(ExchangeAction.Reload) } @@ -408,7 +438,45 @@ fun DeleteExchangeDialog( ) } +@Composable +fun ExchangeKeyChangeDialog( + exchange: ExchangeItem, + keyChange: ExchangeKeyChangeInfo, + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.exchange_key_change_title)) }, + text = { + Column { + Text(stringResource(R.string.exchange_key_change_message, exchange.name)) + Text( + modifier = Modifier.padding(top = 16.dp), + text = stringResource( + R.string.exchange_key_change_keys, + keyChange.supersededMasterPub, + keyChange.currentMasterPub, + ), + style = MaterialTheme.typography.bodySmall, + ) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.exchange_key_change_accept)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + enum class ExchangeAction { + ReviewKeyChange, Reload, ViewTos, AcceptTos, @@ -427,6 +495,7 @@ fun handleExchangeAction( onNavigate: NavigateCallback, ) { when (action) { + ExchangeAction.ReviewKeyChange -> {} // dialog shown by ExchangeListScreen ExchangeAction.Reload -> exchangeManager.reload(exchange.exchangeBaseUrl) ExchangeAction.ViewTos -> { onNavigate(WalletDestination.ReviewExchangeTOS(exchange.exchangeBaseUrl, true), false) diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt index 5dfc8b62..7453b5ec 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -152,6 +152,21 @@ class ExchangeManager( } } + fun confirmKeyChange(exchangeUrl: String, currentMasterPub: String) = scope.launch { + mProgress.value = true + api.request("confirmExchangeKeyChange") { + put("exchangeBaseUrl", exchangeUrl) + put("currentMasterPub", currentMasterPub) + }.onError { + Log.e(TAG, "Error confirming key change: $it") + mProgress.value = false + mReloadError.value = it.toEvent() + }.onSuccess { + mProgress.value = false + list() + } + } + fun findExchangeForCurrency(currency: String): Flow = flow { emit(findExchangeByCurrency(currency)) } diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt index d110f044..ea1060f0 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt @@ -36,10 +36,18 @@ data class ExchangeItem( val paytoUris: List, val scopeInfo: ScopeInfo? = null, val tosStatus: ExchangeTosStatus = ExchangeTosStatus.Unknown, + /** set when the exchange's master key changed and the user has not confirmed it yet */ + val unconfirmedKeyChange: ExchangeKeyChangeInfo? = null, ) { val name: String get() = cleanExchange(exchangeBaseUrl) } +@Serializable +data class ExchangeKeyChangeInfo( + val currentMasterPub: String, + val supersededMasterPub: String, +) + @Serializable enum class ExchangeTosStatus { Unknown, diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 89750709..731ab51a 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -470,6 +470,12 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish Payment services Withdraw Payment service not contacted + Security identity changed, review it before using this payment service + Review identity change + Security alert + The security identity of %1$s has changed unexpectedly. Only trust the new identity if the payment service has confirmed this change. Otherwise, wait and try again later. + Previous key: %1$s\nCurrent key: %2$s + Accept new identity Reload information Manage the payment services known to this wallet Payment services