CW1272 tails, linux generic fixes (#2630)
What changed, and why it matters
This commit fixes Linux and Tails-specific app-data handling, switches SQLite to a desktop-compatible implementation, and corrects a broken icon asset. It is primarily a compatibility and bug-fix patch. There is no direct evidence in the commit that it addresses a security vulnerability, but the changes touch sensitive areas: where wallet data is stored, how old data directories are migrated, and how Tor/SOCKS proxy settings are parsed on a privacy-focused OS (Tails).
Treat as a routine platform-compatibility fix. Reviewers should verify that linuxSymlinkSharedPreferences() handles concurrent runs safely, that the migration does not delete data before a successful copy, and that the new Tor SOCKS parsing fails closed when no valid proxy is configured. No urgent security response is indicated by the commit itself.
Security signals we found
Data directory migration logic copies and deletes user data directories, with potential for data loss if interrupted or if paths overlap
Symlink creation in linuxSymlinkSharedPreferences() runs with ambient filesystem privileges and could be influenced by environment variables (XDG_DATA_HOME, HOME)
Tor proxy parsing now relies on SOCKS_SERVER environment variable rather than hard-coded Tails port; misconfiguration could leak traffic if SOCKS_SERVER is unset or malformed
SQLite database factory is switched to FFI on desktop platforms, changing native library loading behavior
No explicit security claim, CVE, or researcher attribution in commit message or diff
Evidence from the diff
The patch refactors root directory selection in cw_core/lib/root_dir.dart, adding Tails persistence detection, fallback path lists, and a migration helper that copies old shared-preferences directories and replaces them with symlinks. It also changes cw_core/lib/db/sqlite.dart to use sqflite_common_ffi on Linux/Windows and adds sqlite3_flutter_libs as a dependency. cw_core/lib/utils/tor/abstract.dart is updated to prefer the SOCKS_SERVER environment variable and remove the hard-coded Tails SOCKS port fallback. cw_bitcoin/lib/electrum_wallet.dart now uses the new getAppDir() helper for its debug log path. The remaining changes are lockfile updates, test cleanup, whitespace, and a corrected image asset path.
Changed components
cw_core/lib/root_dir.dartcw_core/lib/db/sqlite.dartcw_core/lib/utils/tor/abstract.dartcw_bitcoin/lib/electrum_wallet.dartlib/main.dartcw_core/pubspec.yamlInspect captured patch +189 / −25
diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart
index 7a0fecd3..dc611d50 100644
--- a/cw_bitcoin/lib/electrum_wallet.dart
+++ b/cw_bitcoin/lib/electrum_wallet.dart
@@ -5,6 +5,7 @@ import 'dart:isolate';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
+import 'package:cw_core/root_dir.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -374,7 +375,7 @@ abstract class ElectrumWalletBase
runningIsolate.kill(priority: Isolate.immediate);
}
- final appDir = await getApplicationSupportDirectory();
+ final appDir = await getAppDir();
String debugLogPath = "${appDir.path}/logs/debug.log";
final receivePort = ReceivePort();
diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock
index 681e68d4..c4b45c77 100644
--- a/cw_bitcoin/pubspec.lock
+++ b/cw_bitcoin/pubspec.lock
@@ -1084,6 +1084,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart
index e8bba668..fdd58921 100644
--- a/cw_core/lib/db/sqlite.dart
+++ b/cw_core/lib/db/sqlite.dart
@@ -1,9 +1,14 @@
-import 'package:sqflite/sqflite.dart';
+import 'dart:io';
+
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
late Database db;
Future<void> initDb({String? pathOverride}) async {
+ if (Platform.isLinux || Platform.isWindows) {
+ databaseFactory = databaseFactoryFfi;
+ }
db = await openDatabase(
pathOverride ?? "cake.db",
version: 1,
diff --git a/cw_core/lib/root_dir.dart b/cw_core/lib/root_dir.dart
index c2a8170b..7f75b20f 100644
--- a/cw_core/lib/root_dir.dart
+++ b/cw_core/lib/root_dir.dart
@@ -1,11 +1,76 @@
import 'dart:io';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as p;
String? _rootDirPath;
-void setRootDirFromEnv() => _rootDirPath = Platform.environment['CAKE_WALLET_DIR'];
+const String _tailsData = '/live/persistence/TailsData_unlocked/Persistent';
-Future<Directory> getAppDir({String appName = 'cake_wallet'}) async {
+bool get isNonAmnesticTails {
+ try {
+ final os = File("/etc/os-release").readAsLinesSync();
+ for (var line in os) {
+ if (!line.startsWith("ID=")) continue;
+ if (!line.contains("tails")) continue;
+ return Directory(_tailsData).existsSync();
+ }
+ } catch (e) {
+ return false;
+ }
+ return false;
+}
+
+bool showNotice = true;
+
+void setRootDirFromEnv() =>
+ _rootDirPath = Platform.environment['CAKE_WALLET_DIR'];
+
+void copyDirectory(Directory source, Directory destination) {
+ source.listSync(recursive: false).forEach((var entity) {
+ if (entity is Directory) {
+ var newDirectory = Directory(p.join(destination.absolute.path, p.basename(entity.path)));
+ newDirectory.createSync(recursive: true);
+ copyDirectory(entity.absolute, newDirectory);
+ } else if (entity is File) {
+ destination.createSync(recursive: true);
+ entity.copySync(p.join(destination.path, p.basename(entity.path)));
+ }
+ });
+}
+
+Future<void> linuxSymlinkSharedPreferences() async {
+ if (!Platform.isLinux) return; // nuh-uh
+ final dataHome = Platform.environment["XDG_DATA_HOME"] ?? p.join(Platform.environment["HOME"] ?? "", ".local", "share");
+ var cakeNames = ['com.example.cake_wallet', 'cake_wallet'];
+ for (String name in cakeNames) {
+ final oldPath = p.join(dataHome, name);
+ final newPath = p.join((await getAppDir()).path, "_local_share");
+ final oldDir = Directory(oldPath);
+ final oldLink = Link(oldPath);
+ final newDir = Directory(newPath);
+ if (oldDir.existsSync()) {
+ if (oldLink.existsSync()) {
+ printV("not creating, link exists");
+ } else {
+ if (newDir.existsSync()) {
+ newDir.renameSync("${newPath}_${DateTime.now().millisecondsSinceEpoch~/1000}");
+ }
+ copyDirectory(oldDir, newDir);
+ oldDir.deleteSync(recursive: true);
+ }
+ }
+ if (!oldLink.existsSync()) {
+ oldLink.create(newPath, recursive: true);
+ }
+ if (!newDir.existsSync()) {
+ newDir.createSync(recursive: true);
+ }
+ }
+}
+
+Future<Directory> getAppDir() async {
+ const String appName = 'cake_wallet';
Directory dir;
if (_rootDirPath != null && _rootDirPath!.isNotEmpty) {
@@ -15,16 +80,45 @@ Future<Directory> getAppDir({String appName = 'cake_wallet'}) async {
if (Platform.isWindows) {
dir = await getApplicationSupportDirectory();
} else if (Platform.isLinux) {
- String appDirPath;
-
+ String? appDirPath;
try {
dir = await getApplicationDocumentsDirectory();
appDirPath = '${dir.path}/$appName';
} catch (e) {
- appDirPath = '/home/${Platform.environment['USER']}/.$appName';
+ appDirPath = null;
+ }
+ // App will try to use last entry in here, so {distro,package}-specific paths can be
+ // be put as one of last items (tails - I'm looking at you), and other paths can be
+ // added in the order of preference
+ // Which currently is $HOME/.config/$appName - as this is the most standard directory
+ // for storing things that users in general back-up
+ var linuxAppPath = [
+ if (appDirPath != null) appDirPath, // old preferred
+ p.join('/home', Platform.environment['USER']??"null", appName), // old fallback
+ if (Platform.environment['HOME'] != null) p.join(Platform.environment['HOME']!, ".$appName"), // old fallback but using HOME
+ if (Platform.environment['HOME'] != null) p.join(Platform.environment['HOME']!, '.config', appName), // old fallback but using HOME
+ if (isNonAmnesticTails) p.join(_tailsData, ".$appName") // tails (if persistance is enabled)
+ ];
+
+ String preferredPath = linuxAppPath.last;
+
+ preferredLoop:
+ for (String notSoPreferredPath in linuxAppPath) {
+ if (notSoPreferredPath == linuxAppPath.last) continue;
+ bool useThisOne = Directory(notSoPreferredPath).existsSync();
+ if (useThisOne) {
+ if (showNotice) {
+ showNotice = false;
+ printV("Not using $preferredPath because $notSoPreferredPath exists, falling back for backwards compatibility");
+ printV("Can't see your wallet? Check\n - ${linuxAppPath.join("\n - ")}\n and move directory that to $preferredPath");
+ printV("Or use CAKE_WALLET_DIR=/path/to/app/ ${Platform.executable}");
+ }
+ preferredPath = notSoPreferredPath;
+ break preferredLoop;
+ }
}
- dir = Directory.fromUri(Uri.file(appDirPath));
+ dir = Directory.fromUri(Uri.file(preferredPath));
await dir.create(recursive: true);
} else {
dir = await getApplicationDocumentsDirectory();
diff --git a/cw_core/lib/utils/tor/abstract.dart b/cw_core/lib/utils/tor/abstract.dart
index 6fc4016a..70272e47 100644
--- a/cw_core/lib/utils/tor/abstract.dart
+++ b/cw_core/lib/utils/tor/abstract.dart
@@ -31,16 +31,17 @@ abstract class CakeTorInstance {
final uri = Uri.tryParse(socksServer);
if (uri != null) {
return CakeTorSocks(uri.port);
+ } else {
+ final uri = Uri.tryParse("socks5://$socksServer");
+ if (uri != null) {
+ return CakeTorSocks(uri.port);
+ }
}
}
- final os = File("/etc/os-release").readAsLinesSync();
- for (var line in os) {
- if (!line.startsWith("ID=")) continue;
- if (!line.contains("tails")) continue;
- return CakeTorSocks(9150);
- }
} catch (e) {
- printV("Failed to identify linux version - /etc/os-release missing");
+ printV(
+ "Failed to identify linux version - no SOCKS_SERVER variable found or malformed",
+ );
}
}
try {
diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock
index 918a81a0..dd5dbaac 100644
--- a/cw_core/pubspec.lock
+++ b/cw_core/pubspec.lock
@@ -730,6 +730,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: "direct main"
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml
index 7c963f24..4dc27a06 100644
--- a/cw_core/pubspec.yaml
+++ b/cw_core/pubspec.yaml
@@ -46,6 +46,7 @@ dependencies:
ref: cake-update-v2
sqflite: ^2.4.1
sqflite_common_ffi: ^2.3.4+4
+ sqlite3_flutter_libs: 0.5.40
dev_dependencies:
flutter_test:
@@ -63,7 +64,7 @@ dependency_overrides:
# The following section is specific to Flutter.
flutter:
- uses-material-design: true
+ uses-material-design: true
# To add assets to your package, add an assets section, like this:
# assets:
diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock
index 85585721..ab2826fa 100644
--- a/cw_decred/pubspec.lock
+++ b/cw_decred/pubspec.lock
@@ -761,6 +761,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock
index ae521ab8..f9ef7a82 100644
--- a/cw_monero/pubspec.lock
+++ b/cw_monero/pubspec.lock
@@ -874,6 +874,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_monero/test/monero_wallet_service_test.dart b/cw_monero/test/monero_wallet_service_test.dart
index 3b24c205..780caea4 100644
--- a/cw_monero/test/monero_wallet_service_test.dart
+++ b/cw_monero/test/monero_wallet_service_test.dart
@@ -21,7 +21,6 @@ Future<void> main() async {
late File moneroCBinary;
setUpAll(() async {
- databaseFactory = databaseFactoryFfi;
await initDb(pathOverride: './test/data/db');
Hive.init('./test/data/db');
PathProviderPlatform.instance = MockPathProviderPlatform();
diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock
index c009b650..42df7b8d 100644
--- a/cw_nano/pubspec.lock
+++ b/cw_nano/pubspec.lock
@@ -879,6 +879,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock
index 2e1b3fb0..ce8192f8 100644
--- a/cw_wownero/pubspec.lock
+++ b/cw_wownero/pubspec.lock
@@ -778,6 +778,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/cw_zano/pubspec.lock b/cw_zano/pubspec.lock
index 0c4a5765..826e385b 100644
--- a/cw_zano/pubspec.lock
+++ b/cw_zano/pubspec.lock
@@ -775,6 +775,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.9.0"
+ sqlite3_flutter_libs:
+ dependency: transitive
+ description:
+ name: sqlite3_flutter_libs
+ sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.40"
stack_trace:
dependency: transitive
description:
diff --git a/integration_test_runner.sh b/integration_test_runner.sh
index cf250148..de21b3ed 100755
--- a/integration_test_runner.sh
+++ b/integration_test_runner.sh
@@ -14,6 +14,7 @@ RETRY_COUNT=${RETRY_COUNT:-1}
DATA_DIRS=(
"$HOME/.local/share/com.example.cake_wallet"
"$HOME/Documents/cake_wallet"
+ "$HOME/.config/cake_wallet"
)
# Global state
@@ -46,7 +47,7 @@ format_duration() {
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local secs=$((seconds % 60))
-
+
if (( hours > 0 )); then
echo "${hours}h ${minutes}m ${secs}s"
elif (( minutes > 0 )); then
@@ -92,7 +93,7 @@ run_test() {
local end_time=$(date +%s)
local duration=$((end_time - start_time))
test_durations+=("$duration")
-
+
log "✅ Test passed: $test_name ($(format_duration $duration))"
passed_tests+=("$test_name")
return 0
@@ -101,9 +102,9 @@ run_test() {
local end_time=$(date +%s)
local duration=$((end_time - start_time))
test_durations+=("$duration")
-
+
log "❌ Test failed: $test_name ($(format_duration $duration))"
-
+
if (( retry_count < RETRY_COUNT )); then
log "Retrying test: $test_name"
retry_count=$((retry_count + 1))
@@ -125,7 +126,7 @@ main() {
while IFS= read -r -d $'\0' file; do
targets+=("$file")
done < <(find integration_test/test_suites -name "*.dart" -type f -print0)
-
+
if [[ $? -ne 0 ]]; then
error "Failed to find test files"
exit 1
@@ -140,7 +141,7 @@ main() {
# Record overall start time
local overall_start_time=$(date +%s)
-
+
# Run tests sequentially
for target in "${targets[@]}"; do
run_test "$target"
@@ -156,7 +157,7 @@ main() {
echo "Passed: ${#passed_tests[@]}"
echo "Failed: ${#failed_tests[@]}"
echo "Total duration: $(format_duration $total_duration)"
-
+
if (( ${#passed_tests[@]} > 0 )); then
echo -e "\n✅ Passed Tests:"
for i in $(seq 0 $((${#passed_tests[@]} - 1))); do
diff --git a/lib/main.dart b/lib/main.dart
index fe6e9477..77430f08 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -96,6 +96,12 @@ Future<void> runAppWithZone({Key? topLevelKey}) async {
} catch (e) {
printV("Failed to initialize tor: $e");
}
+
+ try {
+ await linuxSymlinkSharedPreferences();
+ } catch (e) {
+ printV("Failed to symlink linux preferences: $e");
+ }
await initializeAppAtRoot();
diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
index 655abb5a..99cecc5d 100644
--- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
+++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
@@ -48,7 +48,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
final zanoIcon = Image.asset('assets/images/crypto/zano.webp', height: 24, width: 24);
final decredIcon = Image.asset('assets/images/crypto/decred.webp', height: 24, width: 24);
final dogeIcon = Image.asset('assets/images/crypto/dogecoin.webp', height: 24, width: 24);
- final nonWalletTypeIcon = Image.asset('assets/images/close.webp', height: 24, width: 24);
+ final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset(
'assets/images/new_wallet.png',
Why this scored 27/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.