import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/wallet_types.g.dart'; class BackupService { BackupService(this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; static const currentVersion = _v1; static const _v1 = 1; final Cipher _cipher; final FlutterSecureStorage _flutterSecureStorage; final SharedPreferences _sharedPreferences; final Box _walletInfoSource; final KeyService _keyService; List _correctWallets; Future importBackup(Uint8List data, String password, {String nonce = secrets.backupSalt}) async { final version = getVersion(data); final backupBytes = data.toList()..removeAt(0); final backupData = Uint8List.fromList(backupBytes); switch (version) { case _v1: await _importBackupV1(backupData, password, nonce: nonce); break; default: break; } } Future exportBackup(String password, {String nonce = secrets.backupSalt, int version = currentVersion}) async { switch (version) { case _v1: return await _exportBackupV1(password, nonce: nonce); default: throw Exception('Incorrect version: $version for exportBackup'); } } Future _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async { final zipEncoder = ZipFileEncoder(); final appDir = await getApplicationDocumentsDirectory(); final now = DateTime.now(); final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP'); final archivePath = '${tmpDir.path}/backup_${now.toString()}.zip'; final fileEntities = appDir.listSync(recursive: false); final keychainDump = await _exportKeychainDump(password, nonce: nonce); final preferencesDump = await _exportPreferencesJSON(); final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); if (tmpDir.existsSync()) { tmpDir.deleteSync(recursive: true); } tmpDir.createSync(); zipEncoder.create(archivePath); fileEntities.forEach((entity) { if (entity.path == archivePath || entity.path == tmpDir.path) { return; } if (entity.statSync().type == FileSystemEntityType.directory) { zipEncoder.addDirectory(Directory(entity.path)); } else { zipEncoder.addFile(File(entity.path)); } }); await keychainDumpFile.writeAsBytes(keychainDump.toList()); await preferencesDumpFile.writeAsString(preferencesDump); await zipEncoder.addFile(preferencesDumpFile, '~_preferences_dump'); await zipEncoder.addFile(keychainDumpFile, '~_keychain_dump'); zipEncoder.close(); final content = File(archivePath).readAsBytesSync(); tmpDir.deleteSync(recursive: true); final encryptedData = await _encrypt(content, password, nonce); return setVersion(encryptedData, currentVersion); } Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decrypt(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); zip.files.forEach((file) { final filename = file.name; if (file.isFile) { final content = file.content as List; File('${appDir.path}/' + filename) ..createSync(recursive: true) ..writeAsBytesSync(content); } else { Directory('${appDir.path}/' + filename)..create(recursive: true); } }); await _verifyWallets(); await _importKeychainDump(password, nonce: nonce); await _importPreferencesDump(); } Future _verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); _correctWallets = walletInfoSource .values .where((info) => availableWalletTypes.contains(info.type)) .toList(); if (_correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); } } Future> _reloadHiveWalletInfoBox() async { final appDir = await getApplicationDocumentsDirectory(); await Hive.close(); Hive.init(appDir.path); if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { Hive.registerAdapter(WalletInfoAdapter()); } return await Hive.openBox(WalletInfo.boxName); } Future _importPreferencesDump() async { final appDir = await getApplicationDocumentsDirectory(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); if (!preferencesFile.existsSync()) { return; } final data = json.decode(preferencesFile.readAsStringSync()) as Map; String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; final isCorrentCurrentWallet = _correctWallets .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { currentWalletName = _correctWallets.first.name; currentWalletType = serializeToInt(_correctWallets.first.type); } await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, data[PreferencesKey.currentNodeIdKey] as int); await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, data[PreferencesKey.currentBalanceDisplayModeKey] as int); await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, data[PreferencesKey.currentFiatCurrencyKey] as String); await _sharedPreferences.setBool( PreferencesKey.shouldSaveRecipientAddressKey, data[PreferencesKey.shouldSaveRecipientAddressKey] as bool); await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int); await _sharedPreferences.setBool( PreferencesKey.allowBiometricalAuthenticationKey, data[PreferencesKey.allowBiometricalAuthenticationKey] as bool); await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int); await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, data[PreferencesKey.currentLanguageCode] as String); await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, data[PreferencesKey.displayActionListModeKey] as int); await _sharedPreferences.setInt(PreferencesKey.currentPinLength, data[PreferencesKey.currentPinLength] as int); await _sharedPreferences.setInt( PreferencesKey.currentTheme, data[PreferencesKey.currentTheme] as int); await _sharedPreferences.setInt( PreferencesKey.currentDefaultSettingsMigrationVersion, data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int); await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority, data[PreferencesKey.moneroTransactionPriority] as int); await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, data[PreferencesKey.bitcoinTransactionPriority] as int); await preferencesFile.delete(); } Future _importKeychainDump(String password, {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); final decryptedKeychainDumpFileData = await _decrypt( keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; await _flutterSecureStorage.write( key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); await _flutterSecureStorage.write( key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } Future importWalletKeychainInfo(Map info) async { final name = info['name'] as String; final password = info['password'] as String; await _keyService.saveWalletPassword(walletName: name, password: password); } Future _exportKeychainDump(String password, {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); final data = utf8.encode(json.encode({ 'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword })); final encrypted = await _encrypt( Uint8List.fromList(data), '$keychainSalt$password', nonce); return encrypted; } Future _exportPreferencesJSON() async { // FIX-ME: Force unwrap final preferences = { PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName)!, PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey)!, PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences .getInt(PreferencesKey.currentBalanceDisplayModeKey)!, PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType)!, PreferencesKey.currentFiatCurrencyKey: _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!, PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences .getBool(PreferencesKey.shouldSaveRecipientAddressKey)!, PreferencesKey.isDarkThemeLegacy: _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy)!, PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength)!, PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!, PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey)!, PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey)!, PreferencesKey.currentLanguageCode: _sharedPreferences.getString(PreferencesKey.currentLanguageCode)!, PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey)!, PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme)!, PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion)!, PreferencesKey.bitcoinTransactionPriority: _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!, PreferencesKey.moneroTransactionPriority: _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!, }; return json.encode(preferences); } int getVersion(Uint8List data) => data.toList().first; Uint8List setVersion(Uint8List data, int version) { final bytes = data.toList()..insert(0, version); return Uint8List.fromList(bytes); } Future _encrypt( Uint8List data, String secretKeySource, String nonceBase64) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); final nonce = base64.decode(nonceBase64).toList(); final box = await _cipher.encrypt(data.toList(), secretKey: secretKey, nonce: nonce); return Uint8List.fromList(box.cipherText); } Future _decrypt( Uint8List data, String secretKeySource, String nonceBase64) async { throw Exception('Unimplemented'); //final secretKeyHash = await sha256.hash(utf8.encode(secretKeySource)); //final secretKey = SecretKey(secretKeyHash.bytes); //final nonce = Nonce(base64.decode(nonceBase64)); //return await _cipher.decrypt(data, secretKey: secretKey, nonce: nonce); // final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); // final secretKey = SecretKey(secretKeyHash.bytes); // final nonce = base64.decode(nonceBase64).toList(); // final box = SecretBox(data.toList(), nonce: nonce, mac: Mac); // final plainData = await _cipher.decrypt(box, secretKey: secretKey); // return Uint8List.fromList(plainData); } }