diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 9b2b2c7d4..999b67120 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -16,6 +16,7 @@ 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'; +import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { BackupService(this._flutterSecureStorage, this._walletInfoSource, @@ -23,9 +24,10 @@ class BackupService { : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; - static const currentVersion = _v1; + static const currentVersion = _v2; static const _v1 = 1; + static const _v2 = 2; final Cipher _cipher; final FlutterSecureStorage _flutterSecureStorage; @@ -37,13 +39,16 @@ class BackupService { 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: + final backupBytes = data.toList()..removeAt(0); + final backupData = Uint8List.fromList(backupBytes); await _importBackupV1(backupData, password, nonce: nonce); break; + case _v2: + await _importBackupV2(data, password); + break; default: break; } @@ -54,20 +59,26 @@ class BackupService { switch (version) { case _v1: return await _exportBackupV1(password, nonce: nonce); + case _v2: + return await _exportBackupV2(password); default: throw Exception('Incorrect version: $version for exportBackup'); } } + @Deprecated('Use v2 instead') Future _exportBackupV1(String password, - {String nonce = secrets.backupSalt}) async { + {String nonce = secrets.backupSalt}) async + => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); + + Future _exportBackupV2(String password) 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 keychainDump = await _exportKeychainDumpV2(password); final preferencesDump = await _exportPreferencesJSON(); final preferencesDumpFile = File('${tmpDir.path}/~_preferences_dump_TMP'); final keychainDumpFile = File('${tmpDir.path}/~_keychain_dump_TMP'); @@ -98,15 +109,13 @@ class BackupService { final content = File(archivePath).readAsBytesSync(); tmpDir.deleteSync(recursive: true); - final encryptedData = await _encrypt(content, password, nonce); - - return setVersion(encryptedData, currentVersion); + return await _encryptV2(content, password); } Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); - final decryptedData = await _decrypt(data, password, nonce); + final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); zip.files.forEach((file) { @@ -123,7 +132,30 @@ class BackupService { }); await _verifyWallets(); - await _importKeychainDump(password, nonce: nonce); + await _importKeychainDumpV1(password, nonce: nonce); + await _importPreferencesDump(); + } + + Future _importBackupV2(Uint8List data, String password) async { + final appDir = await getApplicationDocumentsDirectory(); + final decryptedData = await _decryptV2(data, password); + 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 _importKeychainDumpV2(password); await _importPreferencesDump(); } @@ -258,12 +290,12 @@ class BackupService { await preferencesFile.delete(); } - Future _importKeychainDump(String password, + Future _importKeychainDumpV1(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( + final decryptedKeychainDumpFileData = await _decryptV1( keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; @@ -288,6 +320,35 @@ class BackupService { keychainDumpFile.deleteSync(); } + Future _importKeychainDumpV2(String password, + {String keychainSalt = secrets.backupKeychainSalt}) async { + final appDir = await getApplicationDocumentsDirectory(); + final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); + final decryptedKeychainDumpFileData = await _decryptV2( + keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + 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; @@ -295,9 +356,14 @@ class BackupService { await _keyService.saveWalletPassword(walletName: name, password: password); } - Future _exportKeychainDump(String password, + @Deprecated('Use v2 instead') + Future _exportKeychainDumpV1(String password, {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async { + String keychainSalt = secrets.backupKeychainSalt}) async + => throw Exception('Deprecated'); + + Future _exportKeychainDumpV2(String password, + {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); @@ -319,49 +385,48 @@ class BackupService { 'wallets': wallets, backupPasswordKey: backupPassword })); - final encrypted = await _encrypt( - Uint8List.fromList(data), '$keychainSalt$password', nonce); + final encrypted = await _encryptV2( + Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; } Future _exportPreferencesJSON() async { - // FIX-ME: Force unwrap final preferences = { PreferencesKey.currentWalletName: - _sharedPreferences.getString(PreferencesKey.currentWalletName)!, + _sharedPreferences.getString(PreferencesKey.currentWalletName), PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey)!, + _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey)!, + .getInt(PreferencesKey.currentBalanceDisplayModeKey), PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType)!, + _sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: - _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!, + _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey)!, + .getBool(PreferencesKey.shouldSaveRecipientAddressKey), PreferencesKey.isDarkThemeLegacy: - _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy)!, + _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength)!, + _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy)!, + .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey)!, + .getBool(PreferencesKey.allowBiometricalAuthenticationKey), PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey)!, + .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: - _sharedPreferences.getString(PreferencesKey.currentLanguageCode)!, + _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: - _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey)!, + _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme)!, + _sharedPreferences.getInt(PreferencesKey.currentTheme), PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion)!, + .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!, + _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: - _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!, + _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), }; return json.encode(preferences); @@ -374,16 +439,12 @@ class BackupService { 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); - } + @Deprecated('Use v2 instead') + Future _encryptV1( + Uint8List data, String secretKeySource, String nonceBase64) async + => throw Exception('Deprecated'); - Future _decrypt( + Future _decryptV1( Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); @@ -395,4 +456,12 @@ class BackupService { final plainData = await _cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } + + Future _encryptV2( + Uint8List data, String passphrase) async + => cake_backup.encrypt(passphrase, data, version: _v2); + + Future _decryptV2( + Uint8List data, String passphrase) async + => cake_backup.decrypt(passphrase, data); } diff --git a/lib/src/screens/backup/backup_page.dart b/lib/src/screens/backup/backup_page.dart index f55388943..a055066c0 100644 --- a/lib/src/screens/backup/backup_page.dart +++ b/lib/src/screens/backup/backup_page.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -// import 'package:esys_flutter_share/esys_flutter_share.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -103,12 +103,14 @@ class BackupPage extends BasePage { Navigator.of(dialogContext).pop(); final backup = await backupViewModelBase.exportBackup(); + if (backup == null) { + return; + } + if (Platform.isAndroid) { - onExportAndroid(context, backup!); + onExportAndroid(context, backup); } else { - // FIX-ME: Share esys_flutter_share.dart - // await Share.file(S.of(context).backup_file, backup.name, - // backup.content, 'application/*'); + await share(backup); } }, actionLeftButton: () => Navigator.of(dialogContext).pop()); @@ -136,12 +138,20 @@ class BackupPage extends BasePage { backup.name, backup.content); Navigator.of(dialogContext).pop(); }, - actionLeftButton: () { + actionLeftButton: () async { Navigator.of(dialogContext).pop(); - // FIX-ME: Share esys_flutter_share.dart - // Share.file(S.of(context).backup_file, backup.name, - // backup.content, 'application/*'); + await share(backup); }); }); } + + Future share(BackupExportFile backup) async { + const mimeType = 'application/*'; + final path = await backupViewModelBase.saveBackupFileLocally(backup); + await Share.shareXFiles([XFile( + path, + name: backup.name, + mimeType: mimeType)]); + await backupViewModelBase.removeBackupFileLocally(backup); + } } diff --git a/lib/src/screens/dashboard/wallet_menu.dart b/lib/src/screens/dashboard/wallet_menu.dart index c313eac8f..68e7cc76d 100644 --- a/lib/src/screens/dashboard/wallet_menu.dart +++ b/lib/src/screens/dashboard/wallet_menu.dart @@ -51,6 +51,21 @@ class WalletMenu { image: Image.asset('assets/images/open_book_menu.png', height: 16, width: 16), handler: () => Navigator.of(context).pushNamed(Routes.addressBook)), + WalletMenuItem( + title: S.current.backup, + image: Image.asset('assets/images/restore_wallet.png', + height: 16, + width: 16, + color: Palette.darkBlue), + handler: () { + Navigator.of(context).pushNamed( + Routes.auth, + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { + if (isAuthenticatedSuccessfully) { + auth.close(route:Routes.backup); + } + }); + }), WalletMenuItem( title: S.current.settings_title, image: Image.asset('assets/images/settings_menu.png', diff --git a/lib/view_model/backup_view_model.dart b/lib/view_model/backup_view_model.dart index 2d75e2000..8cc651b17 100644 --- a/lib/view_model/backup_view_model.dart +++ b/lib/view_model/backup_view_model.dart @@ -8,6 +8,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mobx/mobx.dart'; import 'package:intl/intl.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:path_provider/path_provider.dart'; part 'backup_view_model.g.dart'; @@ -71,6 +72,21 @@ abstract class BackupViewModelBase with Store { } } + Future saveBackupFileLocally(BackupExportFile backup) async { + final appDir = await getApplicationDocumentsDirectory(); + final path = '${appDir.path}/${backup.name}'; + final backupFile = File(path); + await backupFile.writeAsBytes(backup.content); + return path; + } + + Future removeBackupFileLocally(BackupExportFile backup) async { + final appDir = await getApplicationDocumentsDirectory(); + final path = '${appDir.path}/${backup.name}'; + final backupFile = File(path); + await backupFile.delete(); + } + @action void showMasterPassword() => isBackupPasswordVisible = true; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 7a3abd3bc..248a06de0 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -61,6 +61,11 @@ dependencies: permission_handler: ^10.0.0 device_display_brightness: ^0.0.6 platform_device_id: ^1.0.1 + cake_backup: + git: + url: https://github.com/cake-tech/cake_backup.git + ref: main + version: 1.0.0 dev_dependencies: flutter_test: