Skip to content

Commit 08f7882

Browse files
committed
make tor requirements for custom electrum servers opt-out
When tor is enabled, Phoenix will require electrum servers to use an onion address. This way, if the Tor VPN fails, connection will not be established. However, some users may want to opt-out, which is valid if they are connecting to their own server and don't mind connecting to it on clearnet.
1 parent 5866e87 commit 08f7882

File tree

12 files changed

+109
-77
lines changed

12 files changed

+109
-77
lines changed

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ fun AppView(
367367
DisplaySeedView()
368368
}
369369
composable(Screen.ElectrumServer.route) {
370-
ElectrumView()
370+
ElectrumView(onBackClick = { navController.popBackStack() })
371371
}
372372
composable(Screen.TorConfig.route) {
373373
TorConfigView(appViewModel = appVM, onBackClick = { navController.popBackStack() }, onBusinessTeardown = { navController.popToHome() })

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/ConnectionDialog.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ fun ConnectionDialog(
7171
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp)
7272
)
7373
} else {
74+
val isTorEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null).value
7475
val hasConnectionIssues = connections.electrum != Connection.ESTABLISHED || connections.peer != Connection.ESTABLISHED
7576
if (hasConnectionIssues) {
7677
Text(text = stringResource(id = R.string.conndialog_summary_not_ok), Modifier.padding(horizontal = 24.dp))
@@ -87,7 +88,7 @@ fun ConnectionDialog(
8788
}
8889
else -> {
8990
val customElectrumServer by userPrefs.getElectrumServer.collectAsState(initial = null)
90-
if (customElectrumServer?.isOnion == false) {
91+
if (isTorEnabled == true && customElectrumServer?.server?.isOnion == false && customElectrumServer?.requireOnionIfTorEnabled == true) {
9192
TextWithIcon(text = stringResource(R.string.conndialog_electrum_not_onion), textStyle = monoTypo, icon = R.drawable.ic_alert_triangle, iconTint = negativeColor)
9293
} else if (connection is Connection.CLOSED && connection.isBadCertificate()) {
9394
TextWithIcon(text = stringResource(R.string.conndialog_closed_bad_cert), textStyle = monoTypo, icon = R.drawable.ic_alert_triangle, iconTint = negativeColor)
@@ -102,7 +103,7 @@ fun ConnectionDialog(
102103
HSeparator()
103104
Spacer(Modifier.height(16.dp))
104105

105-
val isTorEnabled = userPrefs.getIsTorEnabled.collectAsState(initial = null).value
106+
106107
if (hasConnectionIssues && isTorEnabled == true) {
107108
Card(backgroundColor = mutedBgColor, modifier = Modifier.fillMaxWidth(), internalPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), onClick = onTorClick) {
108109
TextWithIcon(text = stringResource(id = R.string.conndialog_tor_disclaimer_title), icon = R.drawable.ic_tor_shield, textStyle = MaterialTheme.typography.body2)

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/home/HomeTopAndBottom.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ private fun ConnectionBadge(
152152
onClick = onConnectionsStateButtonClick,
153153
modifier = Modifier.alpha(connectionsButtonAlpha)
154154
)
155-
torEnabled.value == true && customElectrumServer?.isOnion == false -> TopBadgeButton(
155+
torEnabled.value == true && customElectrumServer?.server?.isOnion == false && customElectrumServer?.requireOnionIfTorEnabled == true -> TopBadgeButton(
156156
text = stringResource(id = R.string.home_connection_onion),
157157
icon = R.drawable.ic_tor_shield,
158158
iconTint = negativeColor,

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/settings/electrum/ElectrumView.kt

+55-34
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,17 @@ import java.text.DateFormat
5858
import java.text.NumberFormat
5959

6060
@Composable
61-
fun ElectrumView() {
62-
val nc = navController
61+
fun ElectrumView(
62+
onBackClick: () -> Unit
63+
) {
6364
val scope = rememberCoroutineScope()
6465
val userPrefs = userPrefs
65-
val electrumServerInPrefs by userPrefs.getElectrumServer.collectAsState(initial = null)
66+
val electrumConfigInPrefs by userPrefs.getElectrumServer.collectAsState(initial = null)
6667
var showCustomServerDialog by rememberSaveable { mutableStateOf(false) }
6768

6869
DefaultScreenLayout {
6970
DefaultScreenHeader(
70-
onBackClick = { nc.popBackStack() },
71+
onBackClick = onBackClick,
7172
title = stringResource(id = R.string.electrum_title),
7273
)
7374
Card(internalPadding = PaddingValues(16.dp)) {
@@ -78,11 +79,11 @@ fun ElectrumView() {
7879
val config = model.configuration
7980
if (showCustomServerDialog) {
8081
ElectrumServerDialog(
81-
initialAddress = electrumServerInPrefs,
82-
onConfirm = { address ->
82+
initialConfig = electrumConfigInPrefs,
83+
onConfirm = { newConfig ->
8384
scope.launch {
84-
userPrefs.saveElectrumServer(address)
85-
postIntent(ElectrumConfiguration.Intent.UpdateElectrumServer(address))
85+
userPrefs.saveElectrumServer(newConfig)
86+
postIntent(ElectrumConfiguration.Intent.UpdateElectrumServer(newConfig))
8687
showCustomServerDialog = false
8788
}
8889
},
@@ -119,7 +120,7 @@ fun ElectrumView() {
119120
text = stringResource(id = R.string.electrum_description_bad_certificate),
120121
style = MaterialTheme.typography.subtitle2.copy(color = negativeColor)
121122
)
122-
} else if (torEnabled.value == true && !config.server.isOnion) {
123+
} else if (torEnabled.value == true && !config.server.isOnion && config.requireOnionIfTorEnabled) {
123124
Text(
124125
text = stringResource(id = R.string.electrum_description_not_onion),
125126
style = MaterialTheme.typography.subtitle2.copy(color = negativeColor),
@@ -158,23 +159,25 @@ fun ElectrumView() {
158159

159160
@Composable
160161
private fun ElectrumServerDialog(
161-
initialAddress: ServerAddress?,
162-
onConfirm: (ServerAddress?) -> Unit,
162+
initialConfig: ElectrumConfig.Custom?,
163+
onConfirm: (ElectrumConfig.Custom?) -> Unit,
163164
onDismiss: () -> Unit
164165
) {
165166
val context = LocalContext.current
166167
val scope = rememberCoroutineScope()
167168
val keyboardManager = LocalSoftwareKeyboardController.current
168169

169-
var useCustomServer by rememberSaveable { mutableStateOf(initialAddress != null) }
170-
var address by rememberSaveable { mutableStateOf(initialAddress?.run { "$host:$port" } ?: "") }
170+
var useCustomServer by rememberSaveable { mutableStateOf(initialConfig != null) }
171+
var address by rememberSaveable { mutableStateOf(initialConfig?.run { "${server.host}:${server.port}" } ?: "") }
171172
val host = remember(address) { address.trim().substringBeforeLast(":").takeIf { it.isNotBlank() } }
172173
val port = remember(address) { address.trim().substringAfterLast(":").toIntOrNull() ?: 50002 }
173174
val isOnionHost = remember(address) { host?.endsWith(".onion") ?: false }
174175
val isTorEnabled by userPrefs.getIsTorEnabled.collectAsState(initial = null)
175176

176177
var addressError by rememberSaveable { mutableStateOf(false) }
177-
val showTorWithoutOnionError = remember(isOnionHost, isTorEnabled, useCustomServer) { useCustomServer && isTorEnabled == true && !isOnionHost }
178+
val showOnionOnCustomServerWarning = remember(isOnionHost, isTorEnabled, useCustomServer) { useCustomServer && isTorEnabled == true && !isOnionHost }
179+
var requireOnionIfTorEnabled by remember { mutableStateOf(initialConfig?.requireOnionIfTorEnabled ?: true) }
180+
val isViolatingTorRule = remember(isOnionHost, isTorEnabled, useCustomServer, requireOnionIfTorEnabled) { useCustomServer && isTorEnabled == true && !isOnionHost && requireOnionIfTorEnabled }
178181

179182
val vm = viewModel<ElectrumDialogViewModel>()
180183

@@ -204,7 +207,7 @@ private fun ElectrumServerDialog(
204207
modifier = Modifier.fillMaxWidth(),
205208
)
206209
}
207-
Spacer(modifier = Modifier.height(8.dp))
210+
Spacer(modifier = Modifier.height(12.dp))
208211
TextInput(
209212
modifier = Modifier.fillMaxWidth(),
210213
text = address,
@@ -218,24 +221,35 @@ private fun ElectrumServerDialog(
218221
maxLines = 4,
219222
staticLabel = stringResource(id = R.string.electrum_dialog_input),
220223
enabled = useCustomServer && vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking,
221-
errorMessage = if (addressError) stringResource(id = R.string.electrum_dialog_invalid_input) else if (showTorWithoutOnionError) stringResource(R.string.electrum_connection_dialog_tor_enabled) else null
224+
errorMessage = if (addressError) stringResource(id = R.string.electrum_dialog_invalid_input) else if (isViolatingTorRule) "Must use an onion address" else null
222225
)
223-
if (isTorEnabled == true || isOnionHost) {
224-
Spacer(modifier = Modifier.height(4.dp))
225-
TextWithIcon(
226-
text = stringResource(id = R.string.electrum_connection_dialog_onion_port),
227-
textStyle = MaterialTheme.typography.subtitle2,
228-
icon = R.drawable.ic_info,
229-
)
230-
} else {
231-
Spacer(modifier = Modifier.height(4.dp))
232-
TextWithIcon(
233-
text = stringResource(id = R.string.electrum_connection_dialog_tls_port),
234-
textStyle = MaterialTheme.typography.subtitle2,
235-
icon = R.drawable.ic_info,
236-
)
226+
227+
Spacer(modifier = Modifier.height(4.dp))
228+
TextWithIcon(
229+
text = stringResource(id = if (isOnionHost) R.string.electrum_connection_dialog_onion_port else R.string.electrum_connection_dialog_tls_port),
230+
textStyle = MaterialTheme.typography.subtitle2,
231+
icon = R.drawable.ic_info,
232+
modifier = Modifier.padding(horizontal = 16.dp)
233+
)
234+
235+
if (showOnionOnCustomServerWarning) {
236+
Spacer(modifier = Modifier.height(16.dp))
237+
Surface(color = mutedBgColor, shape = RoundedCornerShape(16.dp)) {
238+
Column(modifier = Modifier.fillMaxWidth()) {
239+
Text(text = stringResource(id = R.string.electrum_connection_dialog_tor_enabled_warning), modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp))
240+
Checkbox(
241+
text = stringResource(id = R.string.electrum_connection_dialog_tor_enabled_ignore_box),
242+
checked = !requireOnionIfTorEnabled,
243+
onCheckedChange = { requireOnionIfTorEnabled = !it },
244+
padding = PaddingValues(16.dp),
245+
enabled = useCustomServer && vm.state !is ElectrumDialogViewModel.CertificateCheckState.Checking,
246+
modifier = Modifier.fillMaxWidth(),
247+
)
248+
}
249+
}
237250
}
238251
}
252+
239253
Spacer(modifier = Modifier.height(12.dp))
240254
when (val state = vm.state) {
241255
ElectrumDialogViewModel.CertificateCheckState.Init, is ElectrumDialogViewModel.CertificateCheckState.Failure -> {
@@ -268,9 +282,9 @@ private fun ElectrumServerDialog(
268282
if (address.matches("""(.*):*(\d*)""".toRegex()) && host != null) {
269283
scope.launch {
270284
if (isOnionHost) {
271-
onConfirm(ServerAddress(host, port, TcpSocket.TLS.DISABLED))
285+
onConfirm(ElectrumConfig.Custom.create(ServerAddress(host, port, TcpSocket.TLS.DISABLED), requireOnionIfTorEnabled = requireOnionIfTorEnabled))
272286
} else {
273-
vm.checkCertificate(host, port, onCertificateValid = onConfirm)
287+
vm.checkCertificate(host, port, onCertificateValid = { onConfirm(ElectrumConfig.Custom.create(it, requireOnionIfTorEnabled = requireOnionIfTorEnabled)) })
274288
}
275289
}
276290
} else {
@@ -280,7 +294,7 @@ private fun ElectrumServerDialog(
280294
onConfirm(null)
281295
}
282296
},
283-
enabled = !addressError && !showTorWithoutOnionError,
297+
enabled = !addressError && !isViolatingTorRule,
284298
shape = RoundedCornerShape(16.dp),
285299
)
286300
}
@@ -348,7 +362,14 @@ private fun ElectrumServerDialog(
348362
)
349363
Button(
350364
text = stringResource(id = R.string.electrum_dialog_cert_accept),
351-
onClick = { onConfirm(ServerAddress(state.host, state.port, TcpSocket.TLS.PINNED_PUBLIC_KEY(Base64.encodeToString(cert.publicKey.encoded, Base64.NO_WRAP)))) },
365+
onClick = {
366+
onConfirm(
367+
ElectrumConfig.Custom.create(
368+
server = ServerAddress(state.host, state.port, TcpSocket.TLS.PINNED_PUBLIC_KEY(Base64.encodeToString(cert.publicKey.encoded, Base64.NO_WRAP))),
369+
requireOnionIfTorEnabled = requireOnionIfTorEnabled
370+
)
371+
)
372+
},
352373
icon = R.drawable.ic_check_circle,
353374
iconTint = positiveColor,
354375
space = 8.dp,

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/LegacyMigrationHelper.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import fr.acinq.lightning.wire.LiquidityAds
4444
import fr.acinq.phoenix.android.PhoenixApplication
4545
import fr.acinq.phoenix.android.utils.datastore.HomeAmountDisplayMode
4646
import fr.acinq.phoenix.data.BitcoinUnit
47+
import fr.acinq.phoenix.data.ElectrumConfig
4748
import fr.acinq.phoenix.data.FiatCurrency
4849
import fr.acinq.phoenix.data.WalletPaymentMetadata
4950
import fr.acinq.phoenix.data.lnurl.LnurlAuth
@@ -122,7 +123,7 @@ object LegacyMigrationHelper {
122123
Prefs.getElectrumServer(context).takeIf { it.isNotBlank() }?.let {
123124
val hostPort = HostAndPort.fromString(it).withDefaultPort(50002)
124125
// TODO: handle onion addresses and TOR
125-
ServerAddress(hostPort.host, hostPort.port, TcpSocket.TLS.TRUSTED_CERTIFICATES())
126+
ElectrumConfig.Custom.create(ServerAddress(hostPort.host, hostPort.port, TcpSocket.TLS.TRUSTED_CERTIFICATES()), false)
126127
}?.let {
127128
userPrefs.saveElectrumServer(it)
128129
appConfigurationManager.updateElectrumConfig(it)

phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/utils/datastore/UserPrefsRepository.kt

+13-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import fr.acinq.lightning.utils.msat
2929
import fr.acinq.lightning.utils.sat
3030
import fr.acinq.phoenix.android.utils.UserTheme
3131
import fr.acinq.phoenix.data.BitcoinUnit
32+
import fr.acinq.phoenix.data.ElectrumConfig
3233
import fr.acinq.phoenix.data.FiatCurrency
3334
import fr.acinq.phoenix.data.lnurl.LnurlAuth
3435
import fr.acinq.phoenix.db.migrations.v10.json.SatoshiSerializer
@@ -72,6 +73,7 @@ class UserPrefsRepository(private val data: DataStore<Preferences>) {
7273
// electrum
7374
val PREFS_ELECTRUM_ADDRESS_HOST = stringPreferencesKey("PREFS_ELECTRUM_ADDRESS_HOST")
7475
val PREFS_ELECTRUM_ADDRESS_PORT = intPreferencesKey("PREFS_ELECTRUM_ADDRESS_PORT")
76+
val PREFS_ELECTRUM_ADDRESS_REQUIRE_ONION_IF_TOR_ENABLED = booleanPreferencesKey("PREFS_ELECTRUM_ADDRESS_REQUIRE_ONION_IF_TOR_ENABLED")
7577
val PREFS_ELECTRUM_ADDRESS_PINNED_KEY = stringPreferencesKey("PREFS_ELECTRUM_ADDRESS_PINNED_KEY")
7678
// access control
7779
val PREFS_SCREEN_LOCK_BIOMETRICS = booleanPreferencesKey("PREFS_SCREEN_LOCK")
@@ -123,29 +125,32 @@ class UserPrefsRepository(private val data: DataStore<Preferences>) {
123125
val getHideBalance: Flow<Boolean> = safeData.map { it[HIDE_BALANCE] ?: false }
124126
suspend fun saveHideBalance(hideBalance: Boolean) = data.edit { it[HIDE_BALANCE] = hideBalance }
125127

126-
val getElectrumServer: Flow<ServerAddress?> = safeData.map {
128+
val getElectrumServer: Flow<ElectrumConfig.Custom?> = safeData.map {
127129
val host = it[PREFS_ELECTRUM_ADDRESS_HOST]?.takeIf { it.isNotBlank() }
128130
val port = it[PREFS_ELECTRUM_ADDRESS_PORT]
131+
val requireOnionIfTorEnabled = it[PREFS_ELECTRUM_ADDRESS_REQUIRE_ONION_IF_TOR_ENABLED] ?: true
129132
val pinnedKey = it[PREFS_ELECTRUM_ADDRESS_PINNED_KEY]?.takeIf { it.isNotBlank() }
130133
log.debug("retrieved electrum address from datastore, host=$host port=$port key=$pinnedKey")
131134
if (host != null && port != null && pinnedKey == null) {
132-
ServerAddress(host, port, TcpSocket.TLS.TRUSTED_CERTIFICATES())
135+
ElectrumConfig.Custom.create(ServerAddress(host, port, TcpSocket.TLS.TRUSTED_CERTIFICATES()), requireOnionIfTorEnabled)
133136
} else if (host != null && port != null && pinnedKey != null) {
134-
ServerAddress(host, port, TcpSocket.TLS.PINNED_PUBLIC_KEY(pinnedKey))
137+
ElectrumConfig.Custom.create(ServerAddress(host, port, TcpSocket.TLS.PINNED_PUBLIC_KEY(pinnedKey)), requireOnionIfTorEnabled)
135138
} else {
136139
null
137140
}
138141
}
139142

140-
suspend fun saveElectrumServer(address: ServerAddress?) = data.edit {
141-
if (address == null) {
143+
suspend fun saveElectrumServer(config: ElectrumConfig.Custom?) = data.edit {
144+
if (config == null) {
142145
it.remove(PREFS_ELECTRUM_ADDRESS_HOST)
143146
it.remove(PREFS_ELECTRUM_ADDRESS_PORT)
144147
it.remove(PREFS_ELECTRUM_ADDRESS_PINNED_KEY)
148+
it.remove(PREFS_ELECTRUM_ADDRESS_REQUIRE_ONION_IF_TOR_ENABLED)
145149
} else {
146-
it[PREFS_ELECTRUM_ADDRESS_HOST] = address.host
147-
it[PREFS_ELECTRUM_ADDRESS_PORT] = address.port
148-
val tls = address.tls
150+
it[PREFS_ELECTRUM_ADDRESS_HOST] = config.server.host
151+
it[PREFS_ELECTRUM_ADDRESS_PORT] = config.server.port
152+
it[PREFS_ELECTRUM_ADDRESS_REQUIRE_ONION_IF_TOR_ENABLED] = config.requireOnionIfTorEnabled
153+
val tls = config.server.tls
149154
if (tls is TcpSocket.TLS.PINNED_PUBLIC_KEY) {
150155
it[PREFS_ELECTRUM_ADDRESS_PINNED_KEY] = tls.pubKey
151156
} else {

phoenix-android/src/main/res/values/strings.xml

+3-2
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,8 @@
658658
<string name="electrum_block_height_label">Block height</string>
659659
<string name="electrum_connection_dialog_tls_port">Use the TLS port (default 50002).</string>
660660
<string name="electrum_connection_dialog_onion_port">For onion services, use the plain TCP port, not the TLS one.</string>
661-
<string name="electrum_connection_dialog_tor_enabled">Must be an onion address</string>
661+
<string name="electrum_connection_dialog_tor_enabled_warning">Since you\'ve enabled Tor, you should use an onion address for this server.</string>
662+
<string name="electrum_connection_dialog_tor_enabled_ignore_box">No, I don\'t want to use an onion address</string>
662663

663664
<string name="electrum_connection_closed_with_random">Disconnected from Electrum</string>
664665
<string name="electrum_connection_closed_with_custom">Disconnected from %1$s</string>
@@ -668,7 +669,7 @@
668669

669670
<string name="electrum_description_custom">You are using a custom server</string>
670671
<string name="electrum_description_bad_certificate">This server provided an unknown certificate. Connection is rejected.</string>
671-
<string name="electrum_description_not_onion">Tor is enabled. This server must use an onion address.</string>
672+
<string name="electrum_description_not_onion">Tor is enabled. This server should use an onion address.</string>
672673

673674
<string name="electrum_dialog_checkbox">Use a custom server</string>
674675
<string name="electrum_dialog_input">Server address (host:port)</string>

phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/ElectrumConfiguration.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ object ElectrumConfiguration {
2121
}
2222

2323
sealed class Intent : MVI.Intent() {
24-
data class UpdateElectrumServer(val server: ServerAddress?) : Intent()
24+
data class UpdateElectrumServer(val config: ElectrumConfig.Custom?) : Intent()
2525
}
2626
}

0 commit comments

Comments
 (0)