Improve toast and fix secure clipboard
What changed, and why it matters
This commit fixes a security bug in how the Skylight Wallet app copies sensitive information to the clipboard. Previously, some screens copied wallet addresses and secret keys using the regular system clipboard, which could leave that data exposed in the clipboard history. The patch makes those screens use the app's secure clipboard helper instead, which marks the data as sensitive and clears it automatically. It also adjusts Android 13+ so the app doesn't show a duplicate 'copied' confirmation when the operating system already shows one.
Verify that SecureClipboard.copy() correctly flags data as sensitive and clears it after a short timeout on both Android and iOS. Review whether any other screens still use the raw Clipboard API for wallet-related data. Consider adding a regression test to ensure sensitive copy operations always go through the secure path.
Security signals we found
Sensitive data (addresses, keys) was copied via the regular system clipboard on some screens
Patch routes those copies through SecureClipboard.copy(), which likely sets ClipDescription.EXTRA_IS_SENSITIVE and auto-clears
Android 13+ duplicate clipboard confirmation is suppressed to avoid UX leakage
No explicit CVE, advisory, or researcher attribution in the commit or supplied references
Evidence from the diff
The diff replaces direct Clipboard.setData() calls with SecureClipboard.copy() in receive.dart, lws_details.dart, and secret_keys.dart, ensuring addresses and keys are copied through the project’s secure clipboard path. The Android MainActivity now exposes a systemConfirmsCopy method that reports true on Android 13+ (TIRAMISU), so the Dart layer can suppress the in-app toast where the OS provides its own clipboard confirmation. Most other changes are cosmetic: replacing ScaffoldMessenger SnackBars with centralized showBrandToast/showCopyToast helpers across many screens. The secret_keys screen also removes the explicit sensitive:false flag for public keys, relying on default non-sensitive behavior.
Changed components
Android MainActivity secure clipboard channellib/screens/receive.dartlib/screens/lws_details.dartlib/screens/secret_keys.dartlib/util/secure_clipboard.dart (referenced, not shown in diff)Toast/notification presentation across multiple screensInspect captured patch +46 / −74
diff --git a/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt b/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
index d17cb4f..1646df8 100644
--- a/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/example/monero_light_wallet/MainActivity.kt
@@ -3,6 +3,7 @@ package org.magicgrants.skylight
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
+import android.os.Build
import android.os.PersistableBundle
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -21,6 +22,12 @@ class MainActivity : FlutterFragmentActivity() {
copySensitive(call.argument<String>("text") ?: "")
result.success(null)
}
+ // Android 13 shows its own clipboard confirmation, so the
+ // app must not add a second one. Older releases show
+ // nothing and still need the in-app toast.
+ "systemConfirmsCopy" -> {
+ result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+ }
else -> result.notImplemented()
}
}
diff --git a/lib/screens/address_book.dart b/lib/screens/address_book.dart
index 3a8cf84..97a9bc1 100644
--- a/lib/screens/address_book.dart
+++ b/lib/screens/address_book.dart
@@ -419,7 +419,7 @@ class _AddressRow extends StatelessWidget {
final i18n = AppLocalizations.of(context)!;
// Treat as sensitive (auto-cleared) like other address/key copies.
SecureClipboard.copy(contact.address);
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
+ showCopyToast(context, i18n.addressCopied);
}
@override
@@ -578,7 +578,7 @@ class _ContactSheetState extends State<_ContactSheet> {
if (mounted) Navigator.pop(context);
} catch (_) {
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ showBrandToast(context, i18n.unknownError);
setState(() => _saving = false);
}
}
diff --git a/lib/screens/create_wallet.dart b/lib/screens/create_wallet.dart
index c73e9fb..78d13b5 100644
--- a/lib/screens/create_wallet.dart
+++ b/lib/screens/create_wallet.dart
@@ -28,7 +28,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
final args = ModalRoute.of(context)?.settings.arguments as CreateWalletScreenArgs?;
if (args != null && args.toastMessage != '') {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(args.toastMessage)));
+ showBrandToast(context, args.toastMessage);
}
});
}
diff --git a/lib/screens/create_wallet_password.dart b/lib/screens/create_wallet_password.dart
index ca237c1..e8626ac 100644
--- a/lib/screens/create_wallet_password.dart
+++ b/lib/screens/create_wallet_password.dart
@@ -23,9 +23,7 @@ class _CreateWalletPasswordScreenState extends State<CreateWalletPasswordScreen>
}
} catch (e) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text('Failed to save password: $e')));
+ showBrandToast(context, 'Failed to save password: $e');
}
} finally {
if (mounted) {
diff --git a/lib/screens/generate_seed.dart b/lib/screens/generate_seed.dart
index 59ae757..485fb92 100644
--- a/lib/screens/generate_seed.dart
+++ b/lib/screens/generate_seed.dart
@@ -64,7 +64,7 @@ class _GenerateSeedScreenState extends State<GenerateSeedScreen> with SecureScre
} else {
log(LogLevel.error, error.toString());
}
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
+ showBrandToast(context, errorMsg);
}
}
diff --git a/lib/screens/lws_details.dart b/lib/screens/lws_details.dart
index ceb907f..5661a4c 100644
--- a/lib/screens/lws_details.dart
+++ b/lib/screens/lws_details.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -27,11 +27,11 @@ class _LwsDetailsScreenState extends State<LwsDetailsScreen> {
setState(() => _secretViewKey = key);
}
- void _copy(String value, {required bool sensitive}) {
+ void _copy(String value) {
if (value.isEmpty) return;
- Clipboard.setData(ClipboardData(text: value));
+ SecureClipboard.copy(value);
final i18n = AppLocalizations.of(context)!;
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ showCopyToast(context, i18n.copiedToClipboard);
}
@override
diff --git a/lib/screens/lws_keys.dart b/lib/screens/lws_keys.dart
index eaeea75..c219be3 100644
--- a/lib/screens/lws_keys.dart
+++ b/lib/screens/lws_keys.dart
@@ -36,11 +36,11 @@ class _LwsKeysScreenState extends State<LwsKeysScreen> with SecureScreenMixin {
});
}
- void _copy(String value, {required bool sensitive}) {
+ void _copy(String value) {
if (value.isEmpty) return;
SecureClipboard.copy(value);
final i18n = AppLocalizations.of(context)!;
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ showCopyToast(context, i18n.copiedToClipboard);
}
@override
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index c1a01e8..c2a30b5 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -1,12 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/app_wallet.dart';
+import 'package:skylight_wallet/util/secure_clipboard.dart';
import 'package:skylight_wallet/wallet_core_glue.dart';
import 'package:skylight_wallet/widgets/ui/ui.dart';
@@ -46,8 +46,8 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
void _copyAddressToClipboard(String address) {
final i18n = AppLocalizations.of(context)!;
- Clipboard.setData(ClipboardData(text: address));
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
+ SecureClipboard.copy(address);
+ showCopyToast(context, i18n.addressCopied);
}
@override
diff --git a/lib/screens/restore_wallet.dart b/lib/screens/restore_wallet.dart
index edb1f17..5989ec9 100644
--- a/lib/screens/restore_wallet.dart
+++ b/lib/screens/restore_wallet.dart
@@ -128,14 +128,14 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> with SecureSc
final message = errorMsg == 'Invalid mnemonic.'
? i18n.restoreWalletInvalidMnemonic
: i18n.unknownError;
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
+ showBrandToast(context, message);
}
return;
} catch (error) {
log(LogLevel.error, error.toString());
setState(() => _isLoading = false);
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ showBrandToast(context, i18n.unknownError);
}
return;
}
diff --git a/lib/screens/secret_keys.dart b/lib/screens/secret_keys.dart
index 60219b7..6fba6ca 100644
--- a/lib/screens/secret_keys.dart
+++ b/lib/screens/secret_keys.dart
@@ -54,11 +54,11 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
});
}
- void _copy(String value, {required bool sensitive}) {
+ void _copy(String value) {
if (value.isEmpty) return;
SecureClipboard.copy(value);
final i18n = AppLocalizations.of(context)!;
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.copiedToClipboard)));
+ showCopyToast(context, i18n.copiedToClipboard);
}
@override
@@ -73,8 +73,8 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
);
}
- // Seeds and the secret spend key are blurred until revealed; the public keys
- // aren't sensitive.
+ // Seeds and the secret spend key are blurred until revealed; the public
+ // keys are shown outright.
return KeyRevealView(
title: i18n.secretKeysTitle,
description: i18n.secretKeysDescription,
@@ -105,16 +105,8 @@ class _SecretKeysScreenState extends State<SecretKeysScreen> with SecureScreenMi
value: data.secretSpendKey,
revealable: true,
),
- KeyRevealField(
- label: i18n.secretKeysPublicSpendKey,
- value: data.publicSpendKey,
- sensitive: false,
- ),
- KeyRevealField(
- label: i18n.secretKeysPublicViewKey,
- value: data.publicViewKey,
- sensitive: false,
- ),
+ KeyRevealField(label: i18n.secretKeysPublicSpendKey, value: data.publicSpendKey),
+ KeyRevealField(label: i18n.secretKeysPublicViewKey, value: data.publicViewKey),
],
);
}
diff --git a/lib/screens/send.dart b/lib/screens/send.dart
index 738166c..0d667cc 100644
--- a/lib/screens/send.dart
+++ b/lib/screens/send.dart
@@ -138,9 +138,7 @@ class _SendScreenState extends State<SendScreen> {
if (uri != null && uri.scheme == 'monero') {
if (!wallet.isAddressValid(uri.path)) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.sendInvalidAddressError)));
+ showBrandToast(context, i18n.sendInvalidAddressError);
}
return;
}
@@ -154,9 +152,7 @@ class _SendScreenState extends State<SendScreen> {
address = result;
} else {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.sendInvalidAddressError)));
+ showBrandToast(context, i18n.sendInvalidAddressError);
}
return;
}
@@ -371,9 +367,7 @@ class _SendScreenState extends State<SendScreen> {
_isLoadingFees = false;
});
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.sendFailedToGetFeesError)));
+ showBrandToast(context, i18n.sendFailedToGetFeesError);
}
}
}
@@ -459,7 +453,7 @@ class _SendScreenState extends State<SendScreen> {
}
} else {
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ showBrandToast(context, i18n.unknownError);
}
}
}
@@ -536,13 +530,13 @@ class _SendScreenState extends State<SendScreen> {
errorMsg = 'Failed to send transaction. You might have insufficient unlocked balance.';
}
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
+ showBrandToast(context, errorMsg);
}
rethrow;
} catch (error) {
log(LogLevel.error, error.toString());
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ showBrandToast(context, i18n.unknownError);
}
rethrow;
}
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index ec076d5..7355ce5 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -114,9 +114,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Enabling app-lock is an explicit opt-in, so decline and error both report.
if (result != BiometricAuthResult.authenticated) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.settingsAppLockUnableToAuthError)));
+ showBrandToast(context, i18n.settingsAppLockUnableToAuthError);
}
return;
}
@@ -145,9 +143,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (logFiles.isEmpty) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.settingsExportLogsError)));
+ showBrandToast(context, i18n.settingsExportLogsError);
}
return;
}
@@ -165,9 +161,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
} catch (e) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.settingsExportLogsError)));
+ showBrandToast(context, i18n.settingsExportLogsError);
}
}
}
diff --git a/lib/screens/unlock.dart b/lib/screens/unlock.dart
index ef61a93..a37bd05 100644
--- a/lib/screens/unlock.dart
+++ b/lib/screens/unlock.dart
@@ -45,9 +45,7 @@ class _UnlockScreenState extends State<UnlockScreen> {
if (mounted) Navigator.pushReplacementNamed(context, '/wallet_home');
} else if (result == BiometricAuthResult.error) {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.unlockUnableToAuthError)));
+ showBrandToast(context, i18n.unlockUnableToAuthError);
}
}
}
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index d6241b4..9b3c7e5 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -63,9 +63,7 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
void _showTxSuccessToast() {
final i18n = AppLocalizations.of(context)!;
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.sendTransactionSuccessfullySent)));
+ showBrandToast(context, i18n.sendTransactionSuccessfullySent);
}
@override
diff --git a/lib/widgets/connection_settings_form.dart b/lib/widgets/connection_settings_form.dart
index 0906814..925b666 100644
--- a/lib/widgets/connection_settings_form.dart
+++ b/lib/widgets/connection_settings_form.dart
@@ -171,9 +171,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
_errorMessage = i18n.lwsSetupInvalidQrCode;
});
} else {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.lwsSetupInvalidQrCode)));
+ showBrandToast(context, i18n.lwsSetupInvalidQrCode);
}
}
}
@@ -204,15 +202,10 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
});
if (hadProtocol) {
- ScaffoldMessenger.of(context)
- ..hideCurrentSnackBar()
- ..showSnackBar(
- SnackBar(
- content: Text(
- addressUsesSsl(value) ? i18n.connectionProtocolHttps : i18n.connectionProtocolHttp,
- ),
- ),
- );
+ showBrandToast(
+ context,
+ addressUsesSsl(value) ? i18n.connectionProtocolHttps : i18n.connectionProtocolHttp,
+ );
}
}
@@ -303,9 +296,7 @@ class _ConnectionSettingsFormState extends State<ConnectionSettingsForm> {
}
if (_useTor && TorSettingsService.sharedInstance.torMode == TorMode.disabled) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.lwsSetupTorDisabledError)));
+ showBrandToast(context, i18n.lwsSetupTorDisabledError);
return;
}
Why this scored 47/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.