Enforce passphrase confirmation match on the restore and create wallet flows (#3474)
What changed, and why it matters
This commit fixes a bug in the wallet restore and creation screens where the 'confirm passphrase' field was not actually being checked. A user could type one passphrase and a different confirmation, yet still proceed. The wallet would then be created or restored using only the first passphrase, which could lock users out of their funds if they made a typo. The fix wraps the fields in a proper form and validates the confirmation before continuing.
Treat this as a security-relevant correctness fix and include it in the next release. No immediate incident response is required, but consider auditing other forms in the app for similar validators that are never invoked.
Security signals we found
Missing form validation bypassed confirmation-field check
User could restore/create wallet with mismatched passphrase confirmation
Risk of user locking themselves out of wallet due to undetected typo
Fix aligns restore flow with existing advanced privacy settings pattern
Evidence from the diff
In AddPassphraseBottomSheet, the confirm-passphrase BaseTextFormField had a validator comparing its value to passphraseController.text, but because the fields were not inside a Form and validate() was never called, the validator was dead code. The patch adds a Form with a GlobalKey
Changed components
lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dartlib/src/screens/new_wallet/advanced_privacy_settings_page.dartInspect captured patch +62 / −49
diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart
index 76683bb..2f190c8 100644
--- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart
+++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart
@@ -360,11 +360,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
widget.nodeViewModel.save();
}
- if (passphraseController.text.isNotEmpty) {
- if (_passphraseFormKey.currentState != null &&
- !_passphraseFormKey.currentState!.validate()) {
- return;
- }
+ if (_passphraseFormKey.currentState != null &&
+ !_passphraseFormKey.currentState!.validate()) {
+ return;
}
widget.seedTypeViewModel.setPassphrase(passphraseController.text);
diff --git a/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart
index dde80d1..c1d1f1d 100644
--- a/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart
+++ b/lib/src/widgets/bottom_sheet/add_passphrase_bottom_sheet_widget.dart
@@ -20,6 +20,7 @@ class AddPassphraseBottomSheet extends StatefulWidget {
class _AddPassphraseBottomSheetState extends State<AddPassphraseBottomSheet> {
late final TextEditingController passphraseController;
late final TextEditingController confirmPassphraseController;
+ final _passphraseFormKey = GlobalKey<FormState>();
@override
void initState() {
@@ -112,51 +113,60 @@ class _AddPassphraseBottomSheetState extends State<AddPassphraseBottomSheet> {
),
),
SizedBox(height: 24),
- BaseTextFormField(
- key: ValueKey('add_passphrase_bottom_sheet_widget_passphrase_textfield_key'),
- controller: passphraseController,
- obscureText: obscurePassphrase,
- contentPadding: EdgeInsets.symmetric(horizontal: 12),
- hintText: S.of(context).required_passphrase,
- suffixIcon: GestureDetector(
- onTap: () {
- setState(() {
- obscurePassphrase = !obscurePassphrase;
- });
- },
- child: Icon(
- obscurePassphrase ? Icons.visibility_off : Icons.visibility,
- size: 24,
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
- ),
- ),
- ),
- SizedBox(height: 8),
- BaseTextFormField(
- key: ValueKey('add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key'),
- controller: confirmPassphraseController,
- obscureText: obscurePassphrase,
- contentPadding: EdgeInsets.symmetric(horizontal: 12),
- hintText: S.of(context).confirm_passphrase,
- suffixIcon: GestureDetector(
- onTap: () {
- setState(() {
- obscurePassphrase = !obscurePassphrase;
- });
- },
- child: Icon(
- obscurePassphrase ? Icons.visibility_off : Icons.visibility,
- size: 24,
- color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
- ),
- ),
- validator: (text) {
- if (text == passphraseController.text) {
- return null;
- }
+ Form(
+ key: _passphraseFormKey,
+ child: Column(
+ children: [
+ BaseTextFormField(
+ key: ValueKey('add_passphrase_bottom_sheet_widget_passphrase_textfield_key'),
+ controller: passphraseController,
+ obscureText: obscurePassphrase,
+ contentPadding: EdgeInsets.symmetric(horizontal: 12),
+ hintText: S.of(context).required_passphrase,
+ suffixIcon: GestureDetector(
+ onTap: () {
+ setState(() {
+ obscurePassphrase = !obscurePassphrase;
+ });
+ },
+ child: Icon(
+ obscurePassphrase ? Icons.visibility_off : Icons.visibility,
+ size: 24,
+ color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
+ ),
+ ),
+ ),
+ SizedBox(height: 8),
+ BaseTextFormField(
+ key: ValueKey(
+ 'add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key',
+ ),
+ controller: confirmPassphraseController,
+ obscureText: obscurePassphrase,
+ contentPadding: EdgeInsets.symmetric(horizontal: 12),
+ hintText: S.of(context).confirm_passphrase,
+ suffixIcon: GestureDetector(
+ onTap: () {
+ setState(() {
+ obscurePassphrase = !obscurePassphrase;
+ });
+ },
+ child: Icon(
+ obscurePassphrase ? Icons.visibility_off : Icons.visibility,
+ size: 24,
+ color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
+ ),
+ ),
+ validator: (text) {
+ if (text == passphraseController.text) {
+ return null;
+ }
- return S.of(context).passphrases_doesnt_match;
- },
+ return S.of(context).passphrases_doesnt_match;
+ },
+ ),
+ ],
+ ),
),
SizedBox(height: 16),
Padding(
@@ -184,6 +194,11 @@ class _AddPassphraseBottomSheetState extends State<AddPassphraseBottomSheet> {
child: PrimaryButton(
key: ValueKey('add_passphrase_bottom_sheet_widget_restore_button_key'),
onPressed: () {
+ if (_passphraseFormKey.currentState != null &&
+ !_passphraseFormKey.currentState!.validate()) {
+ return;
+ }
+
Navigator.pop(context);
widget.onRestoreButtonPressed(passphraseController.text);
},
Why this scored 52/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.