What changed, and why it matters
This commit changes how Stack Wallet's Android backup feature picks a save location. Instead of asking the user to choose a single file, it now asks the user to choose a folder. It also switches the auto-backup writer to use a helper that works with Android's Storage Access Framework (SAF), and it disables old-backup cleanup on Android because the SAF library cannot delete files. The change appears to be a bug fix for backups failing or misbehaving on Android due to file-path restrictions, not a fix for an active security vulnerability.
Treat this as a functional/compatability fix rather than a security patch. Review the `FS.writeStringToFile` helper to confirm it validates the content:// URI and does not allow backup writes to attacker-controlled locations. Consider adding backup rotation support for Android SAF or warning users that old backups will accumulate when SAF directories are used.
Security signals we found
Backup destination selection changed from openFile() to pickDir()
Direct File.writeAsString replaced with FS helper that handles Android content:// URIs
Old backup trimming disabled on Android content:// URIs due to missing SAF delete API
No explicit security claim, CVE, or advisory referenced in commit or supplied materials
Evidence from the diff
The patch replaces stackFileSystem.openFile() with stackFileSystem.pickDir() in four backup-related UI files and one desktop page, indicating the backup destination selection moved from file-level to directory-level. In auto_swb_service.dart, File(fileToSave).writeAsString() is replaced by FS.writeStringToFile(content, autoBackupDirectoryPath, fileToSave.split('/').last), which routes writes through a utility that can handle Android content:// URIs. A guard is added in trimBackups() to skip rotation when dirPath is a content:// URI because the SAF library lacks deletion support. The remaining diff is formatting noise. There is no direct evidence of a security bug such as path traversal, arbitrary file write, or information disclosure; the change is consistent with adapting backup I/O to Android SAF constraints.
Changed components
lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dartlib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dartlib/services/auto_swb_service.dartInspect captured patch +30 / −17
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart
index 57785f5..849f10b 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart
@@ -101,8 +101,9 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
child: Text(
"Back",
style: STextStyles.button(context).copyWith(
- color:
- Theme.of(context).extension<StackColors>()!.accentColorDark,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorDark,
),
),
onPressed: () {
@@ -155,8 +156,9 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
child: Text(
"Back",
style: STextStyles.button(context).copyWith(
- color:
- Theme.of(context).extension<StackColors>()!.accentColorDark,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.accentColorDark,
),
),
onPressed: () {
@@ -313,14 +315,13 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
TextSpan(
text: "stackwallet.com.",
style: STextStyles.richLink(context),
- recognizer:
- TapGestureRecognizer()
- ..onTap = () {
- launchUrl(
- Uri.parse("https://stackwallet.com"),
- mode: LaunchMode.externalApplication,
- );
- },
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ launchUrl(
+ Uri.parse("https://stackwallet.com"),
+ mode: LaunchMode.externalApplication,
+ );
+ },
),
],
),
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
index c1bd1b2..9e32ad3 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
@@ -261,7 +261,7 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
await stackFileSystem.prepareStorage();
if (mounted) {
final filePath = await stackFileSystem
- .openFile();
+ .pickDir();
if (mounted) {
setState(() {
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
index 0df1b21..ebb6b94 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
@@ -310,7 +310,7 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
await stackFileSystem.prepareStorage();
if (mounted) {
final filePath = await stackFileSystem
- .openFile();
+ .pickDir();
if (mounted) {
setState(() {
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
index 81d7b9d..077ff21 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
@@ -302,7 +302,7 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
- final filePath = await stackFileSystem.openFile();
+ final filePath = await stackFileSystem.pickDir();
if (mounted) {
setState(() {
diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
index 929c3c2..1a0fc67 100644
--- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
+++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
@@ -321,7 +321,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
await stackFileSystem.prepareStorage();
if (mounted) {
final filePath = await stackFileSystem
- .openFile();
+ .pickDir();
if (mounted) {
setState(() {
diff --git a/lib/services/auto_swb_service.dart b/lib/services/auto_swb_service.dart
index 39b080a..419e7a0 100644
--- a/lib/services/auto_swb_service.dart
+++ b/lib/services/auto_swb_service.dart
@@ -17,6 +17,7 @@ import 'package:tuple/tuple.dart';
import '../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import '../utilities/flutter_secure_storage_interface.dart';
+import '../utilities/fs.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
@@ -91,7 +92,11 @@ class AutoSWBService extends ChangeNotifier {
adkVersion,
);
- await File(fileToSave).writeAsString(content, flush: true);
+ await FS.writeStringToFile(
+ content,
+ autoBackupDirectoryPath,
+ fileToSave.split("/").last,
+ );
Prefs.instance.lastAutoBackup = now;
@@ -121,6 +126,13 @@ class AutoSWBService extends ChangeNotifier {
/// Trim the number of auto backup files based on age
void trimBackups(String dirPath, int numberToKeep) {
+ if (Platform.isAndroid && dirPath.startsWith("content://")) {
+ Logging.instance.w(
+ "Android SAF lib doesn't provide a deletion API. Cannot trim/rotate out old backups",
+ );
+ return;
+ }
+
final dir = Directory(dirPath);
final List<Tuple2<DateTime, FileSystemEntity>> files = [];
Why this scored 33/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.