What changed, and why it matters
This commit fixes a UI bug in Stack Wallet's desktop FROST wallet key popup. The previous version accidentally displayed the current FROST keys/config in the section meant to show the previous generation's keys/config. The patch corrects which data is shown in each section and adds a widget test to verify the fix. There is no direct evidence in the commit that this was a security vulnerability, but mislabeling sensitive key material could confuse users.
Treat as a routine UI/UX bug fix. No immediate security response is indicated by the diff alone. If FROST key rotation workflows are safety-critical, review whether the mislabeling could have caused users to backup or share wrong key material, and consider documenting the fix in release notes.
Security signals we found
UI mislabeling of sensitive cryptographic key material (FROST keys/config)
Potential user confusion between current and previous generation keys
No cryptographic, access-control, or network-layer changes
No vendor disclosure of security relevance in commit or references
Evidence from the diff
In wallet_keys_desktop_popup.dart, the FROST previous-generation display block was using frostData!.keys and frostData!.config instead of frostData!.prevGen!.keys and frostData!.prevGen!.config. The patch swaps those references so the ‘Previous generation Keys’ and ‘Previous generation Config’ sections render the actual previous-generation data. A new widget test asserts that current and previous keys/config are displayed and copyable in the correct order. The rest of the diff is formatting/indentation noise.
Changed components
lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dartFROST wallet desktop key display popupInspect captured patch +187 / −125
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart
index 2c31098..95b2855 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart
@@ -88,90 +88,16 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
const SizedBox(height: 6),
frostData != null
? Column(
- children: [
- Text("Keys", style: STextStyles.desktopTextMedium(context)),
- const SizedBox(height: 8),
- Center(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: RoundedWhiteContainer(
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 9,
- ),
- child: Row(
- children: [
- Flexible(
- child: SelectableText(
- frostData!.keys,
- style: STextStyles.desktopTextExtraExtraSmall(
- context,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- const SizedBox(width: 10),
- IconCopyButton(data: frostData!.keys),
- // TODO [prio=low: Add QR code button and dialog.
- ],
- ),
- ),
- ),
- ),
- const SizedBox(height: 24),
- Text("Config", style: STextStyles.desktopTextMedium(context)),
- const SizedBox(height: 8),
- Center(
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32),
- child: RoundedWhiteContainer(
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 9,
- ),
- child: Row(
- children: [
- Flexible(
- child: SelectableText(
- frostData!.config,
- style: STextStyles.desktopTextExtraExtraSmall(
- context,
- ),
- textAlign: TextAlign.center,
- ),
- ),
- const SizedBox(width: 10),
- IconCopyButton(data: frostData!.config),
- // TODO [prio=low: Add QR code button and dialog.
- ],
- ),
- ),
- ),
- ),
- if (frostData?.prevGen != null) const SizedBox(height: 24),
- if (frostData?.prevGen != null)
- Text(
- "Previous generation Keys",
- style: STextStyles.desktopTextMedium(context),
- ),
- if (frostData?.prevGen != null) const SizedBox(height: 8),
- if (frostData?.prevGen != null)
+ children: [
+ Text("Keys", style: STextStyles.desktopTextMedium(context)),
+ const SizedBox(height: 8),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: RoundedWhiteContainer(
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 9,
@@ -195,22 +121,19 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
),
),
),
- if (frostData?.prevGen != null) const SizedBox(height: 24),
- if (frostData?.prevGen != null)
+ const SizedBox(height: 24),
Text(
- "Previous generation Config",
+ "Config",
style: STextStyles.desktopTextMedium(context),
),
- if (frostData?.prevGen != null) const SizedBox(height: 8),
- if (frostData?.prevGen != null)
+ const SizedBox(height: 8),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: RoundedWhiteContainer(
- borderColor:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 9,
@@ -219,7 +142,7 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
children: [
Flexible(
child: SelectableText(
- frostData!.prevGen!.config,
+ frostData!.config,
style: STextStyles.desktopTextExtraExtraSmall(
context,
),
@@ -227,48 +150,128 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
),
),
const SizedBox(width: 10),
- IconCopyButton(data: frostData!.prevGen!.config),
+ IconCopyButton(data: frostData!.config),
// TODO [prio=low: Add QR code button and dialog.
],
),
),
),
),
- const SizedBox(height: 24),
- ],
- )
- : keyData != null
- ? keyData is ViewOnlyWalletData
- ? Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: ViewOnlyWalletDataWidget(
- data: keyData as ViewOnlyWalletData,
- ),
- )
- : CustomTabView(
- titles: [
- if (words.isNotEmpty) "Mnemonic",
- if (keyData is XPrivData) "XPriv(s)",
- if (keyData is CWKeyData) "Keys",
- ],
- children: [
- if (words.isNotEmpty)
- Padding(
- padding: const EdgeInsets.only(top: 16),
- child: _Mnemonic(words: words),
+ if (frostData?.prevGen != null) const SizedBox(height: 24),
+ if (frostData?.prevGen != null)
+ Text(
+ "Previous generation Keys",
+ style: STextStyles.desktopTextMedium(context),
+ ),
+ if (frostData?.prevGen != null) const SizedBox(height: 8),
+ if (frostData?.prevGen != null)
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: RoundedWhiteContainer(
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 9,
+ ),
+ child: Row(
+ children: [
+ Flexible(
+ child: SelectableText(
+ frostData!.prevGen!.keys,
+ style:
+ STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const SizedBox(width: 10),
+ IconCopyButton(data: frostData!.prevGen!.keys),
+ // TODO [prio=low: Add QR code button and dialog.
+ ],
+ ),
+ ),
),
- if (keyData is XPrivData)
- WalletXPrivs(
- xprivData: keyData as XPrivData,
- walletId: walletId,
+ ),
+ if (frostData?.prevGen != null) const SizedBox(height: 24),
+ if (frostData?.prevGen != null)
+ Text(
+ "Previous generation Config",
+ style: STextStyles.desktopTextMedium(context),
+ ),
+ if (frostData?.prevGen != null) const SizedBox(height: 8),
+ if (frostData?.prevGen != null)
+ Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: RoundedWhiteContainer(
+ borderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 9,
+ ),
+ child: Row(
+ children: [
+ Flexible(
+ child: SelectableText(
+ frostData!.prevGen!.config,
+ style:
+ STextStyles.desktopTextExtraExtraSmall(
+ context,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const SizedBox(width: 10),
+ IconCopyButton(
+ data: frostData!.prevGen!.config,
+ ),
+ // TODO [prio=low: Add QR code button and dialog.
+ ],
+ ),
+ ),
),
- if (keyData is CWKeyData)
- CNWalletKeys(
- cwKeyData: keyData as CWKeyData,
- walletId: walletId,
+ ),
+ const SizedBox(height: 24),
+ ],
+ )
+ : keyData != null
+ ? keyData is ViewOnlyWalletData
+ ? Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: ViewOnlyWalletDataWidget(
+ data: keyData as ViewOnlyWalletData,
),
- ],
- )
+ )
+ : CustomTabView(
+ titles: [
+ if (words.isNotEmpty) "Mnemonic",
+ if (keyData is XPrivData) "XPriv(s)",
+ if (keyData is CWKeyData) "Keys",
+ ],
+ children: [
+ if (words.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: 16),
+ child: _Mnemonic(words: words),
+ ),
+ if (keyData is XPrivData)
+ WalletXPrivs(
+ xprivData: keyData as XPrivData,
+ walletId: walletId,
+ ),
+ if (keyData is CWKeyData)
+ CNWalletKeys(
+ cwKeyData: keyData as CWKeyData,
+ walletId: walletId,
+ ),
+ ],
+ )
: _Mnemonic(words: words),
const SizedBox(height: 32),
],
@@ -311,8 +314,9 @@ class _Mnemonic extends StatelessWidget {
child: MnemonicTable(
words: words,
isDesktop: true,
- itemBorderColor:
- Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
+ itemBorderColor: Theme.of(
+ context,
+ ).extension<StackColors>()!.buttonBackSecondary,
),
),
const SizedBox(height: 24),
diff --git a/test/widget_tests/desktop/wallet_keys_desktop_popup_test.dart b/test/widget_tests/desktop/wallet_keys_desktop_popup_test.dart
new file mode 100644
index 0000000..6cfa28f
--- /dev/null
+++ b/test/widget_tests/desktop/wallet_keys_desktop_popup_test.dart
@@ -0,0 +1,58 @@
+import "package:flutter/material.dart";
+import "package:flutter_riverpod/flutter_riverpod.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:stackwallet/models/isar/stack_theme.dart";
+import "package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart"
+ show IconCopyButton;
+import "package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart";
+import "package:stackwallet/themes/stack_colors.dart";
+
+import "../../sample_data/theme_json.dart";
+
+void main() {
+ testWidgets("shows and copies the previous FROST keys", (tester) async {
+ tester.view.physicalSize = const Size(1200, 1600);
+ tester.view.devicePixelRatio = 1;
+ addTearDown(tester.view.resetPhysicalSize);
+ addTearDown(tester.view.resetDevicePixelRatio);
+
+ await tester.pumpWidget(
+ ProviderScope(
+ child: MaterialApp(
+ theme: ThemeData(
+ extensions: [
+ StackColors.fromStackColorTheme(
+ StackTheme.fromJson(json: lightThemeJsonMap),
+ ),
+ ],
+ ),
+ home: const Scaffold(
+ body: WalletKeysDesktopPopup(
+ words: [],
+ walletId: "wallet",
+ frostData: (
+ myName: "name",
+ keys: "current-keys",
+ config: "current-config",
+ prevGen: (keys: "previous-keys", config: "previous-config"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(
+ tester
+ .widgetList<SelectableText>(find.byType(SelectableText))
+ .map((widget) => widget.data),
+ ["current-keys", "current-config", "previous-keys", "previous-config"],
+ );
+ expect(
+ tester
+ .widgetList<IconCopyButton>(find.byType(IconCopyButton))
+ .map((widget) => widget.data),
+ ["current-keys", "current-config", "previous-keys", "previous-config"],
+ );
+ });
+}
Why this scored 20/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.