feat(dev): add secrets dev screen (used to recover corrupted wallet) (#2234)

* feat(dev): add secrets dev screen (used to recover corrupted wallet) * fix(dev): decode only password fields

cyan committed Apr 30, 2025 at 08:29 UTC da8a82a10f1d915342bcf73e7a4965a39dea2b1d
7 files changed +173
lib/core/backup_service.dart
+16
@@ -251,6 +251,22 @@ class $BackupService {
251 await importWalletKeychainInfo(info);
252 });
253
254 + for (var key in (keychainJSON['_all'] as Map<String, dynamic>).keys) {
255 + try {
256 + if (!key.startsWith('MONERO_WALLET_')) continue;
257 + final decodedPassword = decodeWalletPassword(password: keychainJSON['_all'][key].toString());
258 + final walletName = key.split('_WALLET_')[1];
259 + final walletType = key.split('_WALLET_')[0].toLowerCase();
260 + await importWalletKeychainInfo({
261 + 'name': walletName,
262 + 'type': "WalletType.$walletType",
263 + 'password': decodedPassword,
264 + });
265 + } catch (e) {
266 + printV('Error importing wallet ($key) password: $e');
267 + }
268 + }
269 +
270 keychainDumpFile.deleteSync();
271 }
272
lib/di.dart
+6
@@ -33,12 +33,14 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
33 import 'package:cake_wallet/haven/cw_haven.dart';
34 import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
35 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
36 +import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
37 import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
38 import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
39 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
40 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
41 import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
42 import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
43 +import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
44 import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
45 import 'package:cake_wallet/view_model/link_view_model.dart';
46 import 'package:cake_wallet/tron/tron.dart';
@@ -920,6 +922,8 @@ Future<void> setup({
922
923 getIt.registerFactory(() => DevSharedPreferences());
924
925 + getIt.registerFactory(() => DevSecurePreferences());
926 +
927 getIt.registerFactoryParam<WalletSeedPage, bool, void>((bool isWalletCreated, _) =>
928 WalletSeedPage(getIt.get<WalletSeedViewModel>(), isNewWalletCreated: isWalletCreated));
929
@@ -1463,6 +1467,8 @@ Future<void> setup({
1467 getIt.registerFactory(() => DevMoneroCallProfilerPage());
1468
1469 getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
1470 +
1471 + getIt.registerFactory(() => DevSecurePreferencesPage(getIt.get<DevSecurePreferences>()));
1472
1473 getIt.registerFactory(() => BackgroundSyncLogsViewModel());
1474
lib/router.dart
+6
@@ -36,6 +36,7 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
36 import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
37 import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
38 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
39 +import 'package:cake_wallet/src/screens/dev/secure_preferences_page.dart';
40 import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
41 import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
42 import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
@@ -853,6 +854,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
854 builder: (_) => getIt.get<DevMoneroCallProfilerPage>(),
855 );
856
857 + case Routes.devSecurePreferences:
858 + return MaterialPageRoute<void>(
859 + builder: (_) => getIt.get<DevSecurePreferencesPage>(),
860 + );
861 +
862 default:
863 return MaterialPageRoute<void>(
864 builder: (_) => Scaffold(
lib/routes.dart
+1
@@ -115,6 +115,7 @@ class Routes {
115 static const devMoneroBackgroundSync = '/dev/monero_background_sync';
116 static const devMoneroCallProfiler = '/dev/monero_call_profiler';
117 static const devSharedPreferences = '/dev/shared_preferences';
118 + static const devSecurePreferences = '/dev/secure_preferences';
119 static const devBackgroundSyncLogs = '/dev/background_sync_logs';
120
121 static const signPage = '/sign_page';
lib/src/screens/dev/secure_preferences_page.dart new
+63
@@ -0,0 +1,63 @@
1 +import 'package:cake_wallet/src/screens/base_page.dart';
2 +import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
3 +import 'package:flutter/material.dart';
4 +import 'package:flutter/services.dart';
5 +import 'package:flutter_mobx/flutter_mobx.dart';
6 +
7 +class DevSecurePreferencesPage extends BasePage {
8 + final DevSecurePreferences viewModel;
9 +
10 + DevSecurePreferencesPage(this.viewModel);
11 +
12 + @override
13 + String? get title => "[dev] secure preferences";
14 +
15 + @override
16 + Widget body(BuildContext context) {
17 + return Observer(
18 + builder: (_) {
19 + if (viewModel.values.isEmpty) {
20 + return Center(child: Text("No secure preferences found"));
21 + }
22 + final keys = viewModel.keys;
23 + Map<String, dynamic> values = {};
24 + for (final key in keys) {
25 + values[key] = viewModel.get(key);
26 + }
27 + Map<String, PreferenceType> types = {};
28 + for (final key in keys) {
29 + types[key] = viewModel.getPreferenceType(key);
30 + }
31 + return ListView.builder(
32 + itemCount: keys.length,
33 + itemBuilder: (context, index) {
34 + final key = keys[index];
35 + final type = types[key]!;
36 + return ListTile(
37 + onTap: () {
38 + Clipboard.setData(ClipboardData(text: key + ": " + values[key].toString()));
39 + },
40 + title: switch (type) {
41 + PreferenceType.bool => Text(key, style: TextStyle(color: Colors.blue)),
42 + PreferenceType.int => Text(key, style: TextStyle(color: Colors.green)),
43 + PreferenceType.double => Text(key, style: TextStyle(color: Colors.yellow)),
44 + PreferenceType.listString => Text(key, style: TextStyle(color: Colors.purple)),
45 + PreferenceType.string => Text(key),
46 + PreferenceType.unknown => Text(key),
47 + },
48 + subtitle: switch (type) {
49 + PreferenceType.bool => Text("bool: ${values[key]}"),
50 + PreferenceType.int => Text("int: ${values[key]}"),
51 + PreferenceType.double => Text("double: ${values[key]}"),
52 + PreferenceType.listString => values[key].isEmpty as bool ? Text("listString: []") : Text("listString:\n- ${values[key].join("\n- ")}"),
53 + PreferenceType.string => Text("string: ${values[key]}"),
54 + PreferenceType.unknown => Text("UNKNOWN(${values[key].runtimeType}): ${values[key]}"),
55 + },
56 + );
57 + },
58 + );
59 + },
60 + );
61 + }
62 +
63 +}
\ No newline at end of file
lib/src/screens/settings/other_settings_page.dart
+6
@@ -81,6 +81,12 @@ class OtherSettingsPage extends BasePage {
81 handler: (BuildContext context) =>
82 Navigator.of(context).pushNamed(Routes.devSharedPreferences),
83 ),
84 + if (FeatureFlag.hasDevOptions)
85 + SettingsCellWithArrow(
86 + title: '[dev] secure storage preferences',
87 + handler: (BuildContext context) =>
88 + Navigator.of(context).pushNamed(Routes.devSecurePreferences),
89 + ),
90 if (FeatureFlag.hasDevOptions)
91 SettingsCellWithArrow(
92 title: '[dev] background sync logs',
lib/view_model/dev/secure_preferences.dart new
+75
@@ -0,0 +1,75 @@
1 +import 'package:cake_wallet/core/secure_storage.dart';
2 +import 'package:cake_wallet/entities/encrypt.dart';
3 +import 'package:mobx/mobx.dart';
4 +
5 +part 'secure_preferences.g.dart';
6 +
7 +class DevSecurePreferences = DevSecurePreferencesBase with _$DevSecurePreferences;
8 +
9 +enum PreferenceType {
10 + unknown,
11 + string,
12 + int,
13 + double,
14 + bool,
15 + listString
16 +}
17 +
18 +abstract class DevSecurePreferencesBase with Store {
19 + DevSecurePreferencesBase() {
20 + secureStorageShared.readAll().then((value) {
21 + values = value;
22 + });
23 + }
24 +
25 + @observable
26 + Map<String, String> values = {};
27 +
28 + @computed
29 + List<String> get keys => values.keys.toList()..sort();
30 +
31 + @action
32 + Future<void> delete(String key) async {
33 +
34 + }
35 +
36 + dynamic get(String key) {
37 + if (!values.containsKey(key)) {
38 + return null;
39 + }
40 + if (!key.startsWith("MONERO_WALLET_")) return values[key]!;
41 + try {
42 + final decodedPassword = decodeWalletPassword(password: values[key]!);
43 + return values[key]! + "\n\nDecoded: $decodedPassword";
44 + } catch (e) {
45 + return values[key]! +"\n$e";
46 + }
47 + }
48 +
49 + Future<void> set(String key, PreferenceType type, dynamic value) async {
50 +
51 + }
52 +
53 + PreferenceType getPreferenceType(String key) {
54 + if (!values.containsKey(key)) {
55 + return PreferenceType.unknown;
56 + }
57 + final value = values[key];
58 + if (value is String) {
59 + return PreferenceType.string;
60 + }
61 + if (value is bool) {
62 + return PreferenceType.bool;
63 + }
64 + if (value is int) {
65 + return PreferenceType.int;
66 + }
67 + if (value is double) {
68 + return PreferenceType.double;
69 + }
70 + if (value is List<String>) {
71 + return PreferenceType.listString;
72 + }
73 + return PreferenceType.unknown;
74 + }
75 +}