dev
dart 355 lines 13.4 KB
Raw
1 import 'dart:convert';
2 import 'dart:io';
3 import 'package:cake_wallet/core/secure_storage.dart';
4 import 'package:cake_wallet/entities/get_encryption_key.dart';
5 import 'package:cake_wallet/entities/transaction_description.dart';
6 import 'package:cake_wallet/themes/utils/theme_list.dart';
7 import 'package:cw_core/root_dir.dart';
8 import 'package:cake_wallet/utils/device_info.dart';
9 import 'package:cw_core/utils/print_verbose.dart';
10 import 'package:cw_core/wallet_type.dart';
11 import 'package:flutter/foundation.dart';
12 import 'package:hive/hive.dart';
13 import 'package:cryptography/cryptography.dart';
14 import 'package:shared_preferences/shared_preferences.dart';
15 import 'package:archive/archive_io.dart' hide Mac;
16 import 'package:cw_core/cake_hive.dart';
17 import 'package:cake_wallet/core/key_service.dart';
18 import 'package:cake_wallet/entities/encrypt.dart';
19 import 'package:cake_wallet/entities/preferences_key.dart';
20 import 'package:cake_wallet/entities/secret_store_key.dart';
21 import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
22 import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
23 import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
24 import 'package:cw_core/wallet_info.dart';
25 import 'package:cake_wallet/exchange/trade_legacy.dart';
26 import 'package:cake_wallet/.secrets.g.dart' as secrets;
27 import 'package:cake_wallet/wallet_types.g.dart';
28 import 'package:cake_backup/backup.dart' as cake_backup;
29
30 class $BackupService {
31 $BackupService(
32 this._secureStorage, this.transactionDescriptionBox, this.keyService, this.sharedPreferences)
33 : cipher = Cryptography.instance.chacha20Poly1305Aead(),
34 correctWallets = <WalletInfo>[];
35
36 static const currentVersion = _v3;
37
38 static const _v2 = 2;
39 static const _v3 = 3;
40
41 final Cipher cipher;
42 final SecureStorage _secureStorage;
43 final SharedPreferences sharedPreferences;
44 final Box<TransactionDescription> transactionDescriptionBox;
45 final KeyService keyService;
46 List<WalletInfo> correctWallets;
47
48 Future<void> importBackupV1(Uint8List data, String password, {required String nonce}) async {
49 final appDir = await getAppDir();
50 final decryptedData = await _decryptV1(data, password, nonce);
51 final zip = ZipDecoder().decodeBytes(decryptedData);
52
53 for (var file in zip.files) {
54 final filename = file.name;
55
56 if (file.isFile) {
57 final content = file.content as List<int>;
58 File('${appDir.path}/' + filename)
59 ..createSync(recursive: true)
60 ..writeAsBytesSync(content, flush: true);
61 } else {
62 Directory('${appDir.path}/' + filename)..create(recursive: true);
63 }
64 }
65 ;
66
67 await verifyWallets();
68 await _importKeychainDumpV1(password, nonce: nonce);
69 await importPreferencesDump();
70 }
71
72 // checked with .endsWith - so this should be the last part of the filename
73 static const ignoreFiles = [
74 "flutter_assets/kernel_blob.bin",
75 "flutter_assets/vm_snapshot_data",
76 "flutter_assets/isolate_snapshot_data",
77 "README.txt",
78 ".lock",
79 ];
80
81 Future<void> importBackupV2(Uint8List data, String password) async {
82 final appDir = await getAppDir();
83 final decryptedData = await decryptV2(data, password);
84 final zip = ZipDecoder().decodeBytes(decryptedData);
85
86 outer:
87 for (var file in zip.files) {
88 final filename = file.name;
89 for (var ignore in ignoreFiles) {
90 if (filename.endsWith(ignore) && !filename.contains("wallets/")) {
91 printV("ignoring backup file: $filename");
92 continue outer;
93 }
94 }
95 printV("restoring: $filename");
96 if (file.isFile) {
97 final content = file.content as List<int>;
98 File('${appDir.path}/' + filename)
99 ..createSync(recursive: true)
100 ..writeAsBytesSync(content, flush: true);
101 } else {
102 final dir = Directory('${appDir.path}/' + filename);
103 if (!dir.existsSync()) {
104 dir.createSync(recursive: true);
105 }
106 }
107 }
108 ;
109
110 await verifyWallets();
111 await importKeychainDumpV2(password);
112 await importPreferencesDump();
113 await importTransactionDescriptionDump(); // HiveError: Box has already been closed
114 }
115
116 Future<void> verifyWallets() async {
117 await performHiveMigration(); // for backups made before sqlite migration
118 await performTradeHiveMigration(_secureStorage);
119 await performErc20TokenHiveMigration();
120 await performSplTokenHiveMigration();
121 await performTronTokenHiveMigration();
122
123 correctWallets = (await WalletInfo.getAll())
124 .where((info) => availableWalletTypes.contains(info.type))
125 .toList();
126
127 if (correctWallets.isEmpty) {
128 printV('Correct wallets not detected');
129 }
130 }
131
132 Future<void> importTransactionDescriptionDump() async {
133 final appDir = await getAppDir();
134 final transactionDescriptionFile = File('${appDir.path}/~_transaction_descriptions_dump');
135
136 if (!transactionDescriptionFile.existsSync()) {
137 return;
138 }
139
140 final jsonData =
141 json.decode(transactionDescriptionFile.readAsStringSync()) as Map<String, dynamic>;
142 final descriptionsMap = jsonData.map((key, value) =>
143 MapEntry(key, TransactionDescription.fromJson(value as Map<String, dynamic>)));
144 var box = transactionDescriptionBox;
145 if (!box.isOpen) {
146 final transactionDescriptionsBoxKey = await getEncryptionKey(
147 secureStorage: _secureStorage, forKey: TransactionDescription.boxKey);
148 box = await CakeHive.openBox<TransactionDescription>(TransactionDescription.boxName,
149 encryptionKey: transactionDescriptionsBoxKey);
150 }
151 await box.putAll(descriptionsMap);
152 }
153
154 Future<void> importPreferencesDump() async {
155 final appDir = await getAppDir();
156 final preferencesFile = File('${appDir.path}/~_preferences_dump');
157
158 if (!preferencesFile.existsSync()) {
159 return;
160 }
161
162 final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>;
163
164 try {
165 // shouldn't throw an error but just in case, so it doesn't stop the backup restore
166 for (var entry in data.entries) {
167 String key = entry.key;
168 dynamic value = entry.value;
169
170 // Check the type of the value and save accordingly
171 if (value is String) {
172 await sharedPreferences.setString(key, value);
173 } else if (value is int) {
174 await sharedPreferences.setInt(key, value);
175 } else if (value is double) {
176 await sharedPreferences.setDouble(key, value);
177 } else if (value is bool) {
178 await sharedPreferences.setBool(key, value);
179 } else if (value is List<String>) {
180 await sharedPreferences.setStringList(key, value);
181 } else {
182 if (kDebugMode) {
183 printV(
184 'Skipping individual save for key "$key": Unsupported type (${value.runtimeType}). Value: $value');
185 }
186 }
187 }
188 } catch (_) {}
189
190 String currentWalletName = data[PreferencesKey.currentWalletName] as String;
191 int currentWalletType = data[PreferencesKey.currentWalletType] as int;
192
193 final isCorrectCurrentWallet = correctWallets
194 .any((info) => info.name == currentWalletName && info.type.index == currentWalletType);
195
196 try {
197 if (!isCorrectCurrentWallet) {
198 currentWalletName = correctWallets.first.name;
199 currentWalletType = serializeToInt(correctWallets.first.type);
200 }
201 } catch (e) {}
202
203 if (DeviceInfo.instance.isDesktop) {
204 await sharedPreferences.setInt(PreferencesKey.currentTheme, ThemeList.darkTheme.raw);
205 }
206
207 await preferencesFile.delete();
208 }
209
210 Future<void> _importKeychainDumpV1(String password,
211 {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async {
212 final appDir = await getAppDir();
213 final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
214 final decryptedKeychainDumpFileData =
215 await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce);
216 final keychainJSON =
217 json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
218 final keychainWalletsInfo = keychainJSON['wallets'] as List;
219 final decodedPin = keychainJSON['pin'] as String;
220 final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
221 final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
222 final backupPassword = keychainJSON[backupPasswordKey] as String;
223
224 await _secureStorage.write(key: backupPasswordKey, value: backupPassword);
225
226 keychainWalletsInfo.forEach((dynamic rawInfo) async {
227 final info = rawInfo as Map<String, dynamic>;
228 await importWalletKeychainInfo(info);
229 });
230
231 await _secureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin));
232
233 keychainDumpFile.deleteSync();
234 }
235
236 Future<void> importKeychainDumpV2(String password,
237 {String keychainSalt = secrets.backupKeychainSalt}) async {
238 final appDir = await getAppDir();
239 final keychainDumpFile = File('${appDir.path}/~_keychain_dump');
240 final decryptedKeychainDumpFileData =
241 await decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password');
242 final keychainJSON =
243 json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>;
244 final keychainWalletsInfo = keychainJSON['wallets'] as List;
245 final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
246 final backupPassword = keychainJSON[backupPasswordKey] as String;
247
248 await _secureStorage.write(key: backupPasswordKey, value: backupPassword);
249
250 keychainWalletsInfo.forEach((dynamic rawInfo) async {
251 final info = rawInfo as Map<String, dynamic>;
252 await importWalletKeychainInfo(info);
253 });
254
255 if (keychainJSON['_all'] is Map<String, dynamic>) {
256 for (var key in (keychainJSON['_all'] as Map<String, dynamic>).keys) {
257 try {
258 if (!key.startsWith('MONERO_WALLET_')) continue;
259 final decodedPassword =
260 decodeWalletPassword(password: keychainJSON['_all'][key].toString());
261 final walletName = key.split('_WALLET_')[1];
262 final walletType = key.split('_WALLET_')[0].toLowerCase();
263 await importWalletKeychainInfo({
264 'name': walletName,
265 'type': "WalletType.$walletType",
266 'password': decodedPassword,
267 });
268 } catch (e) {
269 printV('Error importing wallet ($key) password: $e');
270 }
271 }
272 }
273
274 keychainDumpFile.deleteSync();
275 }
276
277 Future<void> importWalletKeychainInfo(Map<String, dynamic> info) async {
278 final name = info['name'] as String;
279 final password = info['password'] as String;
280
281 await keyService.saveWalletPassword(walletName: name, password: password);
282 }
283
284 Future<Uint8List> exportKeychainDumpV2(String password,
285 {String keychainSalt = secrets.backupKeychainSalt}) async {
286 final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
287 final wallets = await Future.wait((await WalletInfo.getAll()).map((walletInfo) async {
288 try {
289 return {
290 'name': walletInfo.name,
291 'type': walletInfo.type.toString(),
292 'password': await keyService.getWalletPassword(walletName: walletInfo.name),
293 'hardwareWalletType': walletInfo.hardwareWalletType?.index,
294 };
295 } catch (e) {
296 return {
297 'name': walletInfo.name,
298 'type': walletInfo.type.toString(),
299 'password': '',
300 'hardwareWalletType': walletInfo.hardwareWalletType?.index,
301 };
302 }
303 }));
304 final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword);
305 final backupPassword = await _secureStorage.read(key: backupPasswordKey);
306 final data = utf8.encode(json.encode({
307 'wallets': wallets,
308 backupPasswordKey: backupPassword,
309 '_all': await _secureStorage.readAll()
310 }));
311 final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password');
312
313 return encrypted;
314 }
315
316 static const List<String> _excludedPrefsKeys = [
317 PreferencesKey.currentPinLength,
318 PreferencesKey.showCameraConsent,
319 PreferencesKey.lastSeenAppVersion,
320 PreferencesKey.failedTotpTokenTrials,
321 ];
322
323 Future<String> exportPreferencesJSON() async {
324 final preferences = <String, dynamic>{};
325 sharedPreferences.getKeys().forEach((key) => preferences[key] = sharedPreferences.get(key));
326
327 _excludedPrefsKeys.forEach((key) => preferences.remove(key));
328
329 return json.encode(preferences);
330 }
331
332 int getVersion(Uint8List data) => data.toList().first;
333
334 Uint8List setVersion(Uint8List data, int version) {
335 final bytes = data.toList()..insert(0, version);
336 return Uint8List.fromList(bytes);
337 }
338
339 Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64,
340 {int macLength = 16}) async {
341 final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource));
342 final secretKey = SecretKey(secretKeyHash.bytes);
343 final nonce = base64.decode(nonceBase64).toList();
344 final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(),
345 nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength)));
346 final plainData = await cipher.decrypt(box, secretKey: secretKey);
347 return Uint8List.fromList(plainData);
348 }
349
350 Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async =>
351 cake_backup.encrypt(passphrase, data, version: _v2);
352
353 Future<Uint8List> decryptV2(Uint8List data, String passphrase) async =>
354 cake_backup.decrypt(passphrase, data);
355 }