Better isolate node vs lws connections
What changed, and why it matters
This commit fixes a Monero wallet bug where the app could accidentally save a private 'view key' to a regular Monero node instead of a lightweight wallet server (LWS). The fix separates saved server addresses for the two connection modes so that switching modes doesn't leave the wrong server address in the settings field. If the wrong address were saved, the user's private view key could be exposed to a node operator, allowing them to see all incoming transactions for that wallet.
Users should update to the patched version and review any saved connection settings to ensure LWS connections are not pointing at Monero node addresses. If a view key may have been sent to an untrusted node, consider rotating to a new wallet.
Security signals we found
Private key exposure risk (Monero view key)
UI state cross-contamination between connection modes
Privacy degradation for all transactions associated with the wallet
Fix is preventive/isolation rather than cryptographic
Evidence from the diff
The patch adds per-connection-type persistence in the wallet adapter and updates the connection settings form to load the saved server for the selected mode. Previously, switching between ‘node’ and ‘lws’ modes could leave the previous mode’s address in the address field; saving that configuration would then point an LWS-mode connection at a Monero node. In Monero, LWS uses a private view key to scan transactions, while a full node does not need this key. Sending the view key to a node would leak it to the node operator, breaking transaction privacy for the wallet.
Changed components
lib/models/app_wallet.dartlib/models/monero_wallet_adapter.dartlib/widgets/connection_settings_form.dartInspect captured patch +47 / −0
diff --git a/lib/models/app_wallet.dart b/lib/models/app_wallet.dart
index 96f962e..1eae75e 100644
--- a/lib/models/app_wallet.dart
+++ b/lib/models/app_wallet.dart
@@ -56,6 +56,13 @@ abstract interface class AppWallet implements Listenable {
// Connection ops
Future<LWSConnectionDetails> getPersistedConnection();
+
+ /// The server saved for [type], whichever type is currently active.
+ ///
+ /// Servers are stored per connection type, so the setup form can show the
+ /// server belonging to the mode being selected instead of leaving the other
+ /// mode's address in the field.
+ Future<LWSConnectionDetails> getPersistedConnectionForType(String type);
void setConnection({
required String address,
required String proxyPort,
diff --git a/lib/models/monero_wallet_adapter.dart b/lib/models/monero_wallet_adapter.dart
index 23fa764..0d11c21 100644
--- a/lib/models/monero_wallet_adapter.dart
+++ b/lib/models/monero_wallet_adapter.dart
@@ -131,6 +131,18 @@ class MoneroWalletAdapter extends ChangeNotifier implements AppWallet {
);
}
+ @override
+ Future<LWSConnectionDetails> getPersistedConnectionForType(String type) async {
+ final c = await _wallet.getPersistedConnectionForType(type);
+ return LWSConnectionDetails(
+ address: c.address,
+ proxyPort: c.proxyPort,
+ useTor: c.useTor,
+ useSsl: _deriveSsl(c.address),
+ connectionType: c.connectionType,
+ );
+ }
+
@override
void setConnection({
required String address,
diff --git a/lib/widgets/connection_settings_form.dart b/lib/widgets/connection_settings_form.dart
index 925b666..5f13ce7 100644
--- a/lib/widgets/connection_settings_form.dart
+++ b/lib/widgets/connection_settings_form.dart
@@ -256,6 +256,34 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
_errorMessage = null;
});
if (value == 'node' && Platform.isAndroid) _loadSyncPrefs();
+ _loadConnectionForType(value);
+ }
+
+ /// Swaps the fields to the server saved for [type].
+ ///
+ /// Servers are stored per connection type, so selecting a mode selects that
+ /// mode's server. Leaving the previous mode's address in the field is how an
+ /// LWS connection could be saved pointing at a Monero node, which would hand
+ /// the node the private view key.
+ ///
+ /// A mode with nothing saved clears the field rather than inheriting the other
+ /// one's address. `_initial*` is deliberately not updated: switching mode is a
+ /// change, so Save stays gated on a passing test.
+ Future<void> _loadConnectionForType(String type) async {
+ final wallet = appWalletOf(context);
+ final conn = await wallet.getPersistedConnectionForType(type);
+ // The user may have tapped another type while this was in flight.
+ if (!mounted || _connectionType != type) return;
+
+ final useTor = conn.useTor && TorSettingsService.sharedInstance.torMode != TorMode.disabled;
+ setState(() {
+ _addressController.text = conn.address;
+ _customProxyPortController.text = conn.proxyPort;
+ _useTor = useTor;
+ _hasTested = false;
+ _connectionSuccess = false;
+ _errorMessage = null;
+ });
}
String _connectionTypeLabel(AppLocalizations i18n, String type) {
Why this scored 72/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.