What changed, and why it matters
This commit adds a feature that checks whether the user's chosen Monero light-wallet server supports subaddresses. Before this change, the app could show a subaddress even when the server did not support it, which could cause incoming transactions to be missed. The change also removes a stored user preference that always defaulted to showing the primary address, and instead auto-detects server capability and defaults to showing a subaddress when supported. The most notable security-relevant side effect is that the app now sends the wallet's secret view key to the light-wallet server during the check, and it does so over plain HTTP unless the user has enabled SSL/Tor. That is a privacy-sensitive design choice, but it is consistent with how Monero light wallets already operate.
Treat this as a privacy-hardening review item rather than an urgent vulnerability. Verify that the /upsert_subaddrs endpoint and payload are consistent with the expected light-wallet server API, add response parsing/validation rather than relying only on status code 200, ensure the view key is never logged (the diff hides it, which is good), and consider warning users when connecting to a server over plain HTTP. Review whether a malicious server returning 200 can trick the wallet into defaulting to subaddresses while not actually indexing them, causing funds to be harder to recover.
Security signals we found
Secret view key transmitted to remote light-wallet server during capability probe
Probe uses http/https depending on user SSL setting; no mandatory encryption
Capability determined solely by HTTP 200 status, which a malicious or compromised server can fake
Default receive address changed from primary to subaddress when server claims support
Tor/SOCKS HTTP helper previously dropped request body; now correctly serializes body
No commit message or code comments describing this as a security fix
Evidence from the diff
The patch introduces WalletModel.checkSubaddressSupport(), which probes the configured light-wallet server by POSTing to /upsert_subaddrs with the primary address, secret view key, and a dummy subaddress list. It sets _serverSupportsSubaddresses based on an HTTP 200 response. The receive screen then uses this flag to decide whether to display a subaddress by default, show a toggle, or fall back to the primary address with a warning. The commit also fixes a bug in the Tor SOCKS helper: getRawHttpRequestString previously called jsonEncode on an already JSON-encoded string body, and makeSocksHttpRequest did not pass a body at all; now the body is forwarded and stringified once. The shared preference showReceiveSubaddress is removed, and the default _showSubaddress is changed from false to true. There is no evidence in the diff of input validation on the server response, certificate pinning, or mitigation for a malicious server that returns 200 to suppress subaddress warnings.
Changed components
lib/models/wallet_model.dartlib/screens/receive.dartlib/util/socks_http.dartlib/main.dartlib/screens/restore_wallet.dartInspect captured patch +203 / −125
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 007e530..daffbf7 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -60,11 +60,11 @@
"homeTransactionConfirmed": "Confirmed",
"homeNoTransactions": "No transactions",
"receiveTitle": "Receive",
- "receivePrimaryAddressWarn": "Warning: For better privacy, consider using subaddresses if supported by your light wallet server.",
- "receiveSubaddressWarn": "Warning: Make sure your light wallet server supports subaddresses, otherwise, you will not be able to see incoming transactions.",
+ "receivePrimaryAddressWarn": "Warning: Unless you know what you're doing, please consider using subaddresses for better privacy.",
"receiveShareButton": "Share",
"receiveShowSubaddressButton": "Show Subaddress",
"receiveShowPrimaryAddressButton": "Show Primary Address",
+ "receiveServerNoSubaddressesWarn": "Warning: This server does not support subaddresses. For better privacy, consider using a server that supports them. You are receiving to your primary address.",
"sendTitle": "Send",
"sendSendButton": "Send",
"sendTransactionSuccessfullySent": "Transaction successfully sent!",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 46ee40d..16295e6 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -461,15 +461,9 @@ abstract class AppLocalizations {
/// No description provided for @receivePrimaryAddressWarn.
///
/// In en, this message translates to:
- /// **'Warning: For better privacy, consider using subaddresses if supported by your light wallet server.'**
+ /// **'Warning: Unless you know what you\'re doing, please consider using subaddresses for better privacy.'**
String get receivePrimaryAddressWarn;
- /// No description provided for @receiveSubaddressWarn.
- ///
- /// In en, this message translates to:
- /// **'Warning: Make sure your light wallet server supports subaddresses, otherwise, you will not be able to see incoming transactions.'**
- String get receiveSubaddressWarn;
-
/// No description provided for @receiveShareButton.
///
/// In en, this message translates to:
@@ -488,6 +482,12 @@ abstract class AppLocalizations {
/// **'Show Primary Address'**
String get receiveShowPrimaryAddressButton;
+ /// No description provided for @receiveServerNoSubaddressesWarn.
+ ///
+ /// In en, this message translates to:
+ /// **'Warning: This server does not support subaddresses. For better privacy, consider using a server that supports them. You are receiving to your primary address.'**
+ String get receiveServerNoSubaddressesWarn;
+
/// No description provided for @sendTitle.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 420e2f5..34227ae 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -198,11 +198,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get receivePrimaryAddressWarn =>
- 'Warning: For better privacy, consider using subaddresses if supported by your light wallet server.';
-
- @override
- String get receiveSubaddressWarn =>
- 'Warning: Make sure your light wallet server supports subaddresses, otherwise, you will not be able to see incoming transactions.';
+ 'Warning: Unless you know what you\'re doing, please consider using subaddresses for better privacy.';
@override
String get receiveShareButton => 'Share';
@@ -213,6 +209,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get receiveShowPrimaryAddressButton => 'Show Primary Address';
+ @override
+ String get receiveServerNoSubaddressesWarn =>
+ 'Warning: This server does not support subaddresses. For better privacy, consider using a server that supports them. You are receiving to your primary address.';
+
@override
String get sendTitle => 'Send';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 9ea976d..d7f95d7 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -198,11 +198,7 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get receivePrimaryAddressWarn =>
- 'Aviso: Para maior privacidade, considere usar subendereços se o seu servidor de light wallet os suportar.';
-
- @override
- String get receiveSubaddressWarn =>
- 'Aviso: Certifique-se de que seu servidor de light wallet suporta subendereços, caso contrário, você não conseguirá ver as transações recebidas.';
+ 'Aviso: A menos que saiba o que está fazendo, por favor considere usar subendereços para melhor privacidade.';
@override
String get receiveShareButton => 'Compartilhar';
@@ -213,6 +209,10 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get receiveShowPrimaryAddressButton => 'Mostrar Endereço Primário';
+ @override
+ String get receiveServerNoSubaddressesWarn =>
+ 'Aviso: Este servidor não suporta subendereços. Para melhor privacidade, considere usar um servidor que os suporte. Você está recebendo no seu endereço primário.';
+
@override
String get sendTitle => 'Enviar';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 0509eb8..0f4fc56 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -60,11 +60,11 @@
"homeTransactionConfirmed": "Confirmado",
"homeNoTransactions": "Sem transações",
"receiveTitle": "Receber",
- "receivePrimaryAddressWarn": "Aviso: Para maior privacidade, considere usar subendereços se o seu servidor de light wallet os suportar.",
- "receiveSubaddressWarn": "Aviso: Certifique-se de que seu servidor de light wallet suporta subendereços, caso contrário, você não conseguirá ver as transações recebidas.",
+ "receivePrimaryAddressWarn": "Aviso: A menos que saiba o que está fazendo, por favor considere usar subendereços para melhor privacidade.",
"receiveShareButton": "Compartilhar",
"receiveShowSubaddressButton": "Mostrar Subendereço",
"receiveShowPrimaryAddressButton": "Mostrar Endereço Primário",
+ "receiveServerNoSubaddressesWarn": "Aviso: Este servidor não suporta subendereços. Para melhor privacidade, considere usar um servidor que os suporte. Você está recebendo no seu endereço primário.",
"sendTitle": "Enviar",
"sendSendButton": "Enviar",
"sendTransactionSuccessfullySent": "Transação enviada com sucesso!",
diff --git a/lib/main.dart b/lib/main.dart
index fe8817d..977dbb1 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -106,6 +106,7 @@ class MyApp extends StatelessWidget {
await wallet.refresh();
await wallet.loadAllStats();
await wallet.connectToDaemon();
+ await wallet.checkSubaddressSupport();
})();
}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 32a9ee0..0c927e8 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:monero/monero.dart' as monero;
import 'package:monero/src/monero.dart';
import 'package:monero/src/wallet2.dart';
+import 'package:http/http.dart' as http;
import 'package:skylight_wallet/consts.dart';
import 'package:skylight_wallet/services/shared_preferences_service.dart';
@@ -20,6 +21,7 @@ import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/util/formatting.dart';
import 'package:skylight_wallet/util/height.dart';
import 'package:skylight_wallet/util/logging.dart';
+import 'package:skylight_wallet/util/socks_http.dart';
import 'package:skylight_wallet/util/wallet.dart';
import 'package:skylight_wallet/util/wallet_password.dart';
@@ -145,6 +147,7 @@ class WalletModel with ChangeNotifier {
double? _unlockedBalance;
double? _totalBalance;
List<TxDetails> _txHistory = [];
+ bool? _serverSupportsSubaddresses;
Wallet2Wallet? get w2Wallet => _w2Wallet;
bool get hasAttemptedConnection => _hasAttemptedConnection;
@@ -155,15 +158,12 @@ class WalletModel with ChangeNotifier {
double? get totalBalance => _totalBalance;
List<TxDetails> get txHistory => _txHistory;
bool get usingTor => _connectionUseTor;
+ bool? get serverSupportsSubaddresses => _serverSupportsSubaddresses;
WalletModel() {
_startTimers();
}
- void notifyListenersFromOutside() {
- notifyListeners();
- }
-
void _startTimers() {
Timer.periodic(Duration(seconds: 1), (timer) {
_runCheckConnectionTimerTask();
@@ -402,6 +402,84 @@ class WalletModel with ChangeNotifier {
}
}
+ Future<void> checkSubaddressSupport() async {
+ final protocol = _connectionUseSsl ? 'https' : 'http';
+ final url = Uri.parse('$protocol://$_connectionAddress/upsert_subaddrs');
+ final primaryAddress = getPrimaryAddress();
+ final viewKey = _w2Wallet!.secretViewKey();
+ final subaddrs = [
+ {
+ "key": 0,
+ "value": [
+ [0, 1],
+ ],
+ },
+ ];
+ final getAll = false;
+
+ final body = json.encode({
+ 'address': primaryAddress,
+ 'view_key': viewKey,
+ 'subaddrs': subaddrs,
+ 'get_all': getAll,
+ });
+
+ log(LogLevel.info, 'Checking subaddress support:');
+ log(LogLevel.info, ' url: $url');
+ log(LogLevel.info, ' primaryAddress: $primaryAddress');
+ log(LogLevel.info, ' viewKey: <hidden>');
+ log(LogLevel.info, ' subaddrs: $subaddrs');
+ log(LogLevel.info, ' getAll: $getAll');
+
+ var httpStatus = 0;
+
+ for (int i = 0; i < 3; i++) {
+ try {
+ if (_connectionUseTor) {
+ await TorService.sharedInstance.waitUntilConnected();
+ final proxyInfo = TorService.sharedInstance.getProxyInfo();
+ final response = await makeSocksHttpRequest(
+ 'POST',
+ url.toString(),
+ proxyInfo,
+ body: body,
+ ).timeout(Duration(seconds: 20));
+
+ httpStatus = response.statusCode;
+ } else {
+ final response = await http
+ .post(
+ url,
+ headers: {'Content-Type': 'application/json'},
+ body: body,
+ )
+ .timeout(Duration(seconds: 5));
+
+ httpStatus = response.statusCode;
+ }
+
+ break;
+ } catch (e) {
+ if (i == 2) {
+ log(
+ LogLevel.warn,
+ 'Failed to check subaddress support after ${i + 1} attempts.',
+ );
+ log(LogLevel.warn, 'Error: $e');
+ }
+ }
+ }
+
+ _serverSupportsSubaddresses = httpStatus == 200;
+
+ log(
+ LogLevel.info,
+ 'Subaddress support check result: $_serverSupportsSubaddresses (status: $httpStatus)',
+ );
+
+ notifyListeners();
+ }
+
Future<void> refresh() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
final historyFfiAddr = _w2TxHistory!.ffiAddress();
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index cf69e08..ff04e92 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
-import 'package:skylight_wallet/services/shared_preferences_service.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
@@ -16,18 +15,16 @@ class ReceiveScreen extends StatefulWidget {
}
class _ReceiveScreenState extends State<ReceiveScreen> {
- String _address = '';
- bool _showSubaddress = false;
- double _previousBrightness = 0.0;
+ var _primaryAddress = '';
+ var _subaddress = '';
+ var _showSubaddress = true;
+ var _previousBrightness = 0.0;
@override
void initState() {
super.initState();
- final wallet = Provider.of<WalletModel>(context, listen: false);
- _address = wallet.getPrimaryAddress();
-
- _loadShowReceiveSubaddress();
+ _loadAddresses();
_setBrightnessToMax();
}
@@ -37,37 +34,22 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
super.dispose();
}
- Future<void> _loadShowReceiveSubaddress() async {
- final showReceiveSubaddress =
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.showReceiveSubaddress,
- ) ??
- false;
-
- _setShowSubaddress(showReceiveSubaddress);
- }
-
- Future<void> _setShowSubaddress(bool value) async {
+ Future<void> _loadAddresses() async {
final wallet = Provider.of<WalletModel>(context, listen: false);
- await SharedPreferencesService.set(
- SharedPreferencesKeys.showReceiveSubaddress,
- value,
- );
+ final primaryAddress = wallet.getPrimaryAddress();
+ final subaddress = await wallet.getUnusedSubaddress();
- final subddress = await wallet.getUnusedSubaddress();
-
- if (value) {
- setState(() {
- _showSubaddress = true;
- _address = subddress;
- });
- } else {
- setState(() {
- _showSubaddress = false;
- _address = wallet.getPrimaryAddress();
- });
- }
+ setState(() {
+ _primaryAddress = primaryAddress;
+ _subaddress = subaddress;
+ });
+ }
+
+ void _setShowSubaddress(bool value) {
+ setState(() {
+ _showSubaddress = value;
+ });
}
Future<void> _setBrightnessToMax() async {
@@ -86,73 +68,89 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final i18n = AppLocalizations.of(context)!;
final brightness = Theme.of(context).brightness;
final isDarkTheme = brightness == Brightness.dark;
+ final wallet = Provider.of<WalletModel>(context);
+ var address = '';
+
+ if (wallet.serverSupportsSubaddresses == false) {
+ address = _primaryAddress;
+ }
+
+ if (wallet.serverSupportsSubaddresses == true) {
+ address = _showSubaddress ? _subaddress : _primaryAddress;
+ }
return Scaffold(
appBar: AppBar(title: Text(i18n.receiveTitle)),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- QrImageView(
- data: _address,
- eyeStyle: QrEyeStyle(
- eyeShape: QrEyeShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- dataModuleStyle: QrDataModuleStyle(
- dataModuleShape: QrDataModuleShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- ),
- if (_showSubaddress)
- Text(
- i18n.receiveSubaddressWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- if (!_showSubaddress)
- Text(
- i18n.receivePrimaryAddressWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- GestureDetector(
- child: Text(
- _address,
- textAlign: TextAlign.center,
- style: TextStyle(fontFamily: 'monospace'),
- ),
- onTap: () async {
- await Clipboard.setData(ClipboardData(text: _address));
- },
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.icon(
- onPressed: () =>
- SharePlus.instance.share(ShareParams(text: _address)),
- icon: Icon(Icons.share),
- label: Text(i18n.receiveShareButton),
- ),
- if (!_showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(true),
- child: Text(i18n.receiveShowSubaddressButton),
+ child: wallet.serverSupportsSubaddresses == null
+ ? CircularProgressIndicator()
+ : Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ QrImageView(
+ data: address,
+ eyeStyle: QrEyeStyle(
+ eyeShape: QrEyeShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
+ dataModuleStyle: QrDataModuleStyle(
+ dataModuleShape: QrDataModuleShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
),
- if (_showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(false),
- child: Text(i18n.receiveShowPrimaryAddressButton),
+ if (wallet.serverSupportsSubaddresses == false)
+ Text(
+ i18n.receiveServerNoSubaddressesWarn,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.red),
+ ),
+ if (!_showSubaddress)
+ Text(
+ i18n.receivePrimaryAddressWarn,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.red),
+ ),
+ GestureDetector(
+ child: Text(
+ address,
+ textAlign: TextAlign.center,
+ style: TextStyle(fontFamily: 'monospace'),
+ ),
+ onTap: () async {
+ await Clipboard.setData(ClipboardData(text: address));
+ },
),
- ],
- ),
- ],
- ),
+ Row(
+ spacing: 20,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FilledButton.icon(
+ onPressed: () => SharePlus.instance.share(
+ ShareParams(text: address),
+ ),
+ icon: Icon(Icons.share),
+ label: Text(i18n.receiveShareButton),
+ ),
+ // Only show toggle button if server supports subaddresses
+ if (wallet.serverSupportsSubaddresses == true &&
+ !_showSubaddress)
+ TextButton(
+ onPressed: () => _setShowSubaddress(true),
+ child: Text(i18n.receiveShowSubaddressButton),
+ ),
+ if (wallet.serverSupportsSubaddresses == true &&
+ _showSubaddress)
+ TextButton(
+ onPressed: () => _setShowSubaddress(false),
+ child: Text(i18n.receiveShowPrimaryAddressButton),
+ ),
+ ],
+ ),
+ ],
+ ),
),
),
);
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index 7b61365..8538d47 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -54,6 +54,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
await wallet.refresh();
await wallet.loadAllStats();
await wallet.connectToDaemon();
+ wallet.checkSubaddressSupport();
setState(() {
_isLoading = false;
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index e0b571f..e3a0f28 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -12,7 +12,6 @@ class SharedPreferencesKeys {
static const String connectionProxyPort = 'connectionProxyPort';
static const String connectionUseTor = 'connectionUseTor';
static const String connectionUseSsl = 'connectionUseSsl';
- static const String showReceiveSubaddress = 'showReceiveSubaddress';
static const String walletRestoreHeight = 'walletRestoreHeight';
static const String txHistoryCount = 'txHistoryCount';
static const String pendingOutgoingTxs = 'pendingOutgoingTxs';
diff --git a/lib/util/socks_http.dart b/lib/util/socks_http.dart
index cee8988..52b5cf4 100644
--- a/lib/util/socks_http.dart
+++ b/lib/util/socks_http.dart
@@ -49,7 +49,7 @@ String getRawHttpRequestString(String method, String url, {Object? jsonBody}) {
request.write('Connection: close\r\n');
request.write('Accept: */*\r\n');
- final jsonBodyStr = jsonBody is Object ? jsonEncode(jsonBody) : null;
+ final jsonBodyStr = jsonBody is Object ? jsonBody.toString() : null;
if (jsonBodyStr != null && jsonBodyStr.isNotEmpty) {
final bodyBytes = utf8.encode(jsonBodyStr);
@@ -119,8 +119,9 @@ ParsedHttpResponse parseHttpResponse(String rawResponse) {
Future<ParsedHttpResponse> makeSocksHttpRequest(
String method,
String url,
- ({InternetAddress host, int port}) proxyInfo,
-) async {
+ ({InternetAddress host, int port}) proxyInfo, {
+ Object? body,
+}) async {
final uri = Uri.parse(url);
final socket = await SOCKSSocket.create(
@@ -132,7 +133,7 @@ Future<ParsedHttpResponse> makeSocksHttpRequest(
await socket.connect();
await socket.connectTo(uri.host, uri.port);
- final rawRequest = getRawHttpRequestString(method, url);
+ final rawRequest = getRawHttpRequestString(method, url, jsonBody: body);
final rawResponse = await socket.send(rawRequest);
final parsedResponse = parseHttpResponse(rawResponse);
Why this scored 37/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.